Konversi Daftar <DerivedClass> ke Daftar <BaseClass>

179

Meskipun kita dapat mewarisi dari kelas dasar / antarmuka, mengapa kita tidak dapat mendeklarasikan List<> menggunakan kelas / antarmuka yang sama?

interface A
{ }

class B : A
{ }

class C : B
{ }

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        List<A> listOfA = new List<C>(); // compiler Error
    }
}

Apakah ada jalan keluar?

Asad
sumber

Jawaban:

230

Cara untuk membuat pekerjaan ini adalah untuk beralih ke daftar dan melemparkan elemen. Ini dapat dilakukan dengan menggunakan ConvertAll:

List<A> listOfA = new List<C>().ConvertAll(x => (A)x);

Anda juga bisa menggunakan Linq:

List<A> listOfA = new List<C>().Cast<A>().ToList();
Mark Byers
sumber
2
pilihan lain: Daftar <A> listOfA = listOfC.ConvertAll (x => (A) x);
ahaliav fox
6
Yang mana yang lebih cepat? ConvertAllatau Cast?
Martin Braun
24
Ini akan membuat salinan daftar. Jika Anda menambahkan atau menghapus sesuatu di daftar baru, ini tidak akan tercermin dalam daftar asli. Dan kedua, ada performa besar dan hukuman memori karena membuat daftar baru dengan objek yang ada. Lihat jawaban saya di bawah ini untuk solusi tanpa masalah ini.
Bigjim
Jawaban untuk modiX: ConvertAll adalah metode pada Daftar <T>, jadi itu hanya akan bekerja pada daftar generik; itu tidak akan berfungsi pada IEnumerable atau IEnumerable <T>; ConvertAll juga dapat melakukan konversi khusus, tidak hanya melakukan casting, misalnya ConvertAll (inci => inci * 25,4). Cast <A> adalah metode ekstensi LINQ, jadi berfungsi pada semua IEnumerable <T> (dan juga berfungsi untuk IEnumerable non-generik), dan seperti sebagian besar LINQ ia menggunakan eksekusi yang ditunda, yaitu, hanya mengkonversi item sebanyak yang ada diambil. Baca lebih lanjut di sini: codeblog.jonskeet.uk/2011/01/13/...
Edward
172

Pertama-tama, berhentilah menggunakan nama-nama kelas yang mustahil untuk dipahami seperti A, B, C. Gunakan Hewan, Mamalia, Jerapah, atau Makanan, Buah, Jeruk atau sesuatu di mana hubungannya jelas.

Pertanyaan Anda kemudian adalah "mengapa saya tidak bisa menetapkan daftar jerapah ke variabel daftar jenis hewan, karena saya bisa menetapkan jerapah ke variabel jenis hewan?"

Jawabannya adalah: seandainya Anda bisa. Lalu apa yang bisa salah?

Nah, Anda dapat menambahkan Macan ke daftar hewan. Misalkan kami mengizinkan Anda untuk meletakkan daftar jerapah dalam variabel yang menyimpan daftar hewan. Kemudian Anda mencoba menambahkan harimau ke daftar itu. Apa yang terjadi? Apakah Anda ingin daftar jerapah mengandung harimau? Apakah Anda ingin crash? atau apakah Anda ingin kompiler melindungi Anda dari crash dengan menjadikan tugas itu ilegal?

Kami memilih yang terakhir.

Konversi semacam ini disebut konversi "kovarian". Dalam C # 4 kami akan memungkinkan Anda untuk membuat konversi kovarian pada antarmuka dan delegasi ketika konversi diketahui selalu aman . Lihat artikel blog saya tentang kovarians dan contravariance untuk detailnya. (Akan ada yang baru pada topik ini pada hari Senin dan Kamis minggu ini.)

Eric Lippert
sumber
3
Apakah ada sesuatu yang tidak aman tentang IList <T> atau ICollection <T> yang mengimplementasikan IList non-generik, tetapi menerapkan memiliki Ilist / ICollection non-generik mengembalikan True untuk IsReadOnly, dan melempar NotSupportedException untuk setiap metode atau properti yang akan mengubahnya?
supercat
33
Sementara jawaban ini mengandung alasan yang cukup dapat diterima itu tidak benar-benar "benar". Jawaban sederhana adalah bahwa C # tidak mendukung ini. Gagasan memiliki daftar hewan yang mengandung jerapah dan harimau sangat valid. Satu-satunya masalah muncul ketika Anda ingin mengakses kelas tingkat yang lebih tinggi. Pada kenyataannya ini tidak berbeda dengan melewatkan kelas induk sebagai parameter ke fungsi dan kemudian mencoba untuk melemparkannya ke kelas saudara yang berbeda. Mungkin ada masalah teknis dengan penerapan pemain tetapi penjelasan di atas tidak memberikan alasan mengapa itu akan menjadi ide yang buruk.
Paul Coldrey
2
@EricLippert Mengapa kita dapat melakukan konversi ini menggunakan IEnumerablebukan List? yaitu: List<Animal> listAnimals = listGiraffes as List<Animal>;tidak mungkin, tetapi IEnumerable<Animal> eAnimals = listGiraffes as IEnumerable<Animal>berfungsi.
LINQ
3
@jbueno: Baca paragraf terakhir dari jawaban saya. Konversi di sana diketahui aman . Mengapa? Karena tidak mungkin mengubah urutan jerapah menjadi urutan hewan dan kemudian memasukkan harimau ke dalam urutan hewan . IEnumerable<T>dan IEnumerator<T>keduanya ditandai sebagai aman untuk kovarians, dan kompiler telah memverifikasi itu.
Eric Lippert
60

Mengutip penjelasan hebat Eric

Apa yang terjadi? Apakah Anda ingin daftar jerapah mengandung harimau? Apakah Anda ingin crash? atau apakah Anda ingin kompiler melindungi Anda dari crash dengan menjadikan tugas itu ilegal? Kami memilih yang terakhir.

Tetapi bagaimana jika Anda ingin memilih untuk crash runtime daripada kesalahan kompilasi? Anda biasanya menggunakan Cast <> atau ConvertAll <> tetapi kemudian Anda akan memiliki 2 masalah: Ini akan membuat salinan daftar. Jika Anda menambahkan atau menghapus sesuatu di daftar baru, ini tidak akan tercermin dalam daftar asli. Dan kedua, ada performa besar dan hukuman memori karena membuat daftar baru dengan objek yang ada.

Saya memiliki masalah yang sama dan karena itu saya membuat kelas pembungkus yang dapat membuat daftar generik tanpa membuat daftar yang sama sekali baru.

Dalam pertanyaan awal, Anda dapat menggunakan:

class Test
{
    static void Main(string[] args)
    {
        A a = new C(); // OK
        IList<A> listOfA = new List<C>().CastList<C,A>(); // now ok!
    }
}

dan di sini kelas wrapper (+ metode extention CastList agar mudah digunakan)

public class CastedList<TTo, TFrom> : IList<TTo>
{
    public IList<TFrom> BaseList;

    public CastedList(IList<TFrom> baseList)
    {
        BaseList = baseList;
    }

    // IEnumerable
    IEnumerator IEnumerable.GetEnumerator() { return BaseList.GetEnumerator(); }

    // IEnumerable<>
    public IEnumerator<TTo> GetEnumerator() { return new CastedEnumerator<TTo, TFrom>(BaseList.GetEnumerator()); }

    // ICollection
    public int Count { get { return BaseList.Count; } }
    public bool IsReadOnly { get { return BaseList.IsReadOnly; } }
    public void Add(TTo item) { BaseList.Add((TFrom)(object)item); }
    public void Clear() { BaseList.Clear(); }
    public bool Contains(TTo item) { return BaseList.Contains((TFrom)(object)item); }
    public void CopyTo(TTo[] array, int arrayIndex) { BaseList.CopyTo((TFrom[])(object)array, arrayIndex); }
    public bool Remove(TTo item) { return BaseList.Remove((TFrom)(object)item); }

    // IList
    public TTo this[int index]
    {
        get { return (TTo)(object)BaseList[index]; }
        set { BaseList[index] = (TFrom)(object)value; }
    }

    public int IndexOf(TTo item) { return BaseList.IndexOf((TFrom)(object)item); }
    public void Insert(int index, TTo item) { BaseList.Insert(index, (TFrom)(object)item); }
    public void RemoveAt(int index) { BaseList.RemoveAt(index); }
}

public class CastedEnumerator<TTo, TFrom> : IEnumerator<TTo>
{
    public IEnumerator<TFrom> BaseEnumerator;

    public CastedEnumerator(IEnumerator<TFrom> baseEnumerator)
    {
        BaseEnumerator = baseEnumerator;
    }

    // IDisposable
    public void Dispose() { BaseEnumerator.Dispose(); }

    // IEnumerator
    object IEnumerator.Current { get { return BaseEnumerator.Current; } }
    public bool MoveNext() { return BaseEnumerator.MoveNext(); }
    public void Reset() { BaseEnumerator.Reset(); }

    // IEnumerator<>
    public TTo Current { get { return (TTo)(object)BaseEnumerator.Current; } }
}

public static class ListExtensions
{
    public static IList<TTo> CastList<TFrom, TTo>(this IList<TFrom> list)
    {
        return new CastedList<TTo, TFrom>(list);
    }
}
Bigjim
sumber
1
Saya baru saja menggunakannya sebagai model tampilan MVC dan mendapatkan tampilan parsial pisau cukur universal yang bagus. Ide yang luar biasa! Saya sangat senang membaca ini.
Stefan Cebulak
5
Saya hanya akan menambahkan "di mana TTo: TFrom" pada deklarasi kelas, sehingga kompiler dapat memperingatkan terhadap penggunaan yang salah. Membuat CastedList dari tipe yang tidak berhubungan adalah tidak masuk akal, dan membuat "CastedList <TBase, TDerived>" tidak akan berguna: Anda tidak dapat menambahkan objek TBase biasa ke dalamnya, dan setiap TDerived yang Anda dapatkan dari Daftar asli sudah dapat digunakan sebagai sebuah TBase.
Wolfzoon
2
@PaulColdrey Alas, enam tahun ke depan adalah penyebab kesalahan.
Wolfzoon
@ Wolfzoon: standar .Cast <T> () juga tidak memiliki batasan ini. Saya ingin membuat perilaku yang sama dengan .Cast sehingga memungkinkan untuk melemparkan daftar Hewan ke daftar Harimau, dan memiliki pengecualian ketika itu akan mengandung Jerapah misalnya. Seperti halnya dengan Cast ...
Bigjim
Hai @Bigjim, saya mengalami masalah dalam memahami cara menggunakan kelas pembungkus Anda, untuk melemparkan array Jerapah yang ada ke dalam array Hewan (kelas dasar). Setiap tips akan dinilai :)
Denis Vitez
28

Jika Anda menggunakan IEnumerablesebagai gantinya, itu akan berfungsi (setidaknya dalam C # 4.0, saya belum mencoba versi sebelumnya). Ini hanya pemain, tentu saja, itu masih akan menjadi daftar.

Dari pada -

List<A> listOfA = new List<C>(); // compiler Error

Dalam kode asli pertanyaan, gunakan -

IEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)

PhistucK
sumber
Bagaimana cara menggunakan IEnumerable dalam kasus itu?
Vladius
1
Alih-alih List<A> listOfA = new List<C>(); // compiler Errordalam kode asli pertanyaan, masukkanIEnumerable<A> listOfA = new List<C>(); // compiler error - no more! :)
PhistucK
Metode mengambil IEnumerable <BaseClass> sebagai parameter akan memungkinkan untuk kelas yang diwariskan untuk diteruskan sebagai Daftar. Jadi ada sangat sedikit yang harus dilakukan selain mengubah tipe parameter.
beauXjames
27

Sejauh mengapa itu tidak berhasil, mungkin akan membantu untuk memahami kovarians dan contravariance .

Hanya untuk menunjukkan mengapa ini tidak berhasil, berikut adalah perubahan pada kode yang Anda berikan:

void DoesThisWork()
{
     List<C> DerivedList = new List<C>();
     List<A> BaseList = DerivedList;
     BaseList.Add(new B());

     C FirstItem = DerivedList.First();
}

Haruskah ini berhasil? Item pertama dalam daftar adalah tipe "B", tetapi tipe item DerivedList adalah C.

Sekarang, asumsikan kita benar-benar hanya ingin membuat fungsi generik yang beroperasi pada daftar beberapa jenis yang mengimplementasikan A, tetapi kami tidak peduli apa jenisnya:

void ThisWorks<T>(List<T> GenericList) where T:A
{

}

void Test()
{
     ThisWorks(new List<B>());
     ThisWorks(new List<C>());
}
Chris Pitman
sumber
"Haruskah ini berhasil?" - Aku akan ya dan tidak. Saya tidak melihat alasan mengapa Anda tidak boleh menulis kode dan gagal pada waktu kompilasi karena sebenarnya melakukan konversi yang tidak valid pada saat Anda mengakses FirstItem sebagai tipe 'C'. Ada banyak cara analog untuk meledakkan C # yang didukung. JIKA Anda benar-benar ingin mencapai fungsi ini untuk alasan yang baik (dan ada banyak) maka jawaban bigjim di bawah ini luar biasa.
Paul Coldrey
16

Anda hanya dapat memilih untuk membaca daftar saja. Sebagai contoh:

IEnumerable<A> enumOfA = new List<C>();//This works
IReadOnlyCollection<A> ro_colOfA = new List<C>();//This works
IReadOnlyList<A> ro_listOfA = new List<C>();//This works

Dan Anda tidak dapat melakukannya untuk daftar yang mendukung elemen hemat. Alasannya adalah:

List<string> listString=new List<string>();
List<object> listObject=(List<object>)listString;//Assume that this is possible
listObject.Add(new object());

Apa sekarang? Ingat bahwa listObject dan listString sebenarnya adalah daftar yang sama, jadi listString sekarang memiliki elemen objek - seharusnya tidak mungkin dan tidak.

Wojciech Mikołajewicz
sumber
1
Yang ini adalah jawaban yang paling bisa dimengerti bagi saya. Khususnya karena menyebutkan bahwa ada sesuatu yang disebut IReadOnlyList, yang akan berfungsi karena menjamin bahwa tidak ada lagi elemen yang akan ditambahkan.
bendtherules
Sayang sekali Anda tidak dapat melakukan hal yang sama untuk IReadOnlyDictionary <TKey, TValue>. Mengapa demikian?
Alastair Maw
Ini saran yang bagus. Saya menggunakan Daftar <> di mana-mana untuk kenyamanan, tetapi sebagian besar waktu saya ingin dibaca hanya. Saran yang bagus karena ini akan meningkatkan kode saya dalam 2 cara dengan mengizinkan para pemain ini dan meningkatkan di mana saya menentukan readonly.
Rick Love
1

Saya pribadi suka membuat lib dengan ekstensi ke kelas

public static List<TTo> Cast<TFrom, TTo>(List<TFrom> fromlist)
  where TFrom : class 
  where TTo : class
{
  return fromlist.ConvertAll(x => x as TTo);
}
vikingfabian
sumber
0

Karena C # tidak mengizinkan jenis itu warisankonversi saat ini .

Sutra Siang
sumber
6
Pertama, ini adalah pertanyaan tentang konversi, bukan warisan. Kedua, kovarians tipe generik tidak akan berfungsi pada tipe kelas, hanya pada tipe antarmuka dan delegasi.
Eric Lippert
5
Yah, aku hampir tidak bisa berdebat denganmu.
Noon Silk
0

Ini adalah perpanjangan dari jawaban brilian BigJim .

Dalam kasus saya, saya memiliki NodeBasekelas dengan Childrenkamus, dan saya membutuhkan cara untuk melakukan O (1) secara umum dari anak-anak. Saya mencoba untuk mengembalikan bidang kamus pribadi di pengambil Children, jadi jelas saya ingin menghindari penyalinan / iterasi yang mahal. Oleh karena itu saya menggunakan kode Bigjim untuk melemparkan Dictionary<whatever specific type>ke generik Dictionary<NodeBase>:

// Abstract parent class
public abstract class NodeBase
{
    public abstract IDictionary<string, NodeBase> Children { get; }
    ...
}

// Implementing child class
public class RealNode : NodeBase
{
    private Dictionary<string, RealNode> containedNodes;

    public override IDictionary<string, NodeBase> Children
    {
        // Using a modification of Bigjim's code to cast the Dictionary:
        return new IDictionary<string, NodeBase>().CastDictionary<string, RealNode, NodeBase>();
    }
    ...
}

Ini bekerja dengan baik. Namun, saya akhirnya mengalami keterbatasan yang tidak terkait dan akhirnya menciptakan FindChild()metode abstrak di kelas dasar yang akan melakukan pencarian. Ternyata ini menghilangkan kebutuhan akan kamus yang sudah dicor. (Saya bisa menggantinya dengan yang sederhana IEnumerableuntuk tujuan saya.)

Jadi pertanyaan yang mungkin Anda tanyakan (terutama jika kinerja adalah masalah yang melarang Anda menggunakan .Cast<>atau .ConvertAll<>) adalah:

"Apakah saya benar-benar perlu membuang seluruh koleksi, atau bisakah saya menggunakan metode abstrak untuk menyimpan pengetahuan khusus yang diperlukan untuk melakukan tugas dan dengan demikian menghindari mengakses koleksi secara langsung?"

Terkadang solusi paling sederhana adalah yang terbaik.

Zach
sumber
0

Anda juga dapat menggunakan System.Runtime.CompilerServices.Unsafepaket NuGet untuk membuat referensi yang sama List:

using System.Runtime.CompilerServices;
...
class Tool { }
class Hammer : Tool { }
...
var hammers = new List<Hammer>();
...
var tools = Unsafe.As<List<Tool>>(hammers);

Dengan contoh di atas, Anda dapat mengakses Hammerinstance yang ada dalam daftar menggunakan toolsvariabel. Menambahkan Toolinstance ke daftar melempar ArrayTypeMismatchExceptionpengecualian karena toolsreferensi variabel yang sama dengan hammers.

Drew
sumber
0

Saya telah membaca seluruh utas ini, dan saya hanya ingin menunjukkan apa yang tampak seperti inkonsistensi bagi saya.

Kompiler mencegah Anda dari melakukan tugas dengan Daftar:

List<Tiger> myTigersList = new List<Tiger>() { new Tiger(), new Tiger(), new Tiger() };
List<Animal> myAnimalsList = myTigersList;    // Compiler error

Tetapi kompiler sangat baik dengan array:

Tiger[] myTigersArray = new Tiger[3] { new Tiger(), new Tiger(), new Tiger() };
Animal[] myAnimalsArray = myTigersArray;    // No problem

Argumen tentang apakah tugas tersebut diketahui aman jatuh terpisah di sini. Tugas yang saya lakukan dengan array tidak aman . Untuk membuktikannya, jika saya tindak lanjuti dengan ini:

myAnimalsArray[1] = new Giraffe();

Saya mendapatkan pengecualian runtime "ArrayTypeMismatchException". Bagaimana seseorang menjelaskan hal ini? Jika kompiler benar-benar ingin mencegah saya melakukan sesuatu yang bodoh, itu seharusnya mencegah saya dari melakukan tugas array.

Rajeev Goel
sumber