VB.NET C# 文字列 | |
|
文字列は複雑で有る
リテラルの文字列と文字列変数の参照が同じとは
次のコードを見て欲しい。C# コード
string s = "nero"; VB.NET コード
string s = "nero";
objectにはReferenceEqualsというメソドが有って引数の2つが同じオブジェクトのインスタンスを
参照している場合はtrueを返します。 それでは上のコードは何を返すのでしょうか? "nero"と言うリテラルの文字列と"s"の参照を比較しています。 殆どの人は「False」を返すと思うかもしれませんが、実は「True」を返します。 参照とは殆どアドレスを同意語です、リテラルの"nero"が参照を返すと言うことはどの様なことなのでしょう。 その答えを書く前にもう1つ数字リテラルで同じコードを書いて見ます。 (注)リテラルとはソースコード内に値を直接表記したものを言います。 文字列の場合" "内の文字を 文字リテラルと言います、上の場合はneroが文字リテラルです。 又1や2と言った数字は数字リテラルを言います。 C# コード
int i = 10; VB.NET コード
Dim i As Integer = 10 このコードは予想通り「False」を返します。 文字リテラルと数字リテラルは何が異なるのでしょうか? 文字列は不変で有る
つまり一度作成された文字列を変更することは出来ません。 コードにより文字列の長さや内容を変えようとするとCLRは新しい文字列を作ってそれを参照させます。 そして参照が終わった古い文字列は適当なタイミングでガベージコレクションされます。 又文字列は値型ではなく参照型です、つまり文字列はスタックに作成されずにヒープ領域に作成されます。 文字列を引数で引き渡す場合は参照が渡されますが、渡された側で文字列が変更されることは有りません。 なぜなら文字列は不変だからです。 文字列はどの様な長さになるか決まっていません、1文字かも知れませんし1Gに近いものかも知れません。 この様な物をスタックに置くことは出来ませんので参照型にしてヒープ領域に作成することは当然と言えましょう。 更にフレームワークにはマルチスレッドを行うと言う使命が有ります、もし文字列が不変で無い場合は、スレッド間の 同期を取る為に幾つかの同じ文字列を作成しなければならなくなるでしょう。 文字列が不変で有ると言うことで、同じ文字列を幾つも作る必要が無くメモリーが節約されます。 いま文字列が沢山有るプログラムを走らせたとします。 CLRはどの様にしてヒープ上の沢山有る文字列から目的の文字列を探し出すのでしょうか? 最初に示したコードの実行結果もまだ説明していません、これらを説明するには 文字列の「インターンプール」と呼ばれるメカニズムを理解しなければなりません。 (注)1、CLR Common Language Runtime--Microsoft社が提唱するMicrosoft .NETで、.NET対応プログラムが 使用する共通動作環境。 2、ヒープとは動的に確保可能なメモリの領域です、配列やクラス等の参照型のデータはここに格納されます。 一方Int等の値型のデータはスタックと呼ばれる領域に格納されます。 ヒープに格納されたデータは使用後(参照が終わったら)はガベージコレクションによって開放されます。 文字列のインターンプール
いま仮に同じ文字の文字列を3つ作成したとしますstring s1 = "nero";この場合"nero"はヒープ領域に3つ作成されるのでしょうか? その答えはNOです。 リテラル値"nero"が有るとCLRはヒープの中に"nero"を格納します。 そして其の参照(アドレス)をs1に設定します。 2番目のs2="nero"が来ると、CLRは先ず"nero"をヒープの中から探しもし有れば、 其の参照をs2に設定します。 そしてこの時点で"nero"の参照(アドレス)とs1、s2の値が一致することになります。 ヒープの中に2つ"nero"を作成するのでなく、文字列が同じなら、同じ文字列の参照を設定するだけなので。 すなわちヒープの中には同じ文字列は存在しないのです。 冒頭のstring s = "nero"; object.ReferenceEquals ("nero",s));がTrueを返した理由が お判りになったと思います。 実はCLRは上の操作にハッシュテーブルを使用します。 プログラムが実行されると、CLRは内部に文字列参照のハッシュテーブルを作成します。 このハッシュテーブルはキーが文字列で値がヒープの中の参照です。 プログラム起動時は当然のことですがハッシュテーブルは空です。 先ず一番初めのstring s1 = "nero";が来た時JITは ハッシュテーブの中の"nero"を検索し、 見つからない為、新しくヒープに"nero"を作成し、ハッシュテーブルKeyと値である参照を追加します。 2番目の"nero"はもうお判りでしょう、ハッシュテーブルから同じKeyを持つ"nero"を探し、 見つかるのでs2その値であるヒープの参照(アドレス)をs2に設定します。 従ってs1とs2は同じヒープ上の"nero"の参照を持ちます。 この後幾つ"nero"を作成してもそれはヒープの参照を設定するだけなのです。 ハッシュテーブルを使用することによりヒープの中の文字列の検索を高速にしているのです。 そしてこれらの方式はインターンプールと呼ばれます。 文字列はインターンにより同じインスタンスは1つだけで管理され、メモリの節約と速度の向上が図られるのです。 インターンプールに登録される文字はプログラム内で宣言または作成されたリテラルな文字列です。 (注)ハッシュテーブル キーと値の組(エントリと呼ぶ)を複数個格納し、キーに対応する値をすばやく参照するための データ構造。 文字列の比較の高速化
objectにはデフォルトで2つのメソドが有ります。1つ目はObject.ReferenceEqualsで2つめはObject.Equalsです。 ReferenceEqualsはobjectの参照が同じで有るか否かを調べ、同じなら「True」を返します。 2つめはobjectそのものが等しいか否かを返すものです。 比較の速度はアドレスを比べるだけの1の方が当然速いことになります。 文字列の比較 String.Equalsはこのメソド使用して、先ずObject.ReferenceEqualsで2つ文字列の ハッシュテーブルが有ればハッシュテーブルで比較します。 もし2つの文字列の一方又は両方がハッシュテーブルに無ければ、Object.Equalsで文字列そのものを 比較します。 次のコードを見て戴きたい。 C# コード
System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch();
これはあらかじめ設定された文字列と合成の文字列の比較です。
(比較の中身は何もしていないのでワーニングが出ますが実行には支障が無いので無視して下さい。) 文字sはInternの説明の様にハッシュテーブルに登録されますが、合成されたs7はハッシュテーブルには 登録されません。 従ってこの比較はObject.Equalsを使用することになります。 コードはこの比較を1000000回させてその処理時間を表示させています。 実行結果は、44msecとなりました、もちろん環境依存であります。 回数の割りに処理時間が文字会理由は、ヒープ上の文字列が少ない為です。 StringクラスにはInternというメソドが有ります。 Intern メソッドは、インターン プールを使用して、引数 と等しい文字列を検索します。 そしてそのような文字列が存在する場合は、インターン プール内の該当する参照が返されます。 文字列が存在しない場合は、引数の文字列がインターン プールに追加された後、その参照が返されます。 つまりs7がハッシュテーブルの参照を返すようにすれば速度が向上することになるわけです。 C# コード
System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); VB.NET コード
Dim st As System.Diagnostics.Stopwatch = New System.Diagnostics.Stopwatch() 上のコードは2つの処理時間を書き出します。 1番目のs7はヒープ上には存在しますが、ハッシュテーブルに有りません。 従ってハッシュテーブルどうしの比較は出来ません。 2番目はs7 = String.Intern(s7)のコードが有ります。 これはs7の文字列をハッシュテーブルから検索してもし有れば、其の参照を返します。 このコードの場合、s7の文字列が既にハッシュテーブルに存在する為に、s7がハッシュテーブルの 参照を指すようになり比較はハッシュテーブル上の参照の比較になります。 この結果は44msecと5msecを示し、結果は期待通りのものでした。 ただしInternを使用する場合、Internの手続きに時間がかかります。 更に内部のハッシュテーブルの保持している文字列はガベージコレクションでは開放できません。 従ってInternは長い文字列の比較を何回も行なう場合のみに使用すべきです。 インデクサによる文字の切り出し
文字列の最初の文字から終わりまでの文字のアスキーコードを書き出すプログラムを作ってみよう。先ず文字からアスキーコードを取得するには以下のコードを使用する。 C# (int)'a';さてここで文字列から文字を切り出す方法で有るが以下のコードを見ていただきたい。 C# コード
string s = "nero"; VB.NET コード
Dim s As String = "nero"
上のコードのs[i] 、s(i)は一体何であろうか?
これは配列の様に見えるが配列では有りません、インデクサと呼ばれるプロパティの一種なのです。 インデクサに関しては「 インデクサ 」を御覧下さい。 Stringクラスはインデクサを持っていて、文字列の中の引数の文字を返します。 つまりs[i]は先頭からi番目の文字を返すのです。 これを読まれた方は文字列の有る特定の1文字をインデクサで変更しようとするかもしれませんが、 文字列のインデクサは読み取り専用で変更は出来ません。 文字列は変更できないと言う大前提を思い出して下さい。 文字列の一文字の変更はStringBuilderクラスのインデクサを使用します。 これに関しては下の「StringBuilderのインデクサ」の中にサンプルを載せて有ります。 動的な文字列操作を行なうStringBuilder
文字列が不変で有ると言うことは都合の悪いことが生じる場合が有ります。
その1つが文字列の動的な連結です。 string s1 = "長安 一片の月\r\n";上のコードを実行するとJITは文字列が連結されごとに新しい文字列を作成して、古い文字列が ガベージコレクションによって消されます。これは大変に時間のかかる処理です。 このように動的に頻繁に文字列が変更される場合はStringBuilderクラスを使用します。 StringBuilderクラスは内部に文字(Char)の配列を持っています。 文字列の足しこみは StringBuilderのAppendメソドに文字列を指定すれば良いだけです。 足しこむ文字の最大数がわかっていたらCapacityプロパティに設定します。 Capcityプロパティに長さを設定しない場合や、設定した長さを超えて文字列を格納しようとした場合は、 設定しようとした文字列の2倍の配列を確保し、文字の配列をコピーします。 従って最大容量を指定しない場合StringBuilderの速度パフォーマンスは低下することになります。 又これは大切なことですが、StringBuilderから文字列に直す場合は、StringBuildeのToStringメソドを 使用しますが、これは配列そのものをコピーするのではなく、StringBuilde配列の参照を、 文字列に設定するだけですので高速です。 以下文字列の連結を文字列で行なった場合と、あえて最大容量を指定しないでStringBuilderを 使用した場合の時間に比較です。 C# コード
string s1 = "長安 一片の月\r\n"; VB.NET コード
Dim s1 As String = "長安 一片の月\r\n"
実行結果は文字列の連結が1秒近くかかったのに比べてStringBuilder使用時は、測定不能であった。
文字列を何回も連結した削ったりする場合はStringBuilderを使用すべきなのです。 StringBuilderのインデクサ
StringBuilderを使用すると、文字列の一部分の書き換えなどがインデクサで簡単に行なえます。C# コード
string s1 = "長安 一片の月"; VB.NET コード
Dim s1 As String = "長安 一片の月"
結果は「長城 一片の月」と一文字が置き換わります。
sb[i]やsb(i)はStringBuilderの内部配列にアクセスしている訳ではなく、StringBuilderの持つインデクサです。 |