Compiler Kesalahan pemanggilan ambigu - metode anonim dan grup metode dengan Func <> atau Action

102

Saya memiliki skenario di mana saya ingin menggunakan sintaks grup metode daripada metode anonim (atau sintaks lambda) untuk memanggil suatu fungsi.

Fungsi ini memiliki dua beban berlebih, satu kelebihan beban, yang Actionlainnya menerima Func<string>.

Saya dengan senang hati dapat memanggil dua kelebihan beban menggunakan metode anonim (atau sintaks lambda), tetapi mendapatkan kesalahan penyusun dari pemanggilan ambigu jika saya menggunakan sintaks grup metode. Saya dapat mengatasinya dengan mentransmisikan secara eksplisit ke Actionatau Func<string>, tetapi menurut saya ini tidak perlu.

Adakah yang bisa menjelaskan mengapa pemeran eksplisit harus diperlukan.

Contoh kode di bawah ini.

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() { }
}

Pembaruan C # 7.3

Sesuai komentar 0xcde di bawah ini pada 20 Maret 2019 (sembilan tahun setelah saya memposting pertanyaan ini!), Kode ini dikompilasi pada C # 7.3 berkat peningkatan kandidat yang kelebihan beban .

Richard Ev
sumber
Saya telah mencoba kode Anda dan saya mendapatkan kesalahan waktu kompilasi tambahan: 'void test.ClassWithSimpleMethods.DoNothing ()' memiliki jenis pengembalian yang salah (yaitu di baris 25, di situlah kesalahan ambiguitas)
Matt Ellen
@ Mat: Saya melihat kesalahan itu juga. Kesalahan yang saya kutip dalam posting saya adalah masalah kompilasi yang disoroti oleh VS bahkan sebelum Anda mencoba kompilasi lengkap.
Richard Ev
1
Ngomong-ngomong, ini adalah pertanyaan yang bagus. Saya suka apa pun yang memaksa saya masuk ke spesifikasi :)
Jon Skeet
1
Perhatikan bahwa kode sampel Anda akan dikompilasi jika Anda menggunakan C # 7.3 ( <LangVersion>7.3</LangVersion>) atau lebih baru berkat kandidat kelebihan beban yang ditingkatkan .
0xced

Jawaban:

97

Pertama, izinkan saya mengatakan bahwa jawaban Jon benar. Ini adalah salah satu bagian paling hairiest dari spesifikasi, sangat bagus bagi Jon untuk menyelami kepalanya terlebih dahulu.

Kedua, izinkan saya mengatakan bahwa baris ini:

Ada konversi implisit dari grup metode ke tipe delegasi yang kompatibel

(penekanan ditambahkan) sangat menyesatkan dan sangat disayangkan. Saya akan berbicara dengan Mads tentang menghapus kata "kompatibel" di sini.

Alasan mengapa hal ini menyesatkan dan tidak menguntungkan adalah karena sepertinya ini memanggil bagian 15.2, "Kompatibilitas delegasi". Bagian 15.2 menjelaskan hubungan kompatibilitas antara metode dan tipe delegasi , tetapi ini adalah pertanyaan tentang konvertibilitas grup metode dan tipe delegasi , yang berbeda.

Sekarang setelah kita menyelesaikannya, kita dapat berjalan melalui bagian 6.6 dari spesifikasi dan melihat apa yang kita dapatkan.

Untuk melakukan resolusi kelebihan beban, pertama-tama kita perlu menentukan kelebihan beban mana yang merupakan kandidat yang berlaku . Kandidat dapat diterapkan jika semua argumen secara implisit dapat diubah menjadi tipe parameter formal. Pertimbangkan versi program Anda yang disederhanakan ini:

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);
    }
}

Jadi mari kita bahas baris demi baris.

Ada konversi implisit dari grup metode ke tipe delegasi yang kompatibel.

Saya sudah membahas bagaimana kata "kompatibel" tidak menguntungkan di sini. Bergerak. Kami bertanya-tanya ketika melakukan resolusi kelebihan beban pada Y (X), apakah metode grup X dikonversi ke D1? Apakah itu dikonversi ke D2?

Diberikan tipe delegasi D dan ekspresi E yang diklasifikasikan sebagai grup metode, konversi implisit ada dari E ke D jika E berisi setidaknya satu metode yang dapat diterapkan [...] ke daftar argumen yang dibuat dengan menggunakan parameter jenis dan pengubah D, seperti yang dijelaskan berikut ini.

Sejauh ini baik. X mungkin berisi metode yang dapat diterapkan dengan daftar argumen D1 atau D2.

Aplikasi waktu kompilasi konversi dari metode grup E ke tipe delegasi D dijelaskan berikut ini.

Baris ini benar-benar tidak mengatakan sesuatu yang menarik.

Perhatikan bahwa keberadaan konversi implisit dari E ke D tidak menjamin bahwa aplikasi waktu kompilasi konversi akan berhasil tanpa kesalahan.

Garis ini sangat menarik. Ini berarti bahwa ada konversi implisit yang ada, tetapi dapat berubah menjadi kesalahan! Ini adalah aturan C # yang aneh. Untuk menyimpang sejenak, berikut contohnya:

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

Operasi increment ilegal di pohon ekspresi. Namun, lambda masih dapat diubah menjadi tipe pohon ekspresi, meskipun jika konversi pernah digunakan, itu adalah kesalahan! Prinsipnya di sini adalah bahwa kita mungkin ingin mengubah aturan tentang apa yang bisa masuk dalam pohon ekspresi nanti; mengubah aturan tersebut seharusnya tidak mengubah aturan sistem jenis . Kami ingin memaksa Anda untuk membuat program Anda tidak ambigu sekarang , sehingga saat kami mengubah aturan pohon ekspresi di masa mendatang untuk membuatnya lebih baik, kami tidak memperkenalkan perubahan yang melanggar dalam resolusi kelebihan beban .

Bagaimanapun, ini adalah contoh lain dari aturan aneh semacam ini. Sebuah konversi bisa saja ada untuk tujuan resolusi yang berlebihan, tapi bisa menjadi kesalahan untuk benar-benar digunakan. Padahal sebenarnya, bukan itu situasi kita saat ini.

Bergerak:

Sebuah metode tunggal M dipilih sesuai dengan pemanggilan metode dari bentuk E (A) [...] Daftar argumen A adalah daftar ekspresi, masing-masing diklasifikasikan sebagai variabel [...] dari parameter yang sesuai secara formal -parameter-daftar D.

BAIK. Jadi kami melakukan resolusi overload pada X sehubungan dengan D1. Daftar parameter formal D1 kosong, jadi kami melakukan resolusi berlebih pada X () dan kegembiraan, kami menemukan metode "string X ()" yang berfungsi. Demikian pula, daftar parameter formal D2 kosong. Sekali lagi, kami menemukan bahwa "string X ()" adalah metode yang berfungsi di sini juga.

Prinsipnya di sini adalah bahwa menentukan konvertibilitas grup metode memerlukan pemilihan metode dari grup metode menggunakan resolusi kelebihan beban , dan resolusi kelebihan beban tidak mempertimbangkan jenis kembalian .

Jika algoritma [...] menghasilkan kesalahan, maka kesalahan waktu kompilasi terjadi. Jika tidak, algoritme menghasilkan satu metode terbaik M yang memiliki jumlah parameter yang sama dengan D dan konversinya dianggap ada.

Hanya ada satu metode dalam metode grup X, jadi itu pasti yang terbaik. Kami telah berhasil membuktikan bahwa ada konversi dari X ke D1 dan dari X ke D2.

Sekarang, apakah baris ini relevan?

Metode yang dipilih M harus kompatibel dengan tipe delegasi D, atau jika tidak, kesalahan waktu kompilasi terjadi.

Sebenarnya, tidak, tidak dalam program ini. Kami tidak pernah sejauh mengaktifkan baris ini. Karena, ingat, yang kami lakukan di sini adalah mencoba melakukan resolusi kelebihan beban pada Y (X). Kami memiliki dua kandidat Y (D1) dan Y (D2). Keduanya dapat diterapkan. Mana yang lebih baik ? Tidak ada dalam spesifikasi yang kami gambarkan tentang kehebatan antara dua kemungkinan konversi ini .

Sekarang, orang pasti dapat berargumen bahwa konversi yang valid lebih baik daripada konversi yang menghasilkan kesalahan. Itu kemudian akan secara efektif mengatakan, dalam kasus ini, bahwa resolusi kelebihan beban TIDAK mempertimbangkan jenis kembalian, yang merupakan sesuatu yang ingin kita hindari. Pertanyaannya kemudian adalah prinsip mana yang lebih baik: (1) pertahankan invarian bahwa resolusi kelebihan beban tidak mempertimbangkan jenis pengembalian, atau (2) mencoba memilih konversi yang kita tahu akan berhasil yang kita tahu tidak akan?

Ini adalah panggilan penghakiman. Dengan lambda , kami melakukannya mempertimbangkan tipe kembali di semacam ini konversi, di bagian 7.4.3.3:

E adalah fungsi anonim, T1 dan T2 adalah tipe delegasi atau tipe pohon ekspresi dengan daftar parameter yang identik, tipe X yang disimpulkan ada untuk E dalam konteks daftar parameter tersebut, dan salah satu dari yang berikut ini:

  • T1 memiliki tipe pengembalian Y1, dan T2 memiliki tipe pengembalian Y2, dan konversi dari X ke Y1 lebih baik daripada konversi dari X ke Y2

  • T1 memiliki tipe pengembalian Y, dan T2 tidak berlaku kembali

Sayangnya, konversi grup metode dan konversi lambda tidak konsisten dalam hal ini. Namun, saya bisa menerimanya.

Bagaimanapun, kami tidak memiliki aturan "betterness" untuk menentukan konversi mana yang lebih baik, X ke D1 atau X ke D2. Oleh karena itu kami memberikan kesalahan ambiguitas pada resolusi Y (X).

Eric Lippert
sumber
8
Cracking - terima kasih banyak atas jawabannya dan (mudah-mudahan) peningkatan yang dihasilkan dalam spesifikasi :) Secara pribadi saya pikir akan masuk akal untuk resolusi yang berlebihan untuk mempertimbangkan jenis pengembalian untuk konversi grup metode untuk membuat perilakunya lebih intuitif, tetapi Saya mengerti itu akan dilakukan dengan mengorbankan konsistensi. (Hal yang sama dapat dikatakan tentang jenis inferensi umum seperti yang diterapkan pada konversi grup metode ketika hanya ada satu metode dalam grup metode, seperti yang saya pikir telah kita diskusikan sebelumnya.)
Jon Skeet
35

EDIT: Saya pikir saya sudah mendapatkannya.

Seperti kata zinglon, itu karena ada konversi implisit dari GetStringmenjadiAction meskipun aplikasi waktu kompilasi akan gagal. Berikut pengantar ke bagian 6.6, dengan beberapa penekanan (milik saya):

Ada konversi implisit (§6.1) dari grup metode (§7.1) ke tipe delegasi yang kompatibel. Diberikan tipe delegasi D dan ekspresi E yang diklasifikasikan sebagai grup metode, konversi implisit ada dari E ke D jika E berisi setidaknya satu metode yang dapat diterapkan dalam bentuk normalnya (§7.4.3.1) ke daftar argumen yang dibuat dengan menggunakan tipe parameter dan pengubah D , seperti yang dijelaskan berikut ini.

Sekarang, saya menjadi bingung dengan kalimat pertama - yang berbicara tentang konversi ke tipe delegasi yang kompatibel. Actionbukan delegasi yang kompatibel untuk metode apa pun dalam GetStringgrup metode, tetapi GetString()metode ini dapat diterapkan dalam bentuk normalnya ke daftar argumen yang dibuat dengan menggunakan tipe parameter dan pengubah D. Perhatikan bahwa ini tidak berbicara tentang tipe kembalian dari D. Itulah mengapa ini menjadi membingungkan ... karena hanya akan memeriksa kompatibilitas delegasi GetString()ketika menerapkan konversi, bukan memeriksa keberadaannya.

Saya pikir itu instruktif untuk meninggalkan persamaan secara singkat, dan melihat bagaimana perbedaan antara keberadaan konversi dan penerapannya dapat terwujud. Berikut contoh singkat tapi lengkap:

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);
    }
}

Tak satu pun dari ekspresi pemanggilan metode dalam Mainkompilasi, tetapi pesan kesalahannya berbeda. Ini dia untuk IntMethod(GetString):

Test.cs (12,9): error CS1502: Metode overloaded terbaik yang cocok untuk 'Program.IntMethod (int)' memiliki beberapa argumen yang tidak valid

Dengan kata lain, bagian 7.4.3.1 dari spesifikasi tidak dapat menemukan anggota fungsi yang berlaku.

Sekarang, inilah kesalahan untuk ActionMethod(GetString):

Test.cs (13,22): error CS0407: 'string Program.GetString ()' memiliki tipe pengembalian yang salah

Kali ini berhasil menemukan metode yang ingin dipanggil - tetapi gagal kemudian melakukan konversi yang diperlukan. Sayangnya saya tidak dapat menemukan sedikit pun dari spesifikasi tempat pemeriksaan terakhir itu dilakukan - sepertinya itu mungkin di 7.5.5.1, tetapi saya tidak dapat melihat persis di mana.


Jawaban lama dihapus, kecuali untuk bagian ini - karena saya berharap Eric bisa menjelaskan "mengapa" dari pertanyaan ini ...

Masih mencari ... sementara itu, jika kita mengatakan "Eric Lippert" tiga kali, apakah menurut Anda kita akan mendapat kunjungan (dan dengan demikian jawaban)?

Jon Skeet
sumber
@ Jon - mungkinkah itu classWithSimpleMethods.GetStringdan classWithSimpleMethods.DoNothingbukan delegasi?
Daniel A. White
@Daniel: Tidak - ekspresi tersebut adalah ekspresi grup metode, dan metode yang kelebihan beban seharusnya hanya dianggap berlaku bila ada konversi implisit dari grup metode ke jenis parameter yang relevan. Lihat bagian 7.4.3.1 dari spesifikasi.
Jon Skeet
Membaca bagian 6.6, tampaknya konversi dari classWithSimpleMethods.GetString ke Action dianggap ada karena daftar parameter kompatibel, tetapi konversi (jika dicoba) gagal pada waktu kompilasi. Oleh karena itu, konversi implisit tidak ada untuk kedua jenis delegasi dan panggilan ambigu.
zinglon
@zinglon: Bagaimana Anda membaca §6.6 untuk menentukan bahwa konversi dari ClassWithSimpleMethods.GetStringke Actionadalah valid? Untuk metode Magar kompatibel dengan tipe delegasi D(§15.2) "identitas atau konversi referensi implisit ada dari tipe kembalian Mke tipe kembalian D."
jason
@Jason: Spesifikasi tidak menyatakan bahwa konversi itu valid, tetapi ada . Faktanya, ini tidak valid karena gagal pada waktu kompilasi. Dua poin pertama dari §6.6 menentukan apakah konversi itu ada. Poin-poin berikut menentukan apakah konversi akan berhasil. Dari poin 2: "Jika tidak, algoritme menghasilkan satu metode terbaik M yang memiliki jumlah parameter yang sama dengan D dan konversinya dianggap ada." §15.2 dipanggil dalam poin 3.
zinglon
1

Menggunakan Func<string>dan Action<string>(jelas sangat berbeda dengan Actiondan Func<string>) di fileClassWithDelegateMethods menghilangkan ambiguitas.

Ambiguitas juga terjadi antara ActiondanFunc<int> .

Saya juga mendapatkan kesalahan ambiguitas dengan ini:

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; }
} 

Eksperimen lebih lanjut menunjukkan bahwa saat meneruskan grup metode sendiri, jenis kembalian sepenuhnya diabaikan saat menentukan kelebihan beban yang akan digunakan.

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(); }
} 
Matt Ellen
sumber
0

Overloading dengan Funcdan Actionserupa (karena keduanya adalah delegasi)

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

void Function() // Action
{
}

Jika Anda perhatikan, kompilator tidak tahu mana yang harus dipanggil karena mereka hanya berbeda menurut tipe kembalian.

Daniel A. White
sumber
Saya tidak berpikir itu benar-benar seperti itu - karena Anda tidak dapat mengubah a Func<string>menjadi Action... dan Anda tidak dapat mengonversi grup metode yang hanya terdiri dari metode yang mengembalikan string menjadi Actionkeduanya.
Jon Skeet
2
Anda tidak dapat mentransmisikan delegasi yang tidak memiliki parameter dan kembali stringke Action. Saya tidak mengerti mengapa ada ambiguitas.
jason
3
@dtb: Ya, menghapus kelebihan beban akan menghilangkan masalah - tetapi itu tidak benar-benar menjelaskan mengapa ada masalah.
Jon Skeet