C#3.0オブジェクト初期化コンストラクターの括弧がオプションなのはなぜですか?


114

C#3.0オブジェクト初期化構文により、パラメーターのないコンストラクターが存在する場合に、コンストラクターで括弧の開閉ペアを除外できるようです。例:

var x = new XTypeName { PropA = value, PropB = value };

とは対照的に:

var x = new XTypeName() { PropA = value, PropB = value };

なぜコンストラクタの開き括弧と閉じ括弧のペアが後でオプションになるのか、私は興味がありXTypeNameますか?


9
余談ですが、先週のコードレビューでこれを発見しました:var list = new List <Foo> {}; 何かが乱用される可能性がある場合...
Blu

@bluそれが私がこの質問をしたかった理由の1つです。コードの不整合に気づきました。一般的に不整合が気になるので、構文のオプション性の背後にある適切な根拠があるかどうかを確認すると思いました。:)
James Dunne

回答:


143

この質問は2010年9月20日の私のブログの主題でした。JoshとChadの回答(「価値がないので、なぜ必要なのか?」と「冗長性を排除する」)は基本的に正しいです。もう少し具体化するには:

オブジェクト初期化子の「より大きな機能」の一部として引数リストを省略できるという機能は、「甘い」機能の基準を満たしました。私たちが考慮したいくつかのポイント:

  • 設計と仕様のコストが低かった
  • とにかく、オブジェクトの作成を処理するパーサーコードを大幅に変更します。パラメータリストをオプションにする追加の開発コストは、より大きな機能のコストに比べてそれほど大きくありませんでした
  • より大きな機能のコストと比較して、テストの負担は比較的少なかった
  • 文書化の負担は比較して比較的少なかった...
  • メンテナンスの負担は少ないと予想された。この機能が出荷されてから数年間、この機能で報告されたバグを思い出しません。
  • この機能は、この領域の将来の機能にすぐに明らかなリスクをもたらすことはありません。(私たちがやりたい最後のことは、安価で簡単な機能を今すぐ作成することです。これにより、将来、より魅力的な機能を実装することが非常に難しくなります。)
  • この機能により、言語の語彙、文法、または意味の分析に新しい曖昧さが追加されることはありません。入力中にIDEの「IntelliSense」エンジンによって実行される一種の「部分プログラム」分析には問題はありません。等々。
  • この機能は、より大きなオブジェクト初期化機能の一般的な「スイートスポット」に到達します。通常、オブジェクト初期化子を使用している場合は、オブジェクトのコンストラクターで必要なプロパティを設定できないためです。そのようなオブジェクトが、そもそもctorにパラメータを持たない「プロパティバッグ」であるのは非常に一般的です。

オブジェクト初期化子を持たないオブジェクト作成式のデフォルトのコンストラクター呼び出しで空の括弧もオプションにしないのはなぜですか?

上記の基準のリストをもう一度見てください。それらの1つは、変更によってプログラムの字句解析、文法解析、または意味解析に新たな曖昧さがもたらされないことです。提案された変更、意味分析のあいまいさをもたらします。

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

1行目では、新しいCを作成し、デフォルトのコンストラクターを呼び出してから、新しいオブジェクトでインスタンスメソッドMを呼び出します。2行目は、BMの新しいインスタンスを作成し、そのデフォルトのコンストラクターを呼び出します。行1の括弧がオプションの場合、行2はあいまいになります。 次に、あいまいさを解決するルールを考え出す必要があります。これは、既存の正当なC#プログラムを壊れたプログラムに変更する重大な変更になるため、エラーにできませんでした。

したがって、ルールは非常に複雑にする必要があります。基本的に、括弧はあいまいさをもたらさない場合にのみオプションです。あいまいさをもたらす可能性のあるすべてのケースを分析し、コンパイラーでコードを記述してそれらを検出する必要があります。

その観点から、前に戻って、私が言及するすべてのコストを見てください。現在、どれだけ大きくなっていますか?複雑なルールには、設計、仕様、開発、テスト、文書化のコストがかかります。複雑なルールは、将来、機能との予期しない相互作用の問題を引き起こす可能性がはるかに高くなります。

何のために?言語に新たな表現力を追加することはありませんが、それに遭遇するいくつかの貧弱で疑いのない魂に「ごちゃごちゃ」と叫ぶのを待っているだけのクレイジーなコーナーケースを追加する小さな顧客の利点。このような機能はすぐに削除され、「これを実行しない」リストに追加されます。

特定のあいまいさをどのようにして特定しましたか?

それはすぐにはっきりしました。ドットで区切られた名前が予期されるときを決定するためのC#の規則にかなり精通しています。

新しい機能を検討するとき、それが曖昧さを引き起こすかどうかをどのように判断しますか?手で、正式な証明で、機械分析で、何ですか?

3つすべて。ほとんどの場合、私は上記のように、スペックとヌードルを見ているだけです。たとえば、C#に "frob"という新しいプレフィックス演算子を追加したいとします。

x = frob 123 + 456;

(更新:frobもちろんawaitです。ここでの分析は、基本的に、設計チームが追加時に行った分析ですawait。)

ここの「フロブ」は「新しい」または「++」のようなものです-ある種の表現の前に来ます。必要な優先順位や結合性などを調べ、「プログラムにすでに型、フィールド、プロパティ、イベント、メソッド、定数、またはローカルと呼ばれるフロブがある場合はどうなるのか」などの質問をします。それはすぐに次のようなケースにつながります:

frob x = 10;

これは、「x = 10の結果に対してフロブ演算を行うか、またはxと呼ばれるフロブ型の変数を作成し、それに10を割り当てる」という意味ですか?(または、frobbingが変数を生成する場合、それは10のへの割り当てである可能性がありfrob xます。結局のところ、が*x = 10;解析され、が正しい場合xは有効ですint*。)

G(frob + x)

それは「xの単項プラス演算子の結果をフロブする」または「xに式フロブを追加する」という意味ですか?

等々。これらのあいまいさを解決するために、ヒューリスティックを導入する場合があります。「var x = 10;」と言うと それはあいまいです。「xのタイプを推論する」ことを意味することもあれば、「xはvarのタイプである」ことを意味することもあります。つまり、ヒューリスティックです。最初にvarという名前の型を検索し、存在しない場合のみxの型を推測します。

または、構文があいまいにならないように変更する場合があります。彼らがC#2.0を設計したとき、彼らはこの問題を抱えていました:

yield(x);

これは、「イテレータでの収量x」または「引数xでyieldメソッドを呼び出す」という意味ですか?に変更することで

yield return(x);

現在は明確です。

オブジェクト初期化子のオプションの括弧の場合、あいまいさが導入されているかどうかを判断するのは簡単です。これは、{で始まるものを導入することが許される状況の数が非常に少ないためです。基本的には、さまざまなステートメントコンテキスト、ステートメントラムダ、配列初期化子だけです。すべてのケースを推論し、あいまいさがないことを示すのは簡単です。IDEの効率を維持することはやや困難ですが、あまり問題なく実行できます。

通常、この種の仕様をいじるだけで十分です。それが特にトリッキーな機能である場合、より重いツールを引き出します。たとえば、LINQを設計するとき、パーサー理論のバックグラウンドを持つコンパイラーの1人とIDEの1人が、曖昧さを探して文法を分析できるパーサージェネレーターを構築し、クエリの理解のために提案されたC#文法をフィードしました; そうすることで、クエリがあいまいになる多くのケースが見つかりました。

または、C#3.0でラムダに対して高度な型推論を行ったとき、提案を書いてから、池を介してケンブリッジのMicrosoft Researchに送った。そこでは、言語チームが十分であり、型推論の提案が正式に証明された。理論的には健全です。

今日、C#にはあいまいな部分がありますか?

承知しました。

G(F<A, B>(0))

C#1では、その意味が明確です。それは同じです:

G( (F<A), (B>0) )

つまり、ブール値である2つの引数でGを呼び出します。C#2では、これはC#1での意味を意味する可能性がありますが、「型パラメーターAおよびBを受け取るジェネリックメソッドFに0を渡し、Fの結果をGに渡す」ことも意味します。パーサーに複雑なヒューリスティックを追加して、おそらく2つのケースのどちらを意味するかを決定しました。

同様に、C#1.0でもキャストはあいまいです。

G((T)-x)

それは「cast -x to T」ですか、「Tからxを引く」ですか?繰り返しになりますが、私たちは良い推測をするヒューリスティックを持っています。


3
申し訳ありませんが、忘れました...バットシグナルアプローチは機能しているように見えますが、直接的な接触手段(IMO)よりも優先されます。索引付け、検索が可能で、簡単に参照できます。代わりに、直接SOに連絡して、ステージングされたSOポスト/アンサーダンスを振り付けますか?:)
James Dunne

5
投稿をステージングしないことをお勧めします。それは、質問に追加の洞察を持っているかもしれない他の人に公平ではありません。より良い方法は、質問を投稿し、参加へのリンクをメールで送ることです。
chilltemp

1
@ジェームズ:フォローアップの質問に対処するために、回答を更新しました。
Eric Lippert

8
@エリック、この「絶対にやらない」リストについてブログを書いてもいいですか?C#言語の一部にはならない他の例を見てみたいと思います:)
Ilya Ryzhenkov

2
@エリック:私は本当に私にあなたの忍耐を本当に本当に感謝します:)ありがとう!非常に有益です。
James Dunne

12

それは言語が指定された方法だからです。それらは値を追加しないので、なぜそれらを含めるのですか?

また、暗黙的に型付けされた配列と非常に似ています

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

リファレンス:http : //msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx


1
彼らは意図が何であるかは明らかであるという意味で値を追加しませんが、括弧が必要なもの(およびコンマ区切りの引数式)とないものの2つの異なるオブジェクト構築構文があるため、一貫性がありません。
James Dunne

1
@James Dunne、それは実際には暗黙的に型付けされた配列構文と非常によく似た構文です。私の編集を参照してください。型もコンストラクターもなく、意図も明らかであるため、宣言する必要はありません
CaffGeek

7

これは、オブジェクトの構築を簡略化するために行われました。言語設計者は(私の知る限り)これが有用だと感じた理由を具体的に述べていませんが、C#バージョン3.0仕様のページで明示的に言及されています

オブジェクト作成式では、オブジェクトまたはコレクション初期化子が含まれている場合、コンストラクター引数リストと括弧を省略できます。コンストラクター引数リストと括弧を省略することは、空の引数リストを指定することと同じです。

この場合、オブジェクト初期化子はオブジェクトのプロパティを構築および設定する意図を示しているため、開発者の意図を示すために括弧は必要ないと感じたと思います。


4

最初の例では、コンパイラーはデフォルトのコンストラクターを呼び出していると推測します(C#3.0言語仕様では、括弧が指定されていない場合、デフォルトのコンストラクターが呼び出されると規定されています)。

2番目では、デフォルトのコンストラクターを明示的に呼び出します。

その構文を使用して、明示的に値をコンストラクターに渡しながら、プロパティを設定することもできます。次のクラス定義がある場合:

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

3つのステートメントはすべて有効です。

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};

正しい。あなたの例の最初と2番目のケースでは、これらは機能的に同じです、正しいですか?
James Dunne

1
@James Dunne-正解です。それは言語仕様で指定された部分です。空の括弧は冗長ですが、それでも指定できます。
Justin Niessner、2010

1

私はエリックリッペルではないので、はっきりとは言えませんが、初期化構造を推測するためにコンパイラーが空の括弧を必要としないためだと思います。したがって、冗長な情報となり、必要ありません。


それは冗長ですが、突然の導入がオプションになる理由に興味がありますか?言語構文の一貫性が損なわれるようです。初期化子ブロックを示す中括弧がなかった場合、これは違法な構文であるはずです。おもしろいことに、リッパート氏は、私や他の人たちが怠惰な好奇心から恩恵を受けることができるように、彼の答えを公に釣り上げていたのです。:)
James Dunne
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.