35

この単純なプログラムをコンパイルします。

class Program
{
    static void Foo( Action bar )
    {
        bar();
    }

    static void Main( string[] args )
    {
        Foo( () => Console.WriteLine( "42" ) );
    }
}

そこには何も奇妙なことはありません。ラムダ関数本体でエラーが発生した場合:

Foo( () => Console.LineWrite( "42" ) );

コンパイラはエラーメッセージを返します。

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'

ここまでは順調ですね。それでは、次の呼び出しで名前付きパラメーターを使用してみましょうFoo

Foo( bar: () => Console.LineWrite( "42" ) );

今回は、コンパイラメッセージはやや紛らわしいです。

error CS1502: The best overloaded method match for 
              'CA.Program.Foo(System.Action)' has some invalid arguments 
error CS1503: Argument 1: cannot convert from 'lambda expression' to 'System.Action'

どうしたの?なぜ実際のエラーを報告しないのですか?

ラムダの代わりに匿名メソッドを使用すると、正しいエラーメッセージが表示されることに注意してください。

Foo( bar: delegate { Console.LineWrite( "42" ); } );
4

3 に答える 3

36

実際のエラーが報告されないのはなぜですか?

いいえ、それが問題です。実際のエラー報告しています。

もう少し複雑な例で説明しましょう。これがあるとします:

class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

OK、C# 仕様によると、エラーは何ですか? ここでは、仕様を注意深く読む必要があります。解決しましょう。

  • Select の呼び出しは、引数が 1 つあり、型引数がない関数呼び出しです。Select という名前の呼び出し可能なもの、つまりデリゲート型のフィールドやメソッドなどを検索して、CustomerCollection の Select を検索します。型引数が指定されていないため、任意のジェネリック メソッド Select で一致します。1 つを見つけて、そこからメソッド グループを作成します。メソッド グループには 1 つの要素が含まれます。

  • メソッドグループはオーバーロード解決によって分析され、最初に候補セットを決定し、次にそれから適用可能な候補セットを決定し、そこから最適な適用可能な候補を決定し、そこから最終的に検証された最適な適用可能な候補を決定する必要があります。これらの操作のいずれかが失敗した場合、オーバーロードの解決はエラーで失敗する必要があります。それらのどれが失敗しますか?

  • 候補セットを構築することから始めます。候補を取得するには、メソッドの型推論を実行して、型引数 R の値を決定する必要があります。メソッドの型推論はどのように機能しますか?

  • パラメータの型がすべてわかっているラムダがあります。正式なパラメータは Customer です。R を決定するには、ラムダの戻り値の型から R へのマッピングを作成する必要があります。ラムダの戻り値の型は何ですか?

  • c が Customer であると仮定し、ラムダ本体の分析を試みます。これを行うと、Customer のコンテキストで FristNmae のルックアップが行われ、ルックアップは失敗します。

  • したがって、ラムダの戻り値の型の推論は失敗し、R に境界が追加されません。

  • すべての引数が分析された後、R に制限はありません。したがって、メソッドの型推論は R の型を決定できません。

  • したがって、メソッド型の推論は失敗します。

  • したがって、メソッドは候補セットに追加されません。

  • したがって、候補セットは空です。

  • したがって、該当する候補者はあり得ません。

  • したがって、ここでの正しいエラー メッセージは、「候補セットが空だったため、過負荷の解決で、最終的に検証された最適な候補を見つけることができませんでした」のようなものになります。

顧客は、このエラー メッセージに非常に不満を感じるでしょう。ユーザーが実際にエラーを修正するためのアクションを実行できる、より「根本的な」エラーを推測しようとするエラー報告アルゴリズムに、かなりの数のヒューリスティックを組み込みました。理由:

  • 実際のエラーは、候補セットが空だったことです。候補セットが空だったのはなぜですか?

  • メソッド グループにメソッドが 1 つしかなく、型の推論に失敗したためです。

OK、「メソッド型の推論に失敗したため、オーバーロードの解決に失敗しました」というエラーを報告する必要がありますか? 繰り返しますが、顧客はそれを不満に思うでしょう。代わりに、「なぜメソッドの型推論が失敗したのか?」という質問をもう一度します。

  • R の束縛集合が空だったからです。

これもひどいエラーです。境界が空に設定されたのはなぜですか?

  • R を決定できる唯一の引数は、戻り値の型を推測できないラムダだったからです。

OK、「ラムダの戻り値の型の推論が戻り値の型の推論に失敗したため、オーバーロードの解決に失敗しました」というエラーを報告する必要がありますか? 繰り返しますが、顧客はそれを不満に思うでしょう。代わりに、「ラムダが戻り値の型を推測できなかったのはなぜですか?」という質問をします。

  • Customer には FristNmae というメンバーがいないためです。

そして、それが私たちが実際に報告するエラーです。

したがって、必要なエラー メッセージを表示するために通過しなければならない推論の完全に曲がりくねった連鎖がわかります。過負荷解決に空の候補セットが与えられたということだけでは、何が問題だったかを言うことはできません。過負荷解決がどのようにしてその状態になったかを判断するには、過去を掘り下げる必要があります。

これを行うコードは非常に複雑です。これは、n 個の異なるジェネリック メソッドがあり、m 個の異なる理由で型推論が失敗する場合など、私が提示したものよりも複雑な状況を扱います。ユーザー。実際には、十数種類の選択とオーバーロードの解決があり、それらすべてがさまざまな理由または同じ理由で失敗する可能性があることを思い出してください。

コンパイラのエラー報告には、あらゆる種類のオーバーロード解決の失敗に対処するためのヒューリスティックがあります。私が説明したものは、それらの 1 つにすぎません。

それでは、あなたの特定のケースを見てみましょう。本当のエラーは何ですか?

  • Foo という 1 つのメソッドを含むメソッド グループがあります。候補セットを作成できますか?

  • はい。候補があります。メソッド Foo は、すべての必須パラメータ (bar) が指定されており、余分なパラメータがないため、呼び出しの候補です。

  • OK、候補セットにはメソッドが 1 つ含まれています。候補セットに該当するメンバーはありますか?

  • いいえ。ラムダ本体にエラーが含まれているため、bar に対応する引数を仮パラメータ型に変換できません。

  • したがって、適用可能な候補セットは空であり、最終的に検証された最適な適用可能な候補がないため、オーバーロードの解決は失敗します。

では、エラーは何でしょうか?繰り返しになりますが、「過負荷の解決は、最終的に検証された最適な候補を見つけることができなかった」とは言えません。顧客が私たちを嫌うからです。エラーメッセージを探し始める必要があります。オーバーロードの解決が失敗したのはなぜですか?

  • 該当する候補セットが空だったため。

なぜ空だったのですか?

  • その中のすべての候補者が拒否されたからです。

最適な候補はありましたか?

  • はい、候補は1つだけでした。

なぜ拒否されたのですか?

  • その引数が仮パラメータ型に変換できなかったためです。

OK、この時点で、明らかに、名前付き引数を含むオーバーロード解決の問題を処理するヒューリスティックは、十分に掘り下げたので、これが報告すべきエラーであると判断します。名前付き引数がない場合、他のヒューリスティックが次のように尋ねます。

議論が転換できなかったのはなぜですか?

  • ラムダ本体にエラーが含まれていたためです。

そして、そのエラーを報告します。

エラーヒューリスティックは完全ではありません。それからはほど遠い。偶然にも、私は今週、「単純な」オーバーロード解決エラー レポート ヒューリスティックの大幅な再構築を行っています。「2 つのパラメーターを受け取るメソッドはありませんでした」と言うときや、「必要なメソッドはプライベートです」と言うときなどです。 」、「その名前に対応するパラメーターはありません」と言う場合など。2 つの引数を持つメソッドを呼び出している可能性は十分にあります。2 つのパラメーターを持つその名前のパブリック メソッドはありません。1 つはプライベートですが、そのうちの 1 つは一致しない名前付き引数を持っています。クイック、どのエラーを報告する必要がありますか? 私たちは最善の推測をしなければなりません.時には、私たちが行うことができたかもしれないが、行うのに十分なほど洗練されていないより良い推測がある場合があります.

それを正しく行うことでさえ、非常にトリッキーな仕事であることが証明されています. 最終的に、LINQ 式内のメソッド型推論の失敗に対処する方法など、非常に負荷の高いヒューリスティックを再設計する段階になったら、あなたのケースを再検討し、ヒューリスティックを改善できるかどうかを確認します。

しかし、表示されるエラー メッセージは完全に正しいため、これはコンパイラのバグではありません。むしろ、特定のケースでのエラー報告ヒューリスティックの欠点にすぎません。

于 2011-11-08T20:26:51.637 に答える
6

編集:エリック・リッパートの回答は、問題を(はるかによく)説明しています-「本物」については彼の回答を参照してください

最終編集: 人が自分の無知を公に証明することを野放しにするのは不愉快なことですが、削除ボタンを押して無知を覆い隠すことには何のメリットもありません。うまくいけば、他の誰かが私の奇妙な答えから恩恵を受けることができます:)

Eric Lippert と svick に感謝します。


ここで「間違った」エラー メッセージが表示される理由は、コンパイラが名前付きパラメータの型解決を処理する方法と組み合わされた、型の差異とコンパイラの推論によるものです。

代表例の型 () => Console.LineWrite( "42" )

型推論と共分散の魔法により、これは次の結果と同じになります。

Foo( bar: delegate { Console.LineWrite( "42" ); } );

最初のブロックは、タイプLambdaExpressionまたはdelegate;のいずれかです。どちらであるかは、使用法と推論に依存します。

Actionそれを考えると、異なる型の共変オブジェクトである可能性があるパラメーターをコンパイラーに渡すと、コンパイラーが混乱するのも不思議ではありませんか? エラー メッセージは、型解決が問題であることを示す主なキーです。

さらに手がかりを得るために IL を見てみましょう: 与えられたすべての例は、LINQPad でこれにコンパイルされます:

IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0005:  brtrue.s    IL_0018
IL_0007:  ldnull      
IL_0008:  ldftn       UserQuery.<Main>b__0
IL_000E:  newobj      System.Action..ctor
IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_001D:  call        UserQuery.Foo

Foo:
IL_0000:  ldarg.0     
**IL_0001:  callvirt    System.Action.Invoke**
IL_0006:  ret         

<Main>b__0:
IL_0000:  ldstr       "42"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret

System.Action.Invoke:への呼び出しの周りの ** は、callvirtまさにそのように見えることに注意してください: 仮想メソッド呼び出しです。

Foo名前付き引数を指定して呼び出すと、実際に渡しているのActionは. 通常、これは にコンパイルされます ( ctor for の後に呼び出される IL 内の に注意してください)が、アクションを渡すことをコンパイラに明示的に伝えたので、渡された を式として扱うのではなく、として使用しようとします。!LambdaExpressionCachedAnonymousMethodDelegate1ActionActionLambdaExpressionAction

短い: 名前付きパラメーターの解決は、ラムダ式のエラーが原因で失敗します (これは、それ自体がハードな失敗です)。

他の言い方は次のとおりです。

Action b = () => Console.LineWrite("42");
Foo(bar: b);

予想されるエラー メッセージが表示されます。

一部の IL については 100% 正確ではないかもしれませんが、一般的な考え方を伝えていただければ幸いです。

編集: dlev は、オーバーロード解決の順序についても OP のコメントで重要な役割を果たしていることを指摘しました。

于 2011-11-08T16:50:20.273 に答える
4

注:実際には答えではありませんが、コメントするには大きすぎます。

型推論を投入すると、より興味深い結果が得られます。次のコードを検討してください。

public class Test
{
    public static void Blah<T>(Action<T> blah)
    {
    }

    public static void Main()
    {
        Blah(x => { Console.LineWrite(x); });
    }
}

何をすべきかを推測する良い方法がないため、コンパイルされませTん。
エラーメッセージ:

メソッドの型引数は'Test.Blah<T>(System.Action<T>)'、使用法から推測できません。型引数を明示的に指定してみてください。

理にかなっています。xの型を明示的に指定して、何が起こるか見てみましょう。

public static void Main()
{
    Blah((int x) => { Console.LineWrite(x); });
}

存在しないため、物事はうまくいきLineWriteません。
エラーメッセージ:

「System.Console」には「LineWrite」の定義が含まれていません

また賢明です。名前付き引数を追加して、何が起こるか見てみましょう。まず、型を指定せずにx:

public static void Main()
{
    Blah(blah: x => { Console.LineWrite(x); });
}

型引数を推論できないというエラー メッセージが表示されることが予想されます。そして、私たちはそうします。しかしそれだけではありません
エラーメッセージ:

メソッドの型引数は'Test.Blah<T>(System.Action<T>)'、使用法から推測できません。型引数を明示的に指定してみてください。

「System.Console」には「LineWrite」の定義が含まれていません

きちんとした。型の推論が失敗し、ラムダ変換が失敗した理由が正確にわかりますでは、 の型を指定して、x何が得られるか見てみましょう。

public static void Main()
{
    Blah(blah: (int x) => { Console.LineWrite(x); });
}

エラーメッセージ:

メソッドの型引数は'Test.Blah<T>(System.Action<T>)'、使用法から推測できません。型引数を明示的に指定してみてください。

「System.Console」には「LineWrite」の定義が含まれていません

は予想外です。型推論はまだ失敗しており (ラムダ ->Action<T>変換が失敗しているため、コンパイラの推測であるTが無効になっているためだと思いますint) 、失敗の原因を報告しています

TL; DR : Eric Lippert がこれらのより複雑なケースのヒューリスティックを調べてくれたら嬉しいです。

于 2011-11-08T21:04:20.667 に答える