1. ホーム
  2. c#

[解決済み] コンパイラの曖昧な呼び出しエラー - Func<>またはActionを持つ無名メソッドとメソッドグループ

2023-02-27 07:11:48

質問

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

この関数には2つのオーバーロードがあり、1つは 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 の2019年3月20日(私がこの質問を投稿してから9年後!)の下のコメントのように、このコードはC# 7.3の時点で、おかげでコンパイルされます。 改良されたオーバーロード候補 .

どのように解決するのですか?

まず最初に、Jonの回答が正しいということをお伝えします。これは仕様で最も厄介な部分の 1 つであり、真っ先にこれに飛び込んだ Jon は偉いです。

第二に、この行を言わせてください。

暗黙の変換が存在し、メソッドグループから 互換性のあるデリゲートタイプ

(強調) は深く誤解を招くものであり、残念なことです。 私は Mads と話して、ここで "compatible" という単語を削除してもらうことにします。

これが誤解を招き、残念なことに、15.2節「"Delegate compatibility"」を呼び出しているように見えるからです。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);
    }
}

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

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

ここで "compatible"という言葉がいかに不幸なものであるかは既に述べたとおりです。次に進みます。私たちは、Y(X) で過負荷解消を行うときに、メソッド グループ X は D1 に変換されるのか? それは D2 に変換されますか?

デリゲートタイプDと メソッドグループとして分類される式Eがある場合 メソッドグループとして分類される式Eが与えられたとき、暗黙の変換 が存在する場合、EからDへの暗黙の変換は に適用可能なメソッドが少なくとも1つ含まれる場合、EからDへの暗黙の変換が存在する。 の引数リストに対して適用可能なメソッドが少なくとも1つ含まれる場合,EからDへの暗黙の変換が存在する。 のパラメータ型と修飾子を用いて構築された引数リストに適用可能な[...]であるメソッドを少なくとも一つ含む場合,EからDへの暗黙の変換が存在する。 Dのパラメータ型と修飾子を用いて構築された引数リストに適用可能な[...]なメソッドを少なくとも一つ含む場合に,EからDへの暗黙の変換が存在する。

ここまではいいとして Xには、D1やD2の引数リストで適用できるメソッドがあるかもしれません。

メソッド群Eからデリゲート型Dへの変換のコンパイル時の適用を以下に記述する。

この行は本当に何も面白いことを言っていません。

<ブロッククオート

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

この行は魅力的です。暗黙の変換が存在するが、それがエラーになる可能性があるということです!これはC#の奇妙なルールです。これはC#の奇妙なルールです。ちょっと脱線しますが、ここに例があります。

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

式木ではインクリメント操作は違法である。しかし、ラムダは 変換可能 を式木型に変換することができます。この原則は、式木に入れられるものの規則を後で変更したいと思うかもしれないということである。 型システムのルール . 私たちは、あなたのプログラムを曖昧さのないものにすることを強制したいのです では そうすることで、将来的に式木のルールをより良いものにするために変更したときに 過負荷の解決に壊れるような変更を導入しないようにするためです。 .

とにかく、これはこの種の奇妙なルールのもう一つの例です。変換は、過負荷の解決のために存在することができますが、実際に使用するとエラーになります。しかし、実際には、私たちがここでいるのは正確にはそのような状況ではないのです。

次に進みます。

<ブロッククオート

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

OK。そこで、D1に関してXのオーバーロード解消を行う。D1の正式なパラメータリストは空なので、X()のオーバーロード解決を行い、喜び、動作するメソッド "string X()" が見つかりました。同様に、D2 の正式なパラメータリストは空です。ここでも、"string X()"が動作するメソッドであることがわかります。

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

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

メソッド群Xには1つのメソッドしかないので、それがベストである必要があります。という変換の証明に成功しました。 が存在する をXからD1へ、XからD2へ変換することに成功した。

さて、この行は関係あるのでしょうか?

選択されたメソッドMはデリゲートタイプDと互換性がなければならず、そうでなければコンパイル時エラーが発生します。

実は、このプログラムでは、そうではありません。この行をアクティブにするまでには至らないのです。 なぜなら、ここでやっていることは、Y(X)の過負荷解消をしようとしているのです。Y(D1) と Y(D2) の 2 つの候補があります。どちらも適用可能です。どちらかというと より良い ? 仕様のどこにも、これらの2つの可能な変換の間のbetternessを記述していません。 .

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

これは判断材料になります。とは ラムダ では、私たちは する は、7.4.3.3節で、この種の変換における戻り値の型について考察しています。

Eは無名関数、T1およびT2 はデリゲート型または式木型 型であり、同一のパラメータリストを持つ。 推論された戻り値の型XはE に存在し、そのパラメータリストのコンテキストで であり、以下のいずれかが成立する。

  • T1は戻り値の型がY1であり、T2は戻り値の型がY2であり、かつ、XからY1への変換は よりもXからY1への変換が優れている。 XからY2への変換

  • T1は戻り値の型がYで、T2は戻り値がvoidです。

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

とにかく、XからD1への変換とXからD2への変換のどちらが良いかを決定するための "betterness"ルールがないのです。したがって、私たちはY(X)の解決に曖昧さエラーを出します。