構造体とクラスがC#で概念を分けているのはなぜですか?


44

C#でプログラミングしているときに、理解できない奇妙な言語設計の決定につまずきました。

したがって、C#(およびCLR)には2つの集約データ型があります:(struct値型、スタックに格納され、継承なし)およびclass(参照型、ヒープに格納され、継承あり)。

この設定は最初はいい感じに聞こえますが、その後、集計タイプのパラメーターを取るメソッドに出くわし、実際に値タイプか参照タイプかを判断するには、そのタイプの宣言を見つける必要があります。時には非常に混乱することがあります。

一般的に受け入れられている問題の解決策は、すべてstructのを「不変」(フィールドをに設定readonly)として宣言して、起こりうる間違いを防ぎ、structsの有用性を制限しているようです。

たとえば、C ++ははるかに使いやすいモデルを採用しています。スタックまたはヒープのいずれかにオブジェクトインスタンスを作成し、値または参照(またはポインター)で渡すことができます。私はC#がC ++に触発されたと聞き続けていますが、なぜこの1つの手法を採用しなかったのか理解できません。組み合わせるclassstruct経由で参照として(明示的に)二つの異なる割り当てオプション(ヒープとスタック)と一方の構築物へと値として周りにそれらを渡したりrefしてout、キーワードはいいことのように思えます。

質問は、なぜやったclassし、structC#とCLRで別々の概念の代わりに、2つのアロケーションオプションを持つ1つの集約タイプになりますか?


34
『構造体の有用性を制限する...「不変」問題への一般的に受け入れられた解決策は、など、すべての構造体を宣言しているようだ』多くの人が作っていることを主張するだろう何か、それがパフォーマンスのボトルネックの原因ではありませんいつでも不変すると、一般的に、それはより便利になります。また、structsは常にスタックに格納されるとは限りません。structフィールドを持つオブジェクトを検討してください。それはさておき、メイソン・ウィーラーがスライスの問題に言及したように、おそらく最大の理由です。
ドーバル

7
C#がC ++に触発されたことは事実ではありません。むしろ、C#は、C ++とJavaの両方の設計におけるすべての(当時は意味があり、良い音だった)ミスに触発されました。
ピータージャーケンズ

18
注:スタックとヒープは実装の詳細です。構造体インスタンスをスタックに割り当て、クラスインスタンスをヒープに割り当てる必要があると言うことはありません。そして、実際にはそうでもありません。たとえば、コンパイラがエスケープ分析を使用して、変数がローカルスコープをエスケープできないと判断し、それがクラスインスタンスであってもスタックに割り当てる可能性が非常に高いです。スタックやヒープが必要だということすらありません。ヒープ上のリンクリストとして環境フレームを割り当てることができ、スタックもまったくありません。
ヨルグWミットタグ


3
「しかし、その後、集約型のパラメーターを取るメソッドに出くわし、それが実際に値型であるか参照型であるかを判断するには、その型の宣言を見つける必要があります」 ?メソッドは値を渡すように要求します。値を渡します。どの時点で、それが参照型か値型かを気にする必要がありますか?
ルアーン

回答:


58

C#(およびJavaと、C ++の後に開発された他のすべてのOO言語)がこの側面でC ++のモデルをコピーしなかった理由は、C ++のやり方が恐ろしい混乱だからです。

上記の関連ポイントを正しく特定しましたstruct:値の型、継承なし。 class:参照型、継承あり。継承と値のタイプ(または、より具体的には、ポリモーフィズムと値渡し)は混在しません。typeのオブジェクトをtype Derivedのメソッド引数に渡し、Baseその仮想メソッドを呼び出す場合、適切な動作を得る唯一の方法は、渡されたものが参照であることを確認することです。

それと、値型として継承可能なオブジェクトを持つことによってC ++で実行される他のすべての混乱(コピーコンストラクターとオブジェクトスライシングが頭に浮かぶ!)との間で、最善の解決策はただ言うだけです。

優れた言語設計は、機能を実装するだけでなく、実装しない機能も知っています。これを行う最良の方法の1つは、前に来た人の間違いから学ぶことです。


29
これは、C ++に関するもう1つの意味のない主観的な暴言です。ダウン投票できませんが、できればできます。
Bartek Banachewicz

17
@MasonWheeler:「恐ろしい混乱です」十分主観的に聞こえます。これは、あなたの別の答えに対する長いコメントスレッドですでに議論されています。スレッドはヌードになりました(残念ながら、フレームウォーソースに含まれていても有用なコメントが含まれていたため)。ここですべてを繰り返す価値はないと思いますが、「C#が正しく、C ++が間違っています」(これはあなたが伝えようとしているメッセージのようです)は実際に主観的な声明です。
アンディプロール

15
@MasonWheeler:私は、スレッドを削除し、他の数人も削除しました。それが、削除されたことが残念だと思う理由です。ここでそのスレッドを複製するのは良い考えではないと思いますが、短いバージョンは次のとおりです:C ++では、デザイナーではなく型のユーザーが、型を使用するセマンティクス(参照セマンティクスまたは値)を決定しますセマンティクス)。これには長所と短所があります。長所を考慮せずに(または知らないで)短所に怒ります。そのため、分析は主観的です。
アンディプロール

7
ため息 LSP違反の議論はすでに行われていると思います。そして私は、LSPの言及はかなり奇妙で無関係であることに大部分の人々が同意したと思いますが、modがコメントスレッドを隠したので確認できません。
Bartek Banachewicz

9
最後の段落を一番上に移動し、現在の最初の段落を削除すると、完璧な議論があると思います。しかし、現在の最初の段落は単なる主観です。
マーティンヨーク

19

類推すると、C#は基本的にメカニックのツールのセットのようなもので、一般にペンチや調節可能なレンチは避けるべきだと誰かが読んでいるので、調節可能なレンチはまったく含まれず、プライヤーは「安全でない」とマークされた特別な引き出しにロックされています、使用者の健康に対する責任を免責する免責事項に署名した後、監督者の承認を得てのみ使用できます。

比較すると、C ++には調整可能なレンチとプライヤーだけでなく、目的がすぐには分からない奇妙な特殊用途のツールも含まれており、それらを保持する正しい方法がわからない場合、 thumb(ただし、使用方法を理解したら、C#ツールボックスの基本ツールでは本質的に不可能なことを実行できます)。さらに、旋盤、フライス盤、平面研削盤、金属切削バンドソーなどがあり、必要に応じていつでもまったく新しいツールを設計および作成できます(ただし、これらの機械工のツールは、あなたが彼らと何をしているのかわからない場合、あるいはあなたがただ不注意になったとしても、重傷)。

これは、哲学の基本的な違いを反映しています。C++は、基本的に必要な設計に必要なすべてのツールを提供しようとします。これらのツールの使用方法を制御しようとすることはほとんどないため、まれな状況でのみ機能するデザインを作成するためにそれらを使用することも簡単です。彼らはまったくうまくいく可能性があります。特に、この決定の大部分は、設計上の決定を分離することで行われます。実際にほとんど常に結合されているものであってもです。その結果、C ++を記述することとC ++を適切に記述することには大きな違いがあります。C ++を上手に書くには、多くのイディオムと経験則(他の経験則を破る前にどれだけ真剣に再検討するかについての経験則を含む)を知る必要があります。結果として、C ++は、学習の容易さよりも(専門家による)使いやすさを重視しています。また、あまりにも簡単に使用できない状況もあります(多すぎる)。

C#は、言語設計者が優れた設計手法と見なしたものを強制する(または少なくとも非常に強く示唆する)ために、さらに多くのことを行います。C ++で分離されている(ただし、実際には通常一緒に行われる)かなりの数のことは、C#で直接結合されます。「安全でない」コードが境界を少し押し上げることを許可しますが、正直なところ、全体ではありません。

その結果、一方では、C#で表現するのにかなり不格好な、C ++でかなり直接表現できるデザインがかなりあります。一方、それはです全体 C#を学ぶために非常に簡単に、そしてその状況に作業(またはおそらく他)しません、本当に恐ろしい設計を生産する可能性がされて大幅に減少しました。多くの場合(おそらくほとんどの場合)、いわば「流れに乗る」だけで、堅実で実行可能な設計を得ることができます。または、私の友人の1人(少なくとも私は彼を友人だと思うのが好きです-彼が本当に同意するかどうかはわかりません)がそれを好むので、C#は成功の落とし穴に陥りやすくします。

したがって、2つの言語でどのようにどのようclassstruct取得されるかという問題をより具体的に見ると、基本クラス/インターフェイスを装って派生クラスのオブジェクトを使用する可能性のある継承階層で作成されたオブジェクトは、通常、何らかのポインタまたは参照を介して行う必要があるという事実に固執しています-具体的なレベルでは、派生クラスのオブジェクトには、基本クラス/のインスタンスとして扱うことができる何かのメモリが含まれていますインターフェイス、および派生オブジェクトは、メモリのその部分のアドレスを介して操作されます。

C ++では、それを正しく行うのはプログラマー次第です。継承を使用している場合、(たとえば)階層内のポリモーフィッククラスで機能する関数がポインターまたはベースへの参照を介して確実に機能するようにするのは彼次第ですクラス。

C#では、基本的に型間の同じ分離はより明確であり、言語自体によって強制されます。プログラマーは、クラスのインスタンスを参照渡しするための手順を実行する必要はありません。これはデフォルトで行われるためです。


2
C ++のファンとして、これはC#とSwiss Army Chainsawの違いの優れた要約だと思います。
デビッドソーン

1
@DavidThornley:少なくとも、私はややバランスのとれた比較だと思うことを書き込もうとしました。指を指すのではなく、これを書いたときに私が見たもののいくつかは、(不正確に言えば)やや不正確だと思いました。
ジェリーコフィン

7

これは「C#:別の言語が必要な理由」からです。-ガンナーソン、エリック:

シンプルさがC#の重要な設計目標でした。

シンプルさと言語の純粋さを重視することはできますが、純粋さのための純粋さはプロのプログラマーにはほとんど役に立ちません。そのため、私たちは、シンプルで簡潔な言語を持ちたいという願望と、プログラマが直面する現実の問題を解決することのバランスをとろうとしました。

[...]

値型、演算子のオーバーロード、およびユーザー定義の変換はすべて言語複雑さを追加しますが、重要なユーザーシナリオを大幅に簡素化できます。

オブジェクトの参照セマンティクスは、多くの問題を回避する方法です(もちろん、オブジェクトのスライシングだけでなく)が、実際の問題では、値セマンティクスを持つオブジェクトが必要になる場合があります(たとえば、参照セマンティクスを使用してはいけないようなサウンドを見てください)?別の観点から)。

したがって、タグの下に値の意味を持つ汚い、い、悪いオブジェクトを分離するよりも良いアプローチはありstructますか?


1
参照セマンティクスを備えた汚い、ugい、悪いオブジェクトを使用していないのでしょうか?
Bartek Banachewicz

たぶん...私は失われた原因です。
マンリオ

2
私見、Javaの設計における最大の欠陥の1つは、変数を使用してIDまたは所有権をカプセル化するかどうかを宣言する手段がないことであり、C#の最大の欠陥の1つは、操作を区別する手段がないことです変数が参照を保持するオブジェクトの操作からの変数。ランタイムがそのような区別を気にしなかったとしても、型変数をint[]共有可能または変更可能(配列はどちらでもよいが、通常は両方ではない)にするかどうかを言語で指定できると、間違ったコードが間違って見えるようになります。
supercat

4

から派生する値型を考えるのではなく、Objectクラスインスタンス型とは完全に別のユニバースに存在するストレージ場所型を考える方が便利ですが、すべての値型には対応するヒープオブジェクト型が必要です。構造体型の保存場所は、型のパブリックフィールドとプライベートフィールドの連結を保持するだけで、ヒープタイプは次のようなパターンに従って自動生成されます。

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

次のようなステートメントの場合:Console.WriteLine( "The value is {0}"、somePoint);

次のように翻訳されます:boxed_Point box1 = new boxed_Point(somePoint); Console.WriteLine( "値は{0}"、box1);

実際には、ストレージロケーションタイプとヒープインスタンスタイプは別々のユニバースに存在するため、ヒープインスタンスタイプを次のように呼び出す必要はありませんboxed_Int32。システムは、ヒープオブジェクトインスタンスを必要とするコンテキストと、保存場所を必要とするコンテキストを知っているためです。

一部の人々は、オブジェクトのように振る舞わない値型は「悪」とみなされるべきだと考えています。私は反対の見方をします。値型の格納場所はオブジェクトでもオブジェクトへの参照でもないため、オブジェクトのように動作するという期待は役に立たないと考えられるべきです。構造体がオブジェクトのように便利に動作できる場合、そのようにすることには何の問題もありませんが、それぞれstructは基本的にダクトテープでつながれたパブリックフィールドとプライベートフィールドの集合にすぎません。

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