コンパイラーのあいまいな呼び出しエラー-Func <>またはActionを持つ匿名メソッドおよびメソッドグループ


102

関数の呼び出しに匿名メソッド(またはラムダ構文)ではなくメソッドグループ構文を使用するシナリオがあります。

関数は、2つのオーバーロード、取るものを持ってAction、他はかかりますFunc<string>

匿名メソッド(またはラムダ構文)を使用して2つのオーバーロードを喜んで呼び出すことができますが、メソッドグループ構文を使用すると、あいまいな呼び出しのコンパイラエラーが発生します。ActionまたはFunc<string>に明示的にキャストすることで回避できますが、これは必要ないと思います。

明示的なキャストが必要な理由を誰かが説明できますか?

以下のコードサンプル。

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

C#7.3アップデート

あたりとして0xcdeさんにC#7.3の感謝のように2019年3月20日に以下のコメント(私はこの質問を投稿9年後!)、このコードのコンパイルの改善過負荷候補


私はあなたのコードを試しましたが、追加のコンパイル時エラーが発生します: 'void test.ClassWithSimpleMethods.DoNothing()'の戻り値の型が間違っています(あいまいさのエラーがある25行目にあります)
Matt Ellen

@マット:私もそのエラーを見ます。私の投稿で引用したエラーは、完全なコンパイルを試みる前にVSが強調するコンパイルの問題でした。
Richard Ev

1
ちなみに、これは素晴らしい質問でした。私は仕様に私を強制するものは何でも大好きです:)
Jon Skeet '13年

1
オーバーロード候補の改善<LangVersion>7.3</LangVersion>により、C#7.3()以降を使用すると、サンプルコードがコンパイルされることに注意してください。
0xced

回答:


97

まず、ジョンの答えが正しいとだけ言っておきましょう。これはスペックの中で最も毛深い部分の1つなので、Jonが最初に頭に飛び込むのに適しています。

第二に、この行を言いましょう:

メソッドグループから互換性のあるデリゲート型への暗黙的な変換が存在します

(強調が追加されています)は非常に誤解を招きやすく、残念です。ここで「互換性のある」という言葉を削除することについて、マッドと話をします。

これが誤解を招く不幸な理由は、これがセクション15.2「デリゲート互換性」を要求しているように見えるためです。セクション15.2 では、メソッドとデリゲート型の間の互換性関係について説明しましたが、これはメソッドグループとデリゲート型の変換可能性の問題であり、これは異なります。

これで問題が解決したので、仕様のセクション6.6をウォークスルーして、何が得られるかを確認できます。

オーバーロードの解決を行うには、まず、どのオーバーロードが適用可能な候補であるかを判別する必要があります。すべての引数が暗黙的に仮パラメーター型に変換可能な場合、候補が適用されます。プログラムのこの簡略化されたバージョンを考えてみましょう:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

では、1行ずつ見ていきましょう。

メソッドグループから互換性のあるデリゲート型への暗黙的な変換が存在します。

「互換性のある」という言葉が残念なことに、ここですでに説明しました。次へ。Y(X)でオーバーロード解決を実行すると、メソッドグループXはD1に変換されますか?D2に変換されますか?

デリゲートタイプDとメソッドグループとして分類される式Eが与えられた場合、Eにパラメーターを使用して作成された引数リストに適用可能なメソッドが少なくとも1つ含まれる場合、EからDへの暗黙の変換が存在します[...]以下で説明するように、Dのタイプと修飾子。

ここまでは順調ですね。Xには、D1またはD2の引数リストに適用できるメソッドが含まれている場合があります。

メソッドグループEからデリゲート型Dへの変換のコンパイル時の適用について、以下で説明します。

この行は本当に興味深いことは何も言っていません。

EからDへの暗黙的な変換の存在は、変換のコンパイル時の適用がエラーなしで成功することを保証しないことに注意してください。

このラインは魅力的です。つまり、暗黙の変換が存在しますが、エラーになる可能性があります!これはC#の奇妙なルールです。少し離れて、ここに例があります:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

式ツリーでは、インクリメント操作は無効です。しかし、ラムダはまだありコンバーチブル変換が今まで使用されている場合、それは誤りであっても、式ツリー型に!ここでの原則は、後で式ツリーに入れることができるルールを変更したいということです。これらのルールを変更しても、型システムのルールは変更されません。私たちはあなたのプログラムを明確にするためにあなたを強制したい今、私たちはより良いそれらを作るために、将来的に式ツリーのルールを変更したときに、そのことを、我々はオーバーロードの解決の変化を壊す導入しません

とにかく、これはこの種の奇妙なルールの別の例です。変換は、オーバーロードの解決を目的として存在する場合がありますが、実際に使用するとエラーになります。実際には、それは私たちがここにいる状況とは正確には異なります。

次に進む:

E(A)[...]形式のメソッド呼び出しに対応する単一のメソッドMが選択されます。引数リストAは式のリストであり、それぞれが形式内の対応するパラメーターの変数[...]として分類されます。 -Dのパラメーターリスト

OK。したがって、D1に関してXで多重定義解決を行います。D1の仮パラメーターリストは空なので、X()とjoyのオーバーロード解決を実行して、機能するメソッド「string X()」を見つけます。同様に、D2の仮パラメーターリストは空です。繰り返しになりますが、「string X()」はここでも機能するメソッドです。

ここでの原則は、メソッドグループの変換可能性決定するには、オーバーロードの解決を使用してメソッドグループからメソッドを選択する必要がありオーバーロードの解決では戻り値の型が考慮されないことです

アルゴリズム[...]がエラーを生成すると、コンパイル時エラーが発生します。そうでない場合、アルゴリズムは、Dと同じ数のパラメーターを持つ単一の最良のメソッドMを生成し、変換が存在すると見なされます。

メソッドグループXにはメソッドが1つしかないため、それが最良の方法である必要があります。XからD1 への変換とXからD2への変換が存在することが証明されました。

さて、この行は関連していますか?

選択されたメソッドMは、デリゲートタイプDと互換性がある必要があります。そうでない場合、コンパイル時エラーが発生します。

実際、いいえ、このプログラムではありません。このラインをアクティブにすることはできません。覚えておいてください。ここで行っているのは、Y(X)のオーバーロード解決を行うことです。Y(D1)とY(D2)の2つの候補があります。どちらも適用されます。どちらが良いですか? 仕様のどこにも、これら2つの可能な変換の間の優れた点を説明していません

さて、確かに、有効な変換はエラーを生成する変換よりも優れていると主張することができます。つまり、この場合、オーバーロードの解決では戻り値の型が考慮されるので、これは避けたいものです。次に問題はどちらの原則が優れているかです:(1)オーバーロードの解決で戻り値の型が考慮されないという不変条件を維持するか、または(2)機能することがわかっている変換を、機能しないことがわかっている変換よりも選択しようとしますか?

これは判断の呼びかけです。ラムダ、我々はやるセクション7.4.3.3で、変換のこの種では戻り値の型を考えてみます。

Eは無名関数であり、T1とT2は同一のパラメーターリストを持つデリゲート型または式ツリー型であり、推測された戻り型XはそのパラメーターリストのコンテキストでEに存在し、次のいずれかが成り立ちます。

  • T1は戻り型Y1を持ち、T2は戻り型Y2を持ち、XからY1への変換はXからY2への変換よりも優れています。

  • T1は戻り型Yを持ち、T2はvoidを返します

メソッドグループ変換とラムダ変換がこの点で矛盾しているのは残念です。しかし、私はそれと共存できます。

とにかく、XからD1またはXからD2への変換のどちらが優れているかを判断する「より良い」ルールはありません。したがって、Y(X)の解像度にあいまいなエラーを与えます。


8
クラッキング-答えと(できれば)仕様の改善の両方に感謝します:)個人的には、動作をより直感的にするために、オーバーロードの解決でメソッドグループの変換に戻り値の型を考慮するのが妥当だと思いますが、一貫性を犠牲にしてそうすることは理解しています。(これまでに説明したように、メソッドグループにメソッドが1つしかない場合にメソッドグループの変換に適用されるジェネリック型推論についても同じことが言えます。)
Jon Skeet

35

編集:私はそれを持っていると思います。

zinglonが言うように、コンパイル時のアプリケーションが失敗GetStringActionても、からへの暗黙の変換があるためです。これはセクション6.6の概要ですが、いくつかの強調点があります(私のもの):

メソッドグループ(§7.1)から互換性のあるデリゲート型への暗黙的な変換(§6.1)が存在します。デリゲートタイプDとメソッドグループとして分類される式Eが与えられた場合、Eが通常の形式(§7.4.3.1)で適用可能なメソッドを少なくとも1つ構築された引数リストに適用できる場合、EからDへの暗黙の変換が存在します。以下で説明するように、Dのパラメータータイプと修飾子を使用する

さて、互換性のあるデリゲート型への変換について述べている最初の文に戸惑いました。ActionGetStringメソッドグループ内のどのメソッドとも互換性のあるデリゲートではありませんが、GetString()メソッド通常の形式で、Dのパラメータータイプと修飾子を使用して構築された引数リストに適用できます。これ、 D.それが混乱している理由です... 変換を適用するGetString()ときに、デリゲートの互換性のみをチェックし、その存在をチェックしないためです。

方程式のオーバーロードを簡単に除外して、コンバージョンの存在とその適用性の違いがどのように現れるかを確認することは有益だと思います。以下は短いが完全な例です。

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Mainコンパイルのメソッド呼び出し式はどちらもありませんが、エラーメッセージは異なります。これは次のものですIntMethod(GetString)

Test.cs(12,9):エラーCS1502: 'Program.IntMethod(int)'のオーバーロードされたメソッドの最適な一致に無効な引数がいくつかあります

つまり、仕様のセクション7.4.3.1は、該当する関数メンバーを見つけることができません。

ここにエラーがありますActionMethod(GetString)

Test.cs(13,22):エラーCS0407: 'string Program.GetString()'の戻り値の型が間違っています

今回は、呼び出すメソッドを解決しましたが、必要な変換を実行できませんでした。残念ながら、最終的なチェックが行われる仕様のビットを見つけることはできません。7.5.5.1のように見えるかもしれませんが、正確な場所はわかりません。


エリックがこの質問の「なぜ」に光を当てることができると思うので、このビットを除いて古い答えは削除されました...

まだ見ています...その間に「エリックリッペルト」を3回言った場合、訪問(つまり、回答)が得られると思いますか?


@ジョン-それはそれであり、代議員classWithSimpleMethods.GetStringclassWithSimpleMethods.DoNothingはないのでしょうか?
ダニエルA.ホワイト

@ダニエル:いいえ-それらの式はメソッドグループ式であり、オーバーロードされたメソッドは、メソッドグループから関連するパラメータータイプへの暗黙的な変換がある場合にのみ適用可能と見なされます。仕様のセクション7.4.3.1を参照してください。
Jon Skeet

セクション6.6を読むと、classWithSimpleMethods.GetStringからActionへの変換はパラメーターリストに互換性があるため存在すると見なされているようですが、変換時に(変換しようとすると)コンパイル時に失敗します。したがって、デリゲート型の両方に暗黙の変換存在し、呼び出しがあいまいです。
ジンロン

@zinglon:からClassWithSimpleMethods.GetStringへの変換Actionが有効であると判断するために、6.6をどのように読んでいますか?メソッドMがデリゲート型D(15.2)と互換性を持つためには、「戻り型のMから戻り型へのアイデンティティまたは暗黙の参照変換が存在しDます。」
ジェイソン

@ジェイソン:仕様では変換が有効であるとは言われておらず、存在していると書かれています。実際、コンパイル時に失敗するため無効です。§6.6の最初の2つのポイントは、変換が存在するかどうかを決定します。次のポイントは、変換が成功するかどうかを決定します。ポイント2から:「それ以外の場合、アルゴリズムはDと同じ数のパラメーターを持つ単一の最良のメソッドMを生成し、変換は存在すると見なされます。」§15.2はポイント3で呼び出されます
zinglon、2013年

1

使用するFunc<string>と、Action<string>(明らかに非常に異なるActionFunc<string>で)ClassWithDelegateMethodsあいまいさを削除します。

との間にもあいまいさが発生ActionFunc<int>ます。

私もこれで曖昧なエラーを受け取ります:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

さらなる実験により、メソッドグループをそれ自体で渡す場合、使用するオーバーロードを決定するときに戻りの型が完全に無視されることが示されています。

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 

0

Funcおよびとのオーバーロードは、Action(両方ともデリゲートであるため)

string Function() // Func<string>
{
}

void Function() // Action
{
}

コンパイラーは、戻り値の型が異なるだけなので、どれを呼び出すかがわかりません。


私はそれが本当にそうだとは思いません-あなたはa Func<string>Action...に変換できないし、文字列を返すメソッドだけで構成されるメソッドグループもに変換できないからActionです。
Jon Skeet、2013年

2
あなたには、パラメータとリターンがないデリゲートキャストすることはできませんstringにしますAction。あいまいさがある理由はわかりません。
ジェイソン

3
@dtb:はい、オーバーロードを削除すると問題が削除されますが、それでも問題が発生する理由を実際に説明しているわけではありません。
Jon Skeet
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.