web-dev-qa-db-ja.com

演算子を短絡する|| &&はnull許容ブール値に存在しますか? RuntimeBinderは時々そう思う

条件付き論理演算子_||_および_&&_のC#言語仕様を読みます。これは、短絡論理演算子とも呼ばれます。 。私には、これらがnull許容ブール値、つまり、オペランド型_Nullable<bool>_(または_bool?_とも書かれている)に存在するかどうかは不明なようだったので、動的でない型付けを試してみました:

_bool a = true;
bool? b = null;
bool? xxxx = b || a;  // compile-time error, || can't be applied to these types
_

これで問題は解決したようです(仕様を明確に理解できませんでしたが、Visual C#コンパイラの実装が適切であると想定していたので、今はわかりました)。

ただし、dynamicバインディングも試してみました。だから私は代わりにこれを試しました:

_static class Program
{
  static dynamic A
  {
    get
    {
      Console.WriteLine("'A' evaluated");
      return true;
    }
  }
  static dynamic B
  {
    get
    {
      Console.WriteLine("'B' evaluated");
      return null;
    }
  }

  static void Main()
  {
    dynamic x = A | B;
    Console.WriteLine((object)x);
    dynamic y = A & B;
    Console.WriteLine((object)y);

    dynamic xx = A || B;
    Console.WriteLine((object)xx);
    dynamic yy = A && B;
    Console.WriteLine((object)yy);
  }
}
_

驚くべき結果は、これが例外なく実行されることです。

まあ、xyは驚くべきことではなく、それらの宣言は両方のプロパティを取得し、結果の値は期待どおりです。xtrueであり、ynullです。

ただし、_A || B_のxxの評価ではバインディング時の例外は発生せず、Aではなく、プロパティBのみが読み取られました。なぜこれが起こるのですか?おわかりのように、Bゲッターを変更して、_"Hello world"_などのクレイジーオブジェクトを返すことができます。また、xxは、バインディングの問題なしにtrueに評価されます...

_A && B_(yyの場合)を評価しても、バインド時エラーは発生しません。もちろん、ここでは両方のプロパティが取得されます。なぜこれがランタイムバインダーによって許可されるのですか? Bから返されたオブジェクトが「不良」オブジェクト(stringなど)に変更された場合、バインディング例外が発生します。

これは正しい動作ですか?(仕様からそれをどのように推測できますか?)

最初のオペランドとしてBを試すと、_B || A_と_B && A_の両方がランタイムバインダー例外を発生させます(_B | A_と_B & A_は、非短絡演算子ではすべて正常であるため、正常に動作します_|_および_&_)。

(Visual Studio 2013のC#コンパイラ、およびランタイムバージョン.NET 4.5.2で試してみました。)

84

まず第一に、非ダイナミックnullable-boolのケースでは仕様が明確でないことを指摘してくれてありがとう。今後のバージョンで修正する予定です。コンパイラの動作は意図された動作です。 _&&_および_||_は、null許容のブール値では機能しません。

ただし、動的バインダーはこの制限を実装していないようです。代わりに、コンポーネント操作を個別にバインドします:_&_/_|_および_?:_。したがって、最初のオペランドがtrueまたはfalse(ブール値であり、したがって_?:_の最初のオペランドとして許可されている)である場合は、それを混乱させることができますが、 nullを最初のオペランドとして(たとえば、上記の例で_B && A_を試行すると)、ランタイムバインディング例外が発生します。

考えてみれば、動的な_&&_と_||_を1つの大きな動的演算としてではなく、このように実装した理由がわかります。動的演算は実行時にバインドされますオペランドが評価された後なので、バインディングはそれらの評価結果の実行時のタイプに基づくことができます。しかし、そのような熱心な評価は、短絡演算子の目的を無効にします。したがって、代わりに、動的_&&_および_||_に対して生成されたコードは、評価を細かく分割し、次のように進行します。

  • 左のオペランドを評価します(結果をxと呼びましょう)
  • 暗黙の変換、またはboolまたはtrue演算子を使用してfalseに変換してみてください(できない場合は失敗します)
  • _?:_操作の条件としてxを使用します
  • Trueブランチでは、結果としてxを使用します
  • Falseブランチでは、-nowは2番目のオペランドを評価します(結果をyと呼びましょう)
  • xおよびyのランタイムタイプに基づいて_&_または_|_演算子をバインドしてみてください(できない場合は失敗します)
  • 選択した演算子を適用します

これは、特定の「不正な」オペランドの組み合わせを許可する動作です。_?:_演算子は、最初のオペランドをnon-nullableブール値、_&_または_|_演算子はそれをnullableブール値として正常に扱い、両者が一致することを確認するために調整することはありません。

したがって、動的な&&および||ではありません。 nullableを処理します。静的な場合と比較して、それらがたまたま寛大に実装されているだけです。これはおそらくバグと考えるべきですが、重大な変更となるため、これを修正することは決してありません。また、それはだれも行動を引き締めることをほとんど助けません。

うまくいけば、これで何が起こり、その理由が説明されます。これは興味深い領域であり、動的を実装したときに行った決定の結果に困惑することがよくあります。この質問はおいしかったです-出してくれてありがとう!

マッド

67

これは正しい動作ですか?

はい、確かにそうです。

仕様からそれをどのように推測できますか?

C#仕様バージョン5.0のセクション7.12には、条件演算子_&&_および_||_と、動的バインディングがそれらにどのように関連するかに関する情報があります。関連セクション:

条件付き論理演算子のオペランドがコンパイル時の型が動的である場合、式は動的にバインドされます(§7.2.2)。この場合、式のコンパイル時の型は動的であり、以下で説明する解決は、これらのオペランドの実行時の型を使用して実行時に行われますコンパイル時の型が動的です。

これがあなたの質問に答えるキーポイントだと思います。実行時に発生する解決策は何ですか?セクション7.12.2、ユーザー定義の条件付き論理演算子では、以下について説明します。

  • 演算x && yはT.false(x)として評価されますか? x:T。&(x、y)、ここでT.false(x)はTで宣言された演算子falseの呼び出し、T。&(x、y)は選択された演算子&の呼び出し
  • 操作x || yはT.true(x)として評価されますか? x:T. |(x、y)、ここでT.true(x)はTで宣言された演算子trueの呼び出しであり、T。|(x、y)は選択された演算子|の呼び出しです。

どちらの場合も、最初のオペランドxは、falseまたはtrue演算子を使用してブール値に変換されます。次に、適切な論理演算子が呼び出されます。これを念頭に置いて、私たちはあなたの残りの質問に答えるのに十分な情報を持っています。

しかし、Aのxxの評価|| Bはバインド時の例外を発生させず、プロパティAのみが読み取られ、Bは読み取られませんでした。なぜこれが起こるのですか?

_||_演算子の場合、true(A) ? A : |(A, B)の後に続くことがわかります。短絡しているため、拘束時間の例外は発生しません。 Afalseであったとしても、指定された解決手順のため、stillはランタイムバインディング例外を受け取りません。 。 Afalseの場合、セクション7.11.4に従って、Null値を正常に処理できる_|_演算子を実行します。

A && B(yyの場合)を評価しても、バインド時エラーは発生しません。もちろん、ここでは両方のプロパティが取得されます。なぜこれがランタイムバインダーによって許可されるのですか? Bから返されたオブジェクトが「不良」オブジェクト(文字列など)に変更されると、バインディング例外が発生します。

同様の理由で、これも機能します。 _&&_はfalse(x) ? x : &(x, y)として評価されます。 Aboolに正常に変換できるため、問題はありません。 Bがnullであるため、_&_演算子は、boolを使用する演算子から_bool?_パラメーターを使用する演算子に引き上げられます(セクション7.3.7)。したがって、実行時例外はありません。

両方の条件演算子で、Bがブール(またはnull動的)以外の場合、ブールと非ブールをパラメーターとして取るオーバーロードを検出できないため、ランタイムバインディングは失敗します。ただし、これはAが演算子の最初の条件を満たさない場合にのみ発生します(trueは_||_の場合、falseは_&&_の場合)。これが発生する理由は、動的バインディングが非常に遅延しているためです。 Aがfalseで、論理演算子を評価するためにそのパスを下がらなければならない場合を除き、論理演算子をバインドしようとはしません。 Aが演算子の最初の条件を満たすことができないと、バインディング例外で失敗します。

最初のオペランドとしてBを試した場合、両方のB || AおよびB && Aは、実行時バインダー例外を発生させます。

うまくいけば、今までに、あなたはすでにこれがなぜ起こるのか知っています(または私は説明する悪い仕事をしました)。この条件演算子を解決する最初のステップは、最初のオペランドBを取り、ブール値変換演算子(false(B)またはtrue(B))の1つを使用してから、論理演算。もちろん、B、つまりnulltrueまたはfalseに変換できないため、ランタイムバインディング例外が発生します。

6