Masalah pemahaman kontradiksi kovarian dengan obat generik di C #

115

Saya tidak mengerti mengapa kode C # berikut tidak dapat dikompilasi.

Seperti yang Anda lihat, saya memiliki metode generik statis Sesuatu dengan IEnumerable<T>parameter (dan Tdibatasi menjadi IAantarmuka), dan parameter ini tidak dapat dikonversi secara implisit IEnumerable<IA>.

Apa penjelasannya? (Saya tidak mencari solusi, hanya untuk memahami mengapa itu tidak berhasil).

public interface IA { }
public interface IB : IA { }
public class CIA : IA { }
public class CIAD : CIA { }
public class CIB : IB { }
public class CIBD : CIB { }

public static class Test
{
    public static IList<T> Something<T>(IEnumerable<T> foo) where T : IA
    {
        var bar = foo.ToList();

        // All those calls are legal
        Something2(new List<IA>());
        Something2(new List<IB>());
        Something2(new List<CIA>());
        Something2(new List<CIAD>());
        Something2(new List<CIB>());
        Something2(new List<CIBD>());
        Something2(bar.Cast<IA>());

        // This call is illegal
        Something2(bar);

        return bar;
    }

    private static void Something2(IEnumerable<IA> foo)
    {
    }
}

Kesalahan saya Something2(bar)mengantre:

Argumen 1: tidak dapat mengonversi dari 'System.Collections.Generic.List' menjadi 'System.Collections.Generic.IEnumerable'

BenLaz
sumber
12
Anda tidak membatasi Tjenis referensi. Jika Anda menggunakan kondisi tersebut where T: class, IAmaka itu harus bekerja. Jawaban terkait memiliki detail lebih lanjut.
Dirk
2
@ Dirk Saya rasa ini tidak harus ditandai sebagai duplikat. Meskipun benar bahwa masalah konsep di sini adalah masalah kovarian / kontravarian dalam menghadapi jenis nilai, kasus spesifiknya di sini adalah "apa arti pesan kesalahan ini" serta penulis tidak menyadari hanya menyertakan "kelas" untuk memperbaiki masalahnya. Saya yakin pengguna di masa mendatang akan mencari pesan kesalahan ini, menemukan posting ini, dan pergi dengan bahagia. (Seperti yang sering saya lakukan.)
Reginald Blue
Anda juga dapat mereproduksi situasi hanya dengan mengatakan Something2(foo);secara langsung. Berkeliling .ToList()untuk mendapatkan List<T>( Tadalah parameter tipe Anda dideklarasikan dengan metode umum) tidak diperlukan untuk memahami ini (a List<T>adalah an IEnumerable<T>).
Jeppe Stig Nielsen
@ReginaldBlue 100%, akan memposting hal yang sama. Jawaban serupa tidak membuat pertanyaan duplikat.
UuDdLrLrSs

Jawaban:

218

Pesan kesalahan tidak cukup informatif, dan itu salah saya. Maaf soal itu.

Masalah yang Anda alami adalah konsekuensi dari fakta bahwa kovarian hanya bekerja pada tipe referensi.

Anda mungkin mengatakan "tapi IAmerupakan tipe referensi" sekarang. Ya itu. Tapi Anda tidak mengatakan itu T sama dengan IA . Anda mengatakan itu Tadalah tipe yang mengimplementasikan IA , dan tipe nilai dapat mengimplementasikan sebuah antarmuka . Oleh karena itu kami tidak tahu apakah kovarian akan berhasil, dan kami melarangnya.

Jika Anda ingin kovarians berfungsi, Anda harus memberi tahu compiler bahwa parameter type adalah tipe referensi dengan classbatasan serta IAbatasan antarmuka.

Pesan kesalahan seharusnya mengatakan bahwa konversi tidak mungkin karena kovarians memerlukan jaminan jenis referensi, karena itulah masalah mendasar.

Eric Lippert
sumber
3
Mengapa Anda mengatakan itu salah Anda?
pengguna4951
77
@ user4951: Karena saya menerapkan semua logika pemeriksaan konversi termasuk pesan kesalahan.
Eric Lippert
@BurnsBA Ini hanya "kesalahan" dalam arti kausal - implementasi secara teknis serta pesan kesalahan benar. (Hanya saja pernyataan kesalahan dari tidak dapat diubah dapat menjelaskan alasan sebenarnya. Tetapi menghasilkan kesalahan yang baik dengan obat generik itu sulit - dibandingkan dengan pesan kesalahan template C ++ beberapa tahun yang lalu, ini jelas dan ringkas.)
Peter - Reinstate Monica
3
@ PeterA.Schneider: Saya menghargainya. Tetapi salah satu tujuan utama saya untuk merancang logika pelaporan kesalahan di Roslyn adalah secara khusus untuk menangkap tidak hanya aturan apa yang dilanggar, tetapi lebih jauh lagi, untuk mengidentifikasi "akar penyebab" jika memungkinkan. Misalnya, untuk apa pesan kesalahan itu customers.Select(c=>c.FristName)? Spesifikasi C # sangat jelas bahwa ini adalah kesalahan resolusi kelebihan beban : sekumpulan metode yang berlaku bernama Select yang dapat membuat lambda kosong. Tetapi akar penyebabnya adalah FirstNamekesalahan ketik.
Eric Lippert
3
@ PeterA.Schneider: Saya melakukan banyak pekerjaan untuk memastikan bahwa skenario yang melibatkan inferensi tipe generik dan lambda menggunakan heuristik yang sesuai untuk menyimpulkan pesan kesalahan apa yang mungkin paling membantu pengembang. Tetapi saya melakukan pekerjaan yang jauh kurang baik pada pesan kesalahan konversi, terutama jika menyangkut varians. Saya selalu menyesali itu.
Eric Lippert
26

Saya hanya ingin melengkapi jawaban orang dalam Eric yang luar biasa dengan contoh kode bagi mereka yang mungkin tidak begitu paham dengan batasan umum.

Ubah Somethingtanda tangan seperti ini: classBatasan harus didahulukan .

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA
Marcell Toth
sumber
2
Saya penasaran ... apa sebenarnya alasan di balik pentingnya pemesanan?
Tom Wright
5
@ TomWright - spesifikasi tidak, tentu saja, menyertakan jawaban untuk banyak pertanyaan "Mengapa?" pertanyaan, tetapi dalam kasus ini memperjelas bahwa ada tiga jenis batasan yang berbeda, dan ketika ketiganya digunakan, mereka harus secara khususprimary_constraint ',' secondary_constraints ',' constructor_constraint
Damien_The_Unbeliever
2
@ TomWright: Damien benar; tidak ada alasan khusus yang saya sadari selain kenyamanan penulis parser. Jika saya memiliki druthers, sintaks untuk batasan tipe akan jauh lebih bertele-tele. classburuk karena artinya "tipe referensi", bukan "kelas". Saya akan lebih bahagia dengan sesuatu yang bertele-telewhere T is not struct
Eric Lippert