web-dev-qa-db-ja.com

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

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

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

この設定は最初はいい感じに聞こえますが、その後、集約型のパラメーターを受け取るメソッドに遭遇し、それが実際に値型か参照型かを判別するには、その型の宣言を見つける必要があります。ときどき混乱することがあります。

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

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

問題は、なぜclassstructが、2つの割り当てオプションを持つ1つの集約型ではなく、C#とCLRで別々の概念になったのか、です。

45
Mints97

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

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

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

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

59
Mason Wheeler

類推すると、C#は基本的には誰かが読んだ機械工のツールのようなもので、ペンチや調節可能なレンチは一般に避けなければならないため、調節可能なレンチはまったく含まれていません。 、そしてあなたの健康に対する責任のあなたの雇用者を免責する免責事項に署名した後、監督者の承認を得てのみ使用できます。

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

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

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

その結果、一方ではC++でかなり直接表現できるデザインがかなり多く、C#で表現するのはかなり扱いにくいものになります。一方、それはwhole C#を学ぶのがはるかに簡単であり、あなたの状況(またはおそらく他のどれでも)には機能しない本当に恐ろしいデザインを生成する可能性は劇的に削減されました。多くの(おそらくほとんどの場合でも)ケースでは、いわば「流れに乗る」だけで、確実で実用的な設計を得ることができます。または、私の友人の1人(少なくとも私は彼を友人だと思っています-彼が本当に同意するかどうかはわかりません)が好むので、C#を使用すると簡単に成功することができます。

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

C++では、それを正しく行うかどうかはプログラマー次第です-継承を使用している場合は、(たとえば)階層内の多相クラスで機能する関数が、ポインターまたはベースへの参照を介して機能することを確認する必要があります。クラス。

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

20
Jerry Coffin

これは からです "C#:なぜ別の言語が必要なのですか?" -Gunnerson、Eric:

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

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

[...]

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

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

したがって、structのタグの下に、値がセマンティックで汚い、醜い、悪いオブジェクトを分離するよりも、どのようなアプローチを取るのが良いでしょうか?

7
manlio

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( "The value is {0}"、box1);

実際には、格納場所タイプとヒープインスタンスタイプは別々のユニバースに存在するため、boxed_Int32などのヒープインスタンスタイプを呼び出す必要はありません。システムは、ヒープオブジェクトインスタンスを必要とするコンテキストと、格納場所を必要とするコンテキストを認識します。

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

4
supercat