関数を呼び出すために、匿名メソッド(またはラムダ構文)ではなくメソッドグループ構文を使用したいシナリオがあります。
この関数には2つのオーバーロードがあり、1つはAction
を受け取り、もう1つはFunc<string>
を受け取ります。
匿名メソッド(またはラムダ構文)を使用して2つのオーバーロードを喜んで呼び出すことができますが、メソッドグループ構文を使用すると、Ambiguous invocationのコンパイラエラーが発生します。 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() { }
}
2019年3月20日(この質問を投稿してから9年後)の xcde のコメントによると、このコードは 改善されたオーバーロード候補 のおかげでC#7.3以降でコンパイルされます。
まず、ジョンの答えが正しいとだけ言っておきましょう。これは、仕様の中で最も毛深い部分の1つであるため、Jonが最初に頭に飛び込むのに適しています。
第二に、この行を言ってみましょう:
メソッドグループから互換のデリゲート型への暗黙的な変換が存在します
(強調を追加)は、非常に誤解を招き、不幸です。ここでは、Wordsの「互換性」を削除することについてMadsと話します。
これが誤解を招く不幸な理由は、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);
}
}
それでは、行ごとに見ていきましょう。
メソッドグループから互換性のあるデリゲート型への暗黙的な変換が存在します。
ここで、「互換性のある」という言葉がいかに不幸であるかについてすでに説明しました。続けて。 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++));
式ツリーでは、インクリメント操作は無効です。ただし、ラムダはまだ式ツリータイプに対してconvertibleです。これは、変換が使用されたとしてもエラーです。ここでの原則は、後で式ツリーに入れることができるもののルールを変更することです。これらのルールを変更しても、型システムルールは変更されません。プログラムを明確にするnowにすることを強制したいので、将来的に式ツリーのルールを変更してより良いものにするために、オーバーロードに重大な変更を導入しません解像度。
とにかく、これはこの種の奇妙なルールの別の例です。変換はオーバーロード解決の目的で存在する場合がありますが、実際に使用するにはエラーになります。実際のところ、それはまさにここにいる状況ではありません。
次へ:
E(A) [...]という形式のメソッド呼び出しに対応する単一のメソッドMが選択されます。引数リストAは、それぞれ変数[..として分類される式のリストです。 。] Dの仮パラメータリスト内の対応するパラメータの.
OK。したがって、D1に関してXのオーバーロード解決を行います。 D1の仮パラメーターリストは空なので、X() and joyでオーバーロード解決を行い、動作するメソッド "string X()"を見つけます。同様に、 D2は空ですが、「string X()」はここでも機能するメソッドであることがわかります。
ここでの原則は、メソッドグループの変換可能性を判断するには、オーバーロード解決を使用してメソッドグループからメソッドを選択する必要があるであり、オーバーロード解決では戻り型を考慮しません。
アルゴリズム[...]でエラーが発生すると、コンパイル時エラーが発生します。それ以外の場合、アルゴリズムはDと同じ数のパラメーターを持つ単一の最適なメソッドMを生成し、変換が存在すると見なされます。
メソッドグループXにはメソッドが1つしかないため、最適なものである必要があります。 XからD1へ、およびXからD2への変換existsがあることが証明されました。
さて、この行は関連していますか?
選択されたメソッドMは、デリゲート型Dと互換性がある必要があります。そうでない場合、コンパイル時エラーが発生します。
実際、このプログラムではありません。この行をアクティブ化することはありません。なぜなら、ここで行っているのは、Y(X)でオーバーロード解決を試みているからです。 2つの候補Y(D1)およびY(D2)があります。どちらも適用可能です。どちらがbetter?仕様のどこにも記述していません。これらの2つの可能な変換間の改善。
今では、エラーを生成する変換よりも有効な変換の方が優れていると断言できます。この場合、オーバーロード解決は戻り値の型を考慮しますが、これは回避したいことです。問題は、(1)オーバーロード解決が戻り値の型を考慮しないという不変式を維持するか、(2)動作しないことがわかっている変換を選択しようとすることです。
これは判断の呼びかけです。 lambdasの場合、doでは、これらの種類の変換の戻り値の型をセクション7.4.3.3で検討します。
Eは匿名関数であり、T1とT2は同一のパラメーターリストを持つデリゲート型または式ツリー型であり、そのパラメーターリストのコンテキストでEの推定戻り型Xが存在し、次のいずれかが成り立ちます。
T1には戻りタイプY1があり、T2には戻りタイプY2があり、XからY1への変換はXからY2への変換よりも優れています。
T1には戻りタイプYがあり、T2にはvoidが戻ります
この点で、メソッドグループの変換とラムダ変換が矛盾しているのは残念です。しかし、私はそれで生きることができます。
とにかく、XからD1またはXからD2のどちらの変換が優れているかを判断するための「より良い」規則はありません。したがって、Y(X)の解像度にあいまいさのエラーを与えます。
編集:私はそれを持っていると思う。
Zinglonが言うように、それは、コンパイル時のアプリケーションが失敗したとしても、GetString
からAction
への暗黙的な変換があるためです。セクション6.6の概要をいくつか強調して説明します(私の場合)。
メソッドグループ(§7.1)から互換性のあるデリゲート型への暗黙的な変換(§6.1)が存在します。デリゲート型Dとメソッドグループとして分類される式Eが与えられると、Eに少なくとも1つのメソッドが含まれる場合、EからDへの暗黙的な変換が存在しますその標準形式(§7.4.3.1)に適用可能)以下で説明するように、パラメータータイプとDの修飾子を使用して構築された引数リスト。
今、私は最初の文で混乱していた-それは互換性のあるデリゲート型への変換について語っています。 Action
は、GetString
メソッドグループ内のメソッドの互換性のあるデリゲートではありませんが、GetString()
メソッドisは、 Dのパラメータタイプと修飾子を使用して構築された引数リスト。これはdoes n'tがDの戻り値のタイプについて説明していることに注意してください。それが混乱する理由です... applyingの場合、GetString()
の互換性を委任します。変換の存在をチェックしません。
オーバーロードを簡単に方程式から外し、変換のexistenceとそのapplicabilityの違いがどのように現れるかを見るのは有益だと思います。これは短いが完全な例です:
_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()'の戻りタイプが間違っています
今回は、呼び出したいメソッドを作成しましたが、必要な変換を実行できませんでした。残念ながら、最終チェックが実行される仕様のビットを見つけることができません-それはそのように見えますmightは7.5.5.1にありますが、どこで正確に見ることができません。
このビットを除いて、古い答えは削除されました-エリックがこの質問の「理由」に光を当てることができると思うからです...
まだ見ている...その間、「Eric Lippert」を3回言うと、私たちは訪問(そして、答え)を得ると思いますか?
Action
でFunc<string>
およびAction<string>
(明らかにClassWithDelegateMethods
およびFunc<string>
とは大きく異なります)を使用すると、あいまいさがなくなります。
Action
とFunc<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(); }
}
Func
とAction
のオーバーロードは似ています(両方ともデリゲートであるため)
string Function() // Func<string>
{
}
void Function() // Action
{
}
気づいた場合、コンパイラは戻り値の型のみが異なるため、どちらを呼び出すかをコンパイラが認識しません。