web-dev-qa-db-ja.com

.NET Stringが不変なのはなぜですか?

ご存じのとおり、 String は不変です。 Stringが不変であり、 StringBuilder classが可変である理由は何ですか?

176
Nirajan Singh
  1. 不変型のインスタンスは、スレッドを変更できないため、本質的にスレッドセーフです。別のスレッドと干渉する方法でスレッドを変更するリスクは除去されます(参照自体は別の問題です)。
  2. 同様に、エイリアスが変更を生成できないという事実(xとyの両方が同じオブジェクトを参照する場合、xの変更はyの変更を伴う)により、コンパイラの最適化がかなり可能になります。
  3. メモリを節約する最適化も可能です。インターンとアトマイズが最も明白な例ですが、同じ原則の他のバージョンを実行することもできます。不変オブジェクトを比較し、同じインスタンスを指すように複製への参照を置き換えることで、約半分のGBのメモリを節約したことがあります(時間がかかりますが、大量のメモリを節約するために1分余分に起動しました問題の場合のパフォーマンスの勝利)。実行できない可変オブジェクトを使用します。
  4. outまたはref(オブジェクトではなく参照を変更するため)でない限り、パラメーターにメソッドとして不変型を渡すことによる副作用はありません。したがって、プログラマーは、メソッドの開始時にstring x = "abc"であり、メソッドの本体で変更されない場合、メソッドの最後でx == "abc"であることを知っています。
  5. 概念的には、セマンティクスは値型に似ています。特に、平等はアイデンティティではなく状態に基づいています。これは、"abc" == "ab" + "c"を意味します。これは不変性を必要としませんが、そのような文字列への参照はその寿命を通して常に「abc」に等しいという事実(これは不変性を必要とします)は、以前の値との同等性を維持することが重要であり、正確性を確保するのがはるかに簡単なキーとして使用しますof(文字列は実際にキーとして一般的に使用されます)。
  6. 概念的には、不変である方が理にかなっています。クリスマスに月を追加する場合、クリスマスは変更せず、1月下旬に新しい日付を作成します。したがって、Christmas.AddMonths(1)は、変更可能なものを変更するのではなく、新しいDateTimeを生成することが理にかなっています。 (別の例として、可変オブジェクトとして名前を変更しても、使用している名前は変更されますが、「Jon」は不変のままであり、他のJonsは影響を受けません。
  7. コピーは高速かつ簡単で、return thisだけでクローンを作成します。とにかくコピーを変更することはできないので、何かが自分のコピーであるふりをしても安全です。
  8. [編集、これを忘れていた]。内部状態は、オブジェクト間で安全に共有できます。たとえば、配列、開始インデックス、およびカウントによってサポートされるリストを実装している場合、サブ範囲を作成する上で最もコストのかかる部分はオブジェクトのコピーです。ただし、不変の場合、サブ範囲オブジェクトは同じ配列を参照できます。開始インデックスとカウントのみを変更する必要があり、構築時間にveryを大幅に変更します。

全体として、目的の一部として変更を受けていないオブジェクトの場合、不変であることには多くの利点があります。主な欠点は、余分な構造を必要とすることです。ただし、ここでも誇張されていることがよくあります(StringBuilderが固有の構造を持つ同等の一連の連結よりも効率的になる前に、いくつかの追加を行う必要があります)。

可変性がオブジェクトの目的の一部である場合は不利になります(給与が変更されることのないEmployeeオブジェクトによってモデル化される必要がある場合)が、それでも役立つ場合があります(多くのWebやその他のステートレスでアプリケーション、読み取り操作を行うコードは更新を行うコードとは別であり、異なるオブジェクトを使用するのは自然かもしれません。パフォーマンスと正確性保証ゲインに対して不変)。

コピーオンライトは妥協点です。ここで、「実」クラスは「状態」クラスへの参照を保持します。状態クラスはコピー操作で共有されますが、状態を変更すると、状態クラスの新しいコピーが作成されます。これはC#よりもC++でよく使用されます。そのため、std:stringは不変型の利点のすべてではなく一部を享受しながら、可変性を維持します。

232
Jon Hanna

文字列を不変にすることには多くの利点があります。自動スレッドセーフを提供し、文字列を単純で効果的な方法で組み込み型のように動作させます。また、実行時に追加の効率(リソースの使用量を削減するために効果的な文字列のインターンを許可するなど)を可能にし、サードパーティのAPI呼び出しで文字列を変更することはできないため、セキュリティ上の大きな利点があります。

不変文字列の1つの大きな欠点に対処するために、StringBuilderが追加されました。不変型の実行時の構築は、GCのプレッシャーの多くを引き起こし、本質的に低速です。これを処理するために明示的で変更可能なクラスを作成することにより、文字列クラスに不要な複雑さを追加することなく、この問題に対処します。

74
Reed Copsey

文字列は実際には不変ではありません。彼らはただ公的に不変です。つまり、パブリックインターフェイスから変更することはできません。しかし、内部では実際に可変です。

信じられない場合は、 reflector を使用してString.Concat定義を見てください。最後の行は...

int length = str0.Length;
string dest = FastAllocateString(length + str1.Length);
FillStringChecked(dest, 0, str0);
FillStringChecked(dest, length, str1);
return dest;

ご覧のとおり、FastAllocateStringは空ですが、割り当てられた文字列を返し、FillStringCheckedによって変更されます

実際、FastAllocateStringは外部メソッドであり、FillStringCheckedは安全ではないため、ポインターを使用してバイトをコピーします。

より良い例があるかもしれませんが、これは私がこれまでに見つけたものです。

21
Carlos Muñoz

文字列管理は高価なプロセスです。文字列を不変に保つと、文字列を繰り返し作成するのではなく、再利用できます。

13
kolosy

C#で文字列型が不変である理由

文字列は参照型であるため、コピーされることはありませんが、参照渡しされます。これを、値で渡されるC++ std :: stringオブジェクト(不変ではない)と比較してください。これは、Hashtableのキーとして文字列を使用する場合、C++で問題ないことを意味します。C++は、後で比較するために、ハッシュテーブル(実際にはstd :: hash_mapですが)にキーを格納するために文字列をコピーするためです。したがって、後でstd :: stringインスタンスを変更しても、大丈夫です。しかし、.Netでは、HashtableでStringを使用すると、そのインスタンスへの参照が保存されます。ここで、文字列は不変ではないとしばらくの間仮定し、何が起こるかを見てみましょう。1.誰かがキー「hello」を持つ値xをハッシュテーブルに挿入します。 2. Hashtableは文字列のハッシュ値を計算し、文字列への参照と値xを適切なバケットに配置します。 3.ユーザーがStringインスタンスを「さようなら」に変更します。 4.誰かが「hello」に関連付けられたハッシュテーブルの値を望んでいます。最終的に正しいバケットを検索しますが、文字列を比較すると「bye」!= "hello"と表示されるため、値は返されません。 5.誰かが「さようなら」という値を望んでいるかもしれません。 「さようなら」はおそらく異なるハッシュを持っているので、ハッシュテーブルは別のバケットを見るでしょう。そのバケットには「さようなら」キーがないため、エントリはまだ見つかりません。

ストリングを不変にすることは、ステップ3が不可能であることを意味します。誰かが文字列を変更すると、彼は新しい文字列オブジェクトを作成し、古いものはそのままにします。これは、ハッシュテーブルのキーがまだ「hello」であり、したがって正しいことを意味します。

したがって、おそらく、とりわけ、不変の文字列は、参照によって渡される文字列をハッシュテーブルまたは類似の辞書オブジェクトのキーとして使用できるようにする方法です。

13
NebuSoft

不変データを防御的にコピーする必要はありません。変異させるためにコピーする必要があるという事実にもかかわらず、多くの場合、自由にエイリアスを作成し、このエイリアスの意図しない結果を心配する必要がないため、防御的なコピーがないため、パフォーマンスが向上する可能性があります。

5
dsimcha

これを投げ込むために、しばしば忘れられがちな見方はセキュリティです。文字列が変更可能な場合、このシナリオを想像してください。

string dir = "C:\SomePlainFolder";

//Kick off another thread
GetDirectoryContents(dir);

void GetDirectoryContents(string directory)
{
  if(HasAccess(directory) {
    //Here the other thread changed the string to "C:\AllYourPasswords\"
    return Contents(directory);
  }
  return null;
}

渡された文字列を変更することを許可された場合、それが非常に、非常に悪くなる可能性があることがわかります。

5
Nick Craver

文字列は、.NETで参照型として渡されます。

参照型は、スタック上に、マネージヒープにある実際のインスタンスへのポインターを配置します。これは、インスタンス全体をスタックに保持するValueタイプとは異なります。

値型がパラメーターとして渡されると、ランタイムはスタックに値のコピーを作成し、その値をメソッドに渡します。これが、更新された値を返すために 'ref'キーワードで整数を渡す必要がある理由です。

参照型が渡されると、ランタイムはスタック上にポインターのコピーを作成します。コピーされたポインターは、引き続き参照型の元のインスタンスを指します。

文字列型にはオーバーロードされた=演算子があり、ポインターのコピーではなく、それ自体のコピーを作成します。これにより、値型のように動作します。ただし、ポインタのみがコピーされた場合、2番目の文字列操作が別のクラスのプライベートメンバーの値を誤って上書きして、かなり厄介な結果を引き起こす可能性があります。

他の投稿で言及したように、StringBuilderクラスを使用すると、GCのオーバーヘッドなしで文字列を作成できます。

5
Kevin McKelvin

通常、文字列やその他の具体的なオブジェクトは、読みやすさと実行時の効率を向上させるために不変オブジェクトとして表現されます。別のセキュリティは、プロセスが文字列を変更し、文字列にコードを挿入することはできません

3
SQLMenace

可変文字列を関数に渡しますが、変更されるとは思わないことを想像してください。次に、関数がその文字列を変更するとどうなりますか?たとえば、C++では、値による呼び出し(std::stringstd::string&パラメーターの違い)だけを行うことができますが、C#では参照がすべてであるため、すべての関数の周りに変更可能な文字列を渡すと、それと予期しない副作用を引き起こします。

これはさまざまな理由の1つにすぎません。パフォーマンスは別のものです(たとえば、インターンされた文字列)。

3
AndiDog

クラスデータが、格納クラスのコントロールの外で変更できないデータを格納する5つの一般的な方法があります。

  1. 値型プリミティブとして
  2. 対象のプロパティがすべて不変であるクラスオブジェクトへの自由に共有可能な参照を保持することにより
  3. 関心のあるプロパティを変更する可能性のあるものに決してさらされない可変クラスオブジェクトへの参照を保持することにより
  4. 構造体として、「可変」または「不変」に関係なく、そのフィールドはすべて#1〜#4(#5ではない)型です。
  5. その参照を介してのみプロパティを変更できるオブジェクトへの参照の現存するコピーのみを保持する。

文字列は可変長であるため、値型のプリミティブにすることも、文字データを構造体に格納することもできません。残りの選択肢の中で、ある種の不変オブジェクトに文字列の文字データを保存する必要のない唯一の選択肢は#5です。オプション#5を中心にフレームワークを設計することは可能ですが、その選択では、制御外で変更できない文字列のコピーを必要とするコードは、それ自体のプライベートコピーを作成する必要があります。それを行うことはほとんど不可能ではありませんが、それを行うために必要な余分なコードの量、およびすべての防御的なコピーを作成するために必要な余分なランタイム処理の量は、string可変である、 特に 可変の文字列型(System.Text.StringBuilder)があり、可変のstringで達成できることの99%を達成できます。

2
supercat

不変文字列は、並行性関連の問題も防ぎます。

0
Ken Liu

他のスレッドがあなたの背中の後ろで修正している文字列で動作するOSであることを想像してください。コピーを作成せずにどのように検証できますか?

0
Eton B.