C#で、Stringが値型のように動作する参照型であるのはなぜですか?


371

文字列は、不変で、==オーバーロードされて同じオブジェクトを参照していることを確認せずにテキストを比較するなど、値型のほとんどの特性を備えていますが、参照型です。

なぜ文字列は単なる値型ではないのですか?


不変の型の場合、区別はほとんど実装の詳細(isテストを省略)であるため、答えはおそらく「歴史的な理由」にあります。不変オブジェクトを物理的にコピーする必要がないため、コピーのパフォーマンスが理由になることはありません。現在、実際にisチェック(または同様の制約)を使用するコードを壊さずに変更することは不可能です。
Elazar

ところで、これはC ++の場合と同じ答えです(ただし、値と参照型の区別は言語では明示されていません)。std::stringコレクションのように動作させるという決定は、現在修正できない古い間違いです。
Elazar 2017

回答:


333

文字列は巨大になる可能性があるため値型ではなく、ヒープに格納する必要があります。値の型は(まだCLRのすべての実装で)スタックに格納されています。スタック割り当て文字列は、あらゆる種類のことを壊します:スタックは32ビットの場合は1 MBのみ、64ビットの場合は4 MBです。各文字列をボックス化する必要があり、コピーペナルティが発生し、文字列をインターンできず、メモリ使用量もありません。気球など

(編集:値の型のストレージが実装の詳細であるという説明が追加されました。これにより、System.ValueTypeから継承されない値のセマティックを持つ型があるという状況が発生します。ありがとう。Ben。)


75
私はここで細心の注意を払っていますが、それは私が質問に関連するブログ投稿にリンクする機会を与えるからです。値の型は必ずしもスタックに保存されるわけではありません。ms.netではほとんどの場合真ですが、CLI仕様ではまったく指定されていません。値型と参照型の主な違いは、参照型が値によるコピーのセマンティクスに従っていることです。参照blogs.msdn.com/ericlippert/archive/2009/04/27/...blogs.msdn.com/ericlippert/archive/2009/05/04/...
ベンSchwehn

8
@Qwertie:Stringは可変サイズではありません。追加すると、実際には別のStringオブジェクトが作成され、そのオブジェクトに新しいメモリが割り当てられます。
codekaizen

5
つまり、理論的には文字列は値型(構造体)である可能性がありますが、「値」は文字列への参照にすぎませんでした。.NET設計者は当然中間者を排除することを決定しました(.NET 1.0では構造体の処理は非効率的であり、文字列がプリミティブ型ではなく参照としてすでに定義されているJavaに従うのは当然でした。さらに、文字列が値型をオブジェクトに変換すると、ボックス化する必要があり、不必要に非効率になります)。
Qwertie

7
@codekaizen Qwertieは正しいですが、言葉遣いが混乱していたと思います。1つの文字列は別の文字列とはサイズが異なる場合があるため、真の値の型とは異なり、コンパイラは文字列値を格納するために割り当てる領域を事前に知ることができませんでした。たとえば、Int32は常に4バイトであるため、文字列変数を定義するたびに、コンパイラは4バイトを割り当てます。int変数(値型の場合)を検出したときに、コンパイラーはどのくらいのメモリーを割り当てる必要がありますか?その時点ではまだ値が割り当てられていないことを理解してください。
Kevin Brock

2
申し訳ありませんが、私のコメントにはタイプミスがあり、今は修正できません。たとえば、Int32は常に4バイトなので、int変数を定義するたびに、コンパイラは4バイトを割り当てます。string変数(値型の場合)を検出したときに、コンパイラーはどのくらいのメモリーを割り当てる必要がありますか?その時点ではまだ値が割り当てられていないことを理解してください。
ケビンブロック

57

値型であり、メソッドに渡されたりメソッドから返されるたびに値をコピーする必要がある場合、パフォーマンス(空間と時間!)はひどいため、値型ではありません。

それは世界を正気に保つための価値の意味論を持っています。コード化するのがどれほど難しいか想像できますか

string s = "hello";
string t = "hello";
bool b = (s == t);

設定bしますかfalse?どんなアプリケーションでもコーディングがどれほど難しいか想像してみてください。


44
Javaは安っぽいことで知られていません。
ジェイソン

3
@マット:まさに。私がC#に切り替えたとき、チームメイトが「==」を使用している間は常に(場合によっては).equals(..)を使用して文字列を比較するため、これはちょっと混乱を招きました。参照を比較するために「==」を残さなかった理由が理解できませんでしたが、90%の確率で、文字列の参照ではなくコンテンツを比較したいと思うかもしれません。
ジュリ

7
@ジュリ:実際には、参照をチェックすることは決して望ましくないと思います。時々new String("foo");、別の人new String("foo")が同じ参照で評価できるため、new演算子が期待するものとは異なる種類のものである場合があります。(または、参照を比較したい場合を教えていただけますか?)
マイケル

1
@Michaelさて、nullとの比較をキャッチするには、すべての比較に参照比較を含める必要があります。参照を文字列と比較するもう1つの良い場所は、等価比較ではなく比較する場合です。比較した場合、2つの同等の文字列は0を返します。ただし、このケースをチェックすると、比較全体を実行するのと同じくらい時間がかかるため、便利なショートカットではありません。確認ReferenceEquals(x, y)は高速なテストであり、すぐに0を返すことができます。また、nullテストと組み合わせると、それ以上の作業は必要ありません。
Jon Hanna

1
...文字列をクラス型ではなくそのスタイルの値型にすることは、のデフォルト値がstringnull参照としてではなく、空の文字列(以前の.netシステムの場合)として動作することを意味します。実際、私自身の好みはStringreference- type を含む値の型をNullableString持つことです。前者は同等のデフォルト値をString.Empty持ち、後者はデフォルトのnullを持ち、特別なボックス化/ボックス化解除ルール(デフォルトのボックス化など)を持ちます。を評価NullableStringすると、String.Empty)への参照が生成されます。
スーパーキャット

26

参照型と値型の違いは、基本的に言語の設計におけるパフォーマンスのトレードオフです。参照型はヒープ上に作成されるため、参照型は構築と破棄、およびガベージコレクションにある程度のオーバーヘッドがあります。一方、値型はメソッドの呼び出しにオーバーヘッドがあります(データサイズがポインターよりも大きい場合)。これは、ポインターだけではなくオブジェクト全体がコピーされるためです。文字列はポインタのサイズよりもはるかに大きくなる可能性があるため(通常はそれよりも大きいため)、参照型として設計されています。また、Servyが指摘したように、値型のサイズはコンパイル時にわかっている必要がありますが、これは文字列の場合とは限りません。

可変性の問題は別の問題です。参照型と値型はどちらも変更可能または不変にすることができます。ただし、可変値型のセマンティクスは混乱を招く可能性があるため、値型は通常不変です。

参照型は一般に変更可能ですが、意味がある場合は不変として設計できます。文字列は、特定の最適化を可能にするため、不変として定義されます。たとえば、同じプログラムで同じ文字列リテラルが複数回出現する場合(これはよくあることです)、コンパイラは同じオブジェクトを再利用できます。

では、なぜ「==」がオーバーロードされて文字列をテキストで比較するのでしょうか。最も有用なセマンティクスだからです。2つの文字列がテキストで等しい場合、最適化のために同じオブジェクト参照である場合とそうでない場合があります。したがって、参照を比較することはほとんど役に立ちませんが、テキストを比較することはほとんど常に必要なことです。

より一般的に言えば、文字列には値セマンティクスと呼ばれるものがあります。これは、C#固有の実装詳細である値型よりも一般的な概念です。値型には値のセマンティクスがありますが、参照型にも値のセマンティクスがある場合があります。型が値のセマンティクスを持っている場合、基礎となる実装が参照型であるか値型であるかを実際には判別できないため、実装の詳細を検討できます。


値の型と参照の型の違いは、パフォーマンスに関するものではありません。それは変数が実際のオブジェクトまたはオブジェクトへの参照を含むかどうかについてです。文字列のサイズは可変であるため、文字列が値型になることはありません。値型になるには、定数である必要があります。パフォーマンスはそれとほとんど関係ありません。参照型も作成するのに費用がかかりません。
2013年

2
@Sevy:文字列のサイズ一定です。
JacquesB 2013年

可変サイズの文字配列への参照が含まれているだけだからです。実際の「値」だけが参照タイプである値タイプがあると、すべての集中的な目的で参照セマンティクスが依然として存在するため、混乱を招くだけです。
2013年

1
@Sevy:配列のサイズは一定です。
JacquesB 2013年

1
配列を作成すると、そのサイズは一定ですが、全世界のすべての配列が完全に同じサイズであるとは限りません。それが私のポイントです。文字列が値型になるには、存在するすべての文字列がすべて同じサイズである必要があります。これは、.NETで値型が設計されている方法だからです。実際に値を取得する前に、そのような値のタイプ用のストレージスペースを予約できる必要があるため、サイズはコンパイル時に認識されている必要があります。このようなstringタイプには、固定サイズのcharバッファーが必要です。これは、制限的であり、非常に非効率的です。
2013年

16

これは古い質問に対する遅い回答ですが、他のすべての回答には要点がありません。つまり、.NETには2005年の.NET 2.0までジェネリックがなかったということです。

Stringなどの非ジェネリックコレクション文字列を最も効率的な方法で格納できるようにすることがMicrosoftにとって非常に重要であったため、は値型ではなく参照型System.Collections.ArrayListです。

ジェネリックでないコレクションに値タイプを格納するにobjectは、ボックス化と呼ばれるタイプへの特別な変換が必要です。CLRは、値の型をボックス化するときに、値をa内にラップSystem.Objectして、マネージヒープに格納します。

コレクションから値を読み取るには、ボックス化解除と呼ばれる逆の操作が必要です。

ボックス化とボックス化解除の両方に無視できないコストがあります。ボックス化には追加の割り当てが必要で、ボックス化解除には型チェックが必要です。

一部の回答は誤ってそれを主張します stringは、サイズが可変であるため、値型として実装することはできないます。実際、Small String Optimization戦略を使用して、文字列を固定長データ構造として実装するのは簡単です。文字列は、外部バッファーへのポインターとして格納される大きな文字列を除いて、Unicode文字のシーケンスとして直接メモリに格納されます。両方の表現は、同じ固定長、つまりポインタのサイズを持つように設計できます。

ジェネリックが初日から存在していた場合、値の型として文字列を使用する方が、よりシンプルなセマンティクス、より優れたメモリ使用量、より優れたキャッシュの局所性を備えた、おそらくより良いソリューションだったと思います。A List<string>だけ小さな文字列を含むが、メモリの単一の連続ブロックされている可能性が。


私、この答えをありがとう!スタックは実装の詳細ですが、私はヒープとスタックの割り当てに関することを言っている他のすべての答えを見てきました。結局のところ、いずれにしても、stringそのサイズとchar配列へのポインタのみが含まれているため、「巨大な値の型」にはなりません。しかし、これがこの設計決定の単純で適切な理由です。ありがとう!
V0ldek

8

文字列だけが不変の参照型ではありません。 マルチキャストデリゲートも。 それが書いても安全な理由です

protected void OnMyEventHandler()
{
     delegate handler = this.MyEventHandler;
     if (null != handler)
     {
        handler(this, new EventArgs());
     }
}

これは文字列を操作してメモリを割り当てる最も安全な方法であるため、文字列は不変であると思います。なぜ値型ではないのですか?以前の作者はスタックサイズなどについて正しいです。プログラムで同じ定数文字列を使用する場合、文字列を参照型にすることでアセンブリサイズを節約できることも付け加えておきます。定義すると

string s1 = "my string";
//some code here
string s2 = "my string";

「my string」定数の両方のインスタンスがアセンブリに1回だけ割り当てられる可能性があります。

通常の参照型のように文字列を管理する場合は、新しいStringBuilder(string s)内に文字列を配置します。または、MemoryStreamsを使用します。

ライブラリを作成する予定で、関数に巨大な文字列が渡されることが予想される場合は、パラメーターをStringBuilderまたはStreamとして定義します。


1
不変の参照型の例はたくさんあります。また、文字列の例についても、現在の実装では確かにかなり保証されています- 技術的にはモジュールごと(アセンブリごとではありません)ですが、ほぼ同じです...
Marc Gravell

5
最後の点について:大きな文字列を渡そうとすると、StringBuilderは役に立ちません(とにかく実際には文字列として実装されているため)。StringBuilderは、文字列を複数回操作するのに役立ちます。
Marc Gravell

Uはハドラーではなくデリゲートハンドラを意味しましたか?(
うるさくて

6

また、文字列の実装方法(プラットフォームごとに異なります)と、それらを一緒にステッチし始めるとき。のようにStringBuilder。それはあなたがコピーするためのバッファを割り当てます、あなたが最後に達すると、それはあなたがあなたのためにさらにより多くのメモリを割り当てます、あなたが大きな連結を行うならパフォーマンスが妨げられないことを期待しています。

たぶんジョン・スキートがここを手伝ってくれる?


5

これは主にパフォーマンスの問題です。

文字列がLIKE値型のように振る舞うと、コードを作成するときに役立ちますが、値型であることはパフォーマンスに大きな影響を与えます。

詳細については、.netフレームワークの文字列に関するすばらしい記事をご覧ください。


3

非常に単純な言葉では、明確なサイズを持つすべての値は、値タイプとして扱うことができます。


これはコメントでなければなりません
ρяσѕρєяK

c#の新しいpplについて理解しやすくなりました
LONG

2

string参照型であることをどのように見分けることができますか?実装方法が重要かどうかはわかりません。C#の文字列は正確に不変であるため、この問題を心配する必要はありません。


System.ValueTypeから派生していないため、これは参照型です(私は信じています)。値の型は、スタックに割り当てられるか、構造体にインラインで割り当てられます。参照型はヒープに割り当てられます。
Davy8 2009年

参照型と値型はどちらも、最終的な基本クラスObjectから派生しています。値型がオブジェクトのように動作する必要がある場合は、値型を参照オブジェクトのように見せかけるラッパーがヒープに割り当てられ、値型の値がその中にコピーされます。
Davy8 2009年

ラッパーにマークが付けられているため、値のタイプが含まれていることがシステムに認識されます。このプロセスはボックス化と呼ばれ、その逆のプロセスはボックス化解除と呼ばれます。ボックス化とボックス化解除により、任意のタイプをオブジェクトとして扱うことができます。(後ろのサイトでは、おそらく記事にリンクしているはずです。)
Davy8 2009年

2

実際、文字列は値の型にほとんど似ていません。まず、すべての値の型が不変であるとは限りません。必要に応じてInt32の値を変更しても、スタック上の同じアドレスのままです。

文字列は非常に良い理由で不変であり、それが参照型であることとは何の関係もありませんが、メモリ管理とは関係があります。文字列のサイズが変更されたときに新しいオブジェクトを作成する方が、マネージヒープで移動するよりも効率的です。値/参照型と不変オブジェクトの概念を組み合わせていると思います。

「==」まで:あなたが言ったように「==」は演算子のオーバーロードであり、文字列を操作するときにフレームワークをより便利にするために、これも非常に適切な理由で実装されました。


値の型は本質的に不変ではないことを私は理解していますが、ほとんどのベストプラクティスは、独自の値を作成するときにそれらがそうであるべきであると示唆しているようです。私は、値型のプロパティではなく特性を述べました。これは、値型がこれらを示すことが多いことを意味しますが、必ずしも定義上ではない
Davy8

5
@ WebMatrix、@ Davy8:プリミティブ型(int、double、boolなど)は不変です。
ジェイソン

1
@ジェイソン、私は不変の用語は主に初期化後に変更できないオブジェクト(参照型)に適用されると思いました。これは値の型にどのように適用されますか?
WebMatrix

8
どういうわけか、「int n = 4; n = 9;」では、「定数」という意味で、int変数が「不変」であるというわけではありません。つまり、値4は不変であり、9に変更されません。int変数 "n"の値は最初に4になり、次に別の値9になります。しかし、値自体は不変です。率直に言って、これはwtfに非常に近いものです。
ダニエルダラナス09年

1
+1。私は、この「文字列は値の型のようなものだ」と聞いて、まったくそうではないのにうんざりしています。
Jon Hanna

1

文字列が文字配列で構成されているほど単純ではありません。文字列を文字配列として見ます[]。したがって、参照メモリの場所はスタックに格納されており、ヒープ上の配列のメモリの場所の先頭を指しているため、ヒープ上にあります。文字列のサイズは、割り当てられるまで不明です。ヒープに最適です。

同じサイズの文字列を変更した場合、コンパイラはそれを認識せず、新しい配列を割り当てて配列内の位置に文字を割り当てる必要があるため、文字列は実際には不変です。文字列を、言語がメモリをその場で割り当てる必要がないように保護する方法であると考えると理にかなっています(プログラミングのようにCを読み取る)。


1
「文字列のサイズは割り当てられる前に不明です」-これはCLRでは正しくありません。
codekaizen 2013年

-1

さらに別の不可解な反対票を投じるリスクがあります...多くの人が値の型とプリミティブ型に関してスタックとメモリについて言及しているという事実は、それらがマイクロプロセッサのレジスタに適合しなければならないためです。レジスタのビット数よりもビット数が多い場合、スタックに何かをプッシュしたりポップしたりすることはできません。命令は、たとえば「pop eax」です。32ビットシステムではeaxが32ビット幅であるためです。

浮動小数点プリミティブ型は、80ビット幅のFPUによって処理されます。

これはすべて、プリミティブ型の定義を難読化するOOP言語が登場するずっと前に決定されたもので、値型はOOP言語専用に作成された用語であると思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.