12

この例はC#コンパイラのバグを示していると思います(間違っている場合は私をからかってください)。このバグはよく知られているかもしれません。結局のところ、この例は、このブログ投稿で説明されている内容を単純に変更したものです。

using System;

namespace GenericConflict
{
  class Base<T, S>
  {
    public virtual int Foo(T t)
    { return 1; }
    public virtual int Foo(S s)
    { return 2; }

    public int CallFooOfT(T t)
    { return Foo(t); }
    public int CallFooOfS(S s)
    { return Foo(s); }
  }

  class Intermediate<T, S> : Base<T, S>
  {
    public override int Foo(T t)
    { return 11; }
  }

  class Conflict : Intermediate<string, string>
  {
    public override int Foo(string t)
    { return 101;  }
  }


  static class Program
  {
    static void Main()
    {
      var conflict = new Conflict();
      Console.WriteLine(conflict.CallFooOfT("Hello mum"));
      Console.WriteLine(conflict.CallFooOfS("Hello mum"));
    }
  }
}

アイデアはBase<T, S>、2つの仮想メソッドを使用してクラスを作成することです。このクラスのシグネチャは、との「悪意のある」選択の後に同一にTなりSます。クラスConflictは仮想メソッドの1つだけをオーバーロードします。が存在するため、どのメソッドIntermediate<,>を明確に定義する必要があります。

しかし、プログラムを実行すると、出力は間違った過負荷がオーバーライドされたことを示しているようです。

Sam Ngのフォローアップ投稿を読むと、タイプロード例外が常にスローされると彼らが信じていたため、そのバグは修正されなかったという表現が得られました。しかし、この例では、コードはコンパイルされ、エラーなしで実行されます(予期しない出力のみ)。


2020年の追加:これは、C#コンパイラの新しいバージョン(Roslyn?)で修正されました。この質問をしたときの出力は次のとおりです。

11
101

2020年の時点で、次の出力tio.runが得られます。

101
2
4

1 に答える 1

21

この例は、C#コンパイラのバグを示していると思われます。

コンパイラのバグを示すときに常に行うべきことを実行しましょう。予想される動作と観察される動作を注意深く対比します。

観察された動作は、プログラムが1番目と2番目の出力としてそれぞれ11と101を生成することです。

期待される動作は何ですか?2つの「仮想スロット」があります。Foo(T)最初の出力は、スロット内のメソッドを呼び出した結果である必要があります。Foo(S)2番目の出力は、スロット内のメソッドを呼び出した結果である必要があります。

それらのスロットには何が入りますか?

メソッドのインスタンスではスロットに入り、Base<T,S>メソッドはスロットに入ります。return 1Foo(T)return 2Foo(S)

メソッドのインスタンスでは、スロットに入り、Intermediate<T,S>メソッドはスロットに入ります。 return 11Foo(T)return 2Foo(S)

うまくいけば、これまでのところあなたは私に同意します。

のインスタンスではConflict、4つの可能性があります。

  • 可能性1:return 11メソッドはFoo(T)スロットに入り、return 101メソッドはFoo(S)スロットに入ります。
  • 可能性2:return 101メソッドはFoo(T)スロットに入り、return 2メソッドはFoo(S)スロットに入ります。
  • 可能性3:return 101メソッドは両方のスロットに入ります。
  • 可能性4:コンパイラーは、プログラムがあいまいであることを検出し、エラーを発行します。

仕様のセクション10.6.4に基づいて、ここで2つのことのいずれかが発生することを期待します。また:

  1. コンパイラは、中間クラスのメソッドが最初に見つかるため、のメソッドがのメソッドをConflictオーバーライドすると判断します。Intermediate<string, string>この場合、可能性2が正しい動作です。または:
  2. コンパイラーは、のメソッドがどの元のConflict宣言をオーバーライドするかについてあいまいであると判断するため、可能性4が正しいものです。

どちらの場合も、正しい可能性はありません。

確かに、この2つのうちどちらが正しいかは100%明確ではありません。私の個人的な感想は、より賢明な振る舞いは、オーバーライドするメソッドを中間クラスのプライベート実装の詳細として扱うことです。私の考えに関連する質問は、中間クラスが基本クラスのメソッドをオーバーライドするかどうかではなく、一致するシグネチャを持つメソッドを宣言するかどうかです。その場合、正しい動作は可能性4を選択することです。

コンパイラが実際に行うことは、期待どおりです。可能性2を選択します。中間クラスには一致するメンバーがあるため、メソッドが中間クラスで宣言されていないにもかかわらず、「オーバーライドするもの」として選択します。コンパイラーは、それIntermediate<string, string>.Fooがによってオーバーライドされたメソッドであると判断しConflict.Foo、それに応じてコードを出力します。プログラムにエラーがないと判断するため、エラーは発生しません。

では、コンパイラがコードを正しく分析し、可能性2を選択し、エラーを生成しない場合、実行にコンパイラが可能性2ではなく可能性1を選択したように見えるのはなぜですか?

一般的な構造の下で2つのメソッドを統合するプログラムを作成することは、ランタイムの実装定義の動作であるためです。この場合、ランタイムは何でも実行することを選択できます。タイプロードエラーを与えることを選択できます。検証エラーが発生する可能性があります。プログラムを許可することを選択できますが、独自に選択した基準に従ってスロットを埋めます。そして実際、後者はそれが行うことです。ランタイムは、C#コンパイラーによって発行されたプログラムを調べて、このプログラムを分析する正しい方法である可能性を独自に判断します。

これで、これがコンパイラのバグであるかどうかという、かなり哲学的な質問があります。コンパイラーは仕様の合理的な解釈に従っていますが、それでも期待どおりの動作は得られません。その意味で、それは非常にコンパイラのバグです。コンパイラの仕事は、C#で記述されたプログラムをILで記述されたまったく同等のプログラムに変換することです。コンパイラはそうすることに失敗しています。これは、C#で記述されたプログラムを、C#言語仕様で指定された動作ではなく、実装定義の動作を持つILで記述されたプログラムに変換しています。

サムがブログ投稿で明確に説明しているように、C#言語が特定の意味を与えるトポロジと、CLRが特定の意味を与えるトポロジとの間のこの不一致をよく認識しています。C#言語は、可能性2がほぼ間違いなく正しいものであることを合理的に明確にしていますが、2つのメソッドが同じシグネチャを持つように統合するときはいつでもCLRは基本的に実装定義の動作をするため、CLRにそれを行わせるコードはありません。したがって、私たちの選択は次のとおりです。

  • 何もしない。これらのクレイジーで非現実的なプログラムが、C#仕様と正確に一致しない動作を継続できるようにします。
  • ヒューリスティックを使用します。Samが指摘しているように、メタデータメカニズムを使用して、どのメソッドが他のどのメソッドをオーバーライドするかをCLRに伝える方が賢明かもしれません。しかし...これらのメカニズムは、メソッドシグネチャを使用してあいまいなケースを明確にし、今では以前と同じボートに戻っています。現在、実装定義の動作でプログラムを明確にするために、実装定義の動作でメカニズムを使用しています。これは初心者ではありません。
  • 実行時によって動作が実装定義されているプログラムを発行する可能性がある場合は常に、コンパイラーに警告またはエラーを生成させます。
  • メソッドがシグニチャで統一される原因となる型トポロジの動作が明確に定義され、C#言語の動作と一致するように、CLRを修正します。

最後の選択は非常に高価です。そのコストを支払うことで、ユーザーの利益はほとんどなくなり、賢明なプログラムを作成するユーザーが直面する現実的な問題の解決から直接予算を奪うことになります。そして、いずれにせよ、それを行うという決定は完全に私の手に負えません。

したがって、C#コンパイラチームでは、最初の戦略と3番目の戦略を組み合わせて使用​​することを選択しました。このような状況で警告やエラーが発生することもあれば、何もせずにプログラムが実行時に奇妙なことを実行できるようにすることもあります。

実際には、この種のプログラムが現実的な基幹業務プログラミングシナリオで発生することはめったにないので、これらのコーナーケースについてはそれほど悪くはありません。それらが安価で修正が容易な場合、私たちはそれらを修正しますが、それらは安価でも修正も容易ではありません。

この主題に興味がある場合は、2つのメソッドを統合させることで、警告と実装定義の動作が発生する、さらに別の方法に関する私の記事を参照してください。

http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx

http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx

于 2012-04-16T17:52:53.137 に答える