web-dev-qa-db-ja.com

最後の呼び出しが条件付きであるときに、C#コンパイラがメソッド呼び出しのチェーンを削除するのはなぜですか?

次のクラスを検討してください。

public class A {
    public B GetB() {
        Console.WriteLine("GetB");
        return new B();
    }
}

public class B {
    [System.Diagnostics.Conditional("DEBUG")]
    public void Hello() {
        Console.WriteLine("Hello");
    }
}

さて、メソッドをこの方法で呼び出す場合:

var a = new A();
var b = a.GetB();
b.Hello();

リリースビルド(つまり、DEBUGフラグなし)では、Hello()の呼び出しがコンパイラによって省略されるため、コンソールにGetBのみが表示されます。デバッグビルドでは、両方の出力が表示されます。

次に、メソッド呼び出しをチェーンしましょう:

a.GetB().Hello();

デバッグビルドの動作は変更されていません。ただし、フラグが設定されていない場合は、異なる結果が得られます。both呼び出しは省略され、コンソールに印刷は表示されません。 ILを簡単に見ると、行全体がコンパイルされていなかったことがわかります。

C#の最新のECMA標準 (ECMA-334、つまりC#5.0)によると、Conditional属性がメソッドに配置されたときの予想される動作は次のとおりです(エンファシスマイニング):

関連する条件付きコンパイルシンボルの1つ以上が呼び出しポイントで定義されている場合、条件付きメソッドの呼び出しが含まれます。それ以外の場合呼び出しは省略されます。 (§22.5.3)

これは、チェーン全体を無視する必要があることを示していないようです。したがって、私の質問です。とはいえ、 MicrosoftのC#6.0ドラフト仕様 にはもう少し詳細があります。

シンボルが定義されている場合、呼び出しが含まれます。それ以外の場合、呼び出し(受信側の評価と呼び出しのパラメーターを含む)は省略されます。

呼び出しのパラメーターが評価されないという事実は、関数本体で#ifディレクティブではなくこの機能を使用する理由の1つであるため、十分に文書化されています。ただし、「受信者の評価」に関する部分は新しいものです。他の場所で見つけることはできないようで、上記の動作を説明しているようです。

これに照らして、私の質問は:この状況でC#コンパイラが評価しないa.GetB()理由は何ですか?条件付き呼び出しの受信者が一時変数に格納されているかどうかに基づいて異なる動作をしますか?

69
Kyrio

少し掘り下げてみると、 C#5.0言語仕様 には実際にセクションに2番目の引用符が既に含まれていることがわかりました17.4.2条件属性424ページ。

Marc Gravellの答え はすでに、この動作が意図されていることと実際に意味することを示しています。また、この背後にある根拠についても質問しましたが、Marcのオーバーヘッドの除去については不満のようです。

たぶん、あなたはなぜそれが除去できるオーバーヘッドと考えられているのだろうか?

a.GetB().Hello();が省略されてシナリオでまったく呼び出されないHello()は、額面では奇妙に見えるかもしれません。

私はこの決定の背後にある理論的根拠を知りませんが、私自身の推論をもっともらしく見つけました。たぶんあなたにも役立つでしょう。

メソッドチェーン は、前の各メソッドに戻り値がある場合にのみ可能です。これは、これらの値、つまりa.GetFoos().MakeBars().AnnounceBars();を使用して何かをしたい場合に意味があります。

値を返さずに何かをdoesするだけの関数がある場合、その後ろに何かを連鎖することはできませんが、条件付きの場合のように、メソッドチェーンの最後に置くことができますメソッドは、戻り値の型がvoidでなければならないためです。

また、前のメソッド呼び出しのresultは、throwawayを取得します。したがって、a.GetB().Hello();の例では、GetB()の結果には、このステートメントが実行された後に生きる理由がありません。基本的に、あなたは暗黙的にGetB()の結果が必要なのはHello()を使用する場合だけです。

Hello()が省略されている場合、なぜGetB()にする必要があるのですか? Hello()を省略すると、行は割り当てられずにa.GetB();になり、多くのツールは戻り値を使用しないことを警告します。

これで大丈夫でないように見える理由は、メソッドが特定の値を返すために必要なことをしようとしているだけでなく、 副作用 、つまりI/Oも持っているからです。代わりに 純粋な関数 を使用した場合、後続の呼び出しを省略した場合、つまり、次のような場合、GetB()really理由はありません結果に対して何もしません。

GetB()の結果を変数に割り当てた場合、これはそれ自体のステートメントであり、とにかく実行されます。この理由は

var b = a.GetB();
b.Hello();

Hello()への呼び出しのみが省略され、メソッドチェーンを使用する場合はチェーン全体が省略されます。

より完全な視点を得るために、まったく異なる場所を探すこともできます。C#6.0で導入された null-conditional operator または elvis operator? nullチェックを使用したより複雑な式の構文シュガーだけですが、nullチェックに基づいて短絡するオプションを使用して、メソッドチェーンのようなものを構築できます。

例えば。 GetFoos()?.MakeBars()?.AnnounceBars();は、前のメソッドがnullを返さない場合にのみ最後に到達します。そうでない場合、後続の呼び出しは省略されます。

直観に反するかもしれませんが、シナリオをこれの逆と考えてみてください。チェーンの終わりに到達していないので、コンパイラはHello()チェーンのa.GetB().Hello();の前の呼び出しを省略します。


免責事項

これはすべてアームチェアの推論でしたので、これとエルビス演算子との類似性を一粒の塩で取ってください。

13

次のフレーズになります。

(受信者の評価と呼び出しのパラメーターを含む)は省略されます。

式では:

a.GetB().Hello();

「受信者の評価」はa.GetB()です。そのため:仕様によるは省略され、[Conditional]使用されていないのオーバーヘッドを回避できる便利なトリックです。ローカルに配置する場合:

var b = a.GetB();
b.Hello();

「レシーバの評価」はローカルbだけですが、元のvar b = a.GetB();はまだ評価されます(ローカルbが削除された場合でも)。

このcanは意図しない結果になるため、[Conditional]を慎重に使用してください。しかし、その理由は、ロギングやデバッグなどを簡単に追加および削除できるようにするためです。パラメーターは、単純に処理するとalsoが問題になる可能性があることに注意してください。

LogStatus("added: " + engine.DoImportantStuff());

そして:

var count = engine.DoImportantStuff();
LogStatus("added: " + count);

LogStatus[Conditional]とマークされている場合は、veryと異なる場合があります。その結果、実際の「重要なもの」が完了しませんでした。

62
Marc Gravell

条件付き呼び出しの受信者が一時変数に格納されているかどうかに基づいて、実際に異なる動作をする必要がありますか?

はい。

この状況でa.GetB()を評価しないC#コンパイラの背後にある理由は何ですか?

MarcとSørenからの答えは基本的に正しいです。この答えは、タイムラインを明確に文書化することです。

  • この機能は1999年に設計されたもので、この機能の目的は常にステートメント全体を削除することでした。
  • 2003年の設計ノートは、設計チームがその時点で仕様が不明確であることを認識したことを示しています。この時点まで、仕様はargumentsが評価されないことだけを呼びました。私は、仕様が引数を「パラメーター」と呼ぶ一般的な間違いを犯していることに注意しますが、もちろん「公式パラメーター」ではなく「実際のパラメーター」を意味すると推測できます。
  • この点でECMA仕様を修正するために、作業項目が作成されることになっていた。どうやらそれは決して起こらなかった。
  • 修正されたテキストがC#仕様で初めて表示されるのは2010年だと思うC#4.0仕様でした。
  • 2017 ECMA仕様にこの修正が含まれていない場合、それは次のリリースで修正されるべき間違いです。絶対に15年遅れていると思います。
19
Eric Lippert