Apakah praktik yang baik untuk mewarisi dari tipe generik?

35

Apakah lebih baik digunakan List<string>dalam jenis anotasi atau di StringListmanaStringList

class StringList : List<String> { /* no further code!*/ }

Aku berlari ke beberapa dari ini di Ironi .

Ruudjah
sumber
Jika Anda tidak mewarisi dari jenis generik, lalu mengapa mereka ada di sana?
Mawg
7
@ Mawg Mereka hanya tersedia untuk instantiation.
Theodoros Chatzigiannakis
3
Ini adalah salah satu hal favorit saya untuk dilihat dalam kode
Kik
4
Yuck. Tidak, serius. Apa perbedaan semantik antara StringListdan List<string>? Tidak ada, namun Anda (generik "Anda") berhasil menambahkan detail lain ke basis kode yang unik hanya untuk proyek Anda dan tidak berarti apa-apa bagi orang lain sampai setelah mereka mempelajari kode tersebut. Mengapa menyembunyikan nama daftar string hanya agar tetap menyebutnya daftar string? Di sisi lain, jika Anda ingin melakukannya, BagOBagels : List<string>saya pikir itu lebih masuk akal. Anda dapat mengulanginya sebagai IEnumerable, konsumen eksternal tidak peduli dengan implementasinya - baiklah.
Craig
1
XAML memiliki masalah di mana Anda tidak dapat menggunakan obat generik dan Anda perlu membuat jenis seperti ini untuk mengisi daftar
Daniel Little

Jawaban:

27

Ada alasan lain mengapa Anda mungkin ingin mewarisi dari tipe generik.

Microsoft merekomendasikan untuk menghindari bertelur tipe generik dalam tanda tangan metode .

Maka itu ide yang bagus, untuk membuat jenis-jenis nama bisnis-domain untuk beberapa obat generik Anda. Alih-alih memiliki IEnumerable<IDictionary<string, MyClass>>, buat tipe MyDictionary : IDictionary<string, MyClass>dan kemudian anggota Anda menjadi IEnumerable<MyDictionary>, yang jauh lebih mudah dibaca. Ini juga memungkinkan Anda untuk menggunakan MyDictionary di sejumlah tempat, meningkatkan kejelasan kode Anda.

Ini mungkin kedengarannya bukan manfaat besar, tetapi dalam kode bisnis dunia nyata saya telah melihat obat generik yang akan membuat rambut Anda berdiri. Hal-hal seperti List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. Arti generik itu sama sekali tidak jelas. Namun, jika dibangun dengan serangkaian jenis yang dibuat secara eksplisit, maksud pengembang asli kemungkinan akan jauh lebih jelas. Lebih jauh lagi, jika tipe generik itu perlu diubah karena suatu alasan, menemukan dan mengganti semua instance dari tipe generik itu mungkin benar-benar menyakitkan, dibandingkan dengan mengubahnya di kelas turunan.

Akhirnya, ada alasan lain mengapa seseorang mungkin ingin membuat tipenya sendiri yang berasal dari obat generik. Pertimbangkan kelas-kelas berikut:

public class MyClass
{
    public ICollection<MyItems> MyItems { get; private set; }
    // plumbing code here
}

public class MyOtherClass
{
    public ICollection<MyItems> MyItemCache { get; private set; }
    // plumbing code here
}

public class MyConsumerClass
{
    public MyConsumerClass(ICollection<MyItems> myItems)
    {
        // use the collection
    }
}

Sekarang bagaimana kita tahu mana yang ICollection<MyItems>harus dimasukkan ke konstruktor untuk MyConsumerClass? Jika kita membuat kelas turunan class MyItems : ICollection<MyItems>dan class MyItemCache : ICollection<MyItems>, MyConsumerClass kemudian bisa sangat spesifik tentang yang mana dari dua koleksi yang sebenarnya diinginkan. Ini kemudian bekerja dengan sangat baik dengan wadah IoC, yang dapat menyelesaikan dua koleksi dengan sangat mudah. Ini juga memungkinkan programmer untuk lebih akurat - jika masuk akal untuk MyConsumerClassbekerja dengan ICollection apa pun, mereka dapat menggunakan konstruktor generik. Namun, jika tidak masuk akal secara bisnis untuk menggunakan salah satu dari dua jenis turunan, pengembang dapat membatasi parameter konstruktor ke jenis tertentu.

Singkatnya, seringkali bermanfaat untuk mendapatkan tipe dari tipe generik - memungkinkan kode yang lebih eksplisit jika sesuai dan membantu keterbacaan dan pemeliharaan.

Stephen
sumber
12
Itu rekomendasi Microsoft yang benar-benar konyol.
Thomas Eding
7
Saya tidak berpikir itu konyol untuk API publik. Sekilas nested generic sulit untuk dipahami dan nama tipe yang lebih bermakna seringkali dapat membantu keterbacaan.
Stephen
4
Saya tidak berpikir itu konyol, dan tentu saja oke untuk API pribadi ... tetapi untuk API publik saya tidak berpikir itu ide yang bagus. Bahkan Microsoft merekomendasikan untuk menerima dan mengembalikan tipe paling dasar saat Anda menulis API yang dimaksudkan untuk konsumsi publik.
Paul d'Aoust
35

Pada prinsipnya tidak ada perbedaan antara mewarisi dari tipe generik dan mewarisi dari yang lain.

Namun, mengapa Anda akan berasal dari kelas mana saja dan tidak menambahkan apa pun lebih lanjut?

Julia Hayward
sumber
17
Saya setuju. Tanpa memberikan kontribusi apa-apa, aku akan membaca "StringList" dan berpikir sendiri, "Apa yang benar-benar lakukan?"
Neil
1
Saya setuju juga, dalam bahasa mei untuk mewarisi Anda menggunakan kata kunci "extends", ini adalah pengingat yang baik bahwa jika Anda akan mewarisi kelas maka Anda harus memperluasnya dengan fungsionalitas lebih lanjut.
Elliot Blackburn
14
-1, karena tidak memahami bahwa ini hanya satu cara untuk membuat abstraksi dengan memilih nama yang berbeda - membuat abstraksi bisa menjadi alasan yang sangat baik untuk tidak menambahkan apa pun ke kelas ini.
Doc Brown
1
Pertimbangkan jawaban saya, saya membahas beberapa alasan seseorang memutuskan untuk melakukan ini.
NPSF3000
1
"Kenapa kamu bisa berasal dari kelas mana saja dan tidak menambahkan apa-apa lagi?" Saya sudah melakukannya untuk kontrol pengguna. Anda bisa membuat kontrol pengguna umum tetapi Anda tidak bisa menggunakannya di perancang formulir windows, jadi Anda harus mewarisinya, menentukan jenisnya, dan menggunakannya.
George T
13

Saya tidak tahu C # dengan sangat baik, tetapi saya percaya ini adalah satu-satunya cara untuk mengimplementasikan typedef, yang biasanya digunakan oleh para programmer C ++ karena beberapa alasan:

  • Itu membuat kode Anda tidak terlihat seperti kurung sudut.
  • Ini memungkinkan Anda menukar wadah yang berbeda jika kebutuhan Anda berubah nanti, dan menjelaskan bahwa Anda berhak melakukannya.
  • Ini memungkinkan Anda memberi jenis nama yang lebih relevan dalam konteks.

Mereka pada dasarnya membuang keuntungan terakhir dengan memilih nama generik yang membosankan, tetapi dua alasan lainnya masih bertahan.

Karl Bielefeldt
sumber
7
Eh ... Mereka tidak persis sama. Di C ++ Anda memiliki alias literal. StringList adalah sebuah List<string>- mereka dapat digunakan secara bergantian. Ini penting ketika bekerja dengan API lain (atau ketika mengekspos API Anda), karena semua orang di dunia menggunakannya List<string>.
Telastyn
2
Saya menyadari mereka tidak persis sama. Sedekat tersedia. Tentu saja ada kelemahan pada versi yang lebih lemah.
Karl Bielefeldt
24
Tidak, using StringList = List<string>;adalah setara C # terdekat dari typedef. Subklasifikasi seperti ini akan mencegah penggunaan konstruktor dengan argumen.
Pete Kirkham
4
@PeteKirkham: mendefinisikan StringList dengan usingmembatasi ke file kode sumber di mana usingditempatkan. Dari sudut pandang ini, pewarisan lebih dekat typedef. Dan membuat subclass tidak mencegah seseorang menambahkan semua konstruktor yang dibutuhkan (meskipun mungkin membosankan).
Doc Brown
2
@ Random832: izinkan saya mengungkapkan ini secara berbeda: dalam C ++, sebuah typedef dapat digunakan untuk menetapkan nama di satu tempat , untuk menjadikannya "sumber kebenaran tunggal". Dalam C #, usingtidak dapat digunakan untuk tujuan ini, tetapi pewarisan bisa. Ini menunjukkan mengapa saya tidak setuju dengan komentar terbalik Pete Kirkham - saya tidak menganggap usingsebagai alternatif nyata untuk C ++ typedef.
Doc Brown
9

Saya dapat memikirkan setidaknya satu alasan praktis.

Mendeklarasikan tipe non-generik yang mewarisi dari tipe generik (bahkan tanpa menambahkan apa pun), memungkinkan tipe tersebut untuk dirujuk dari tempat-tempat di mana mereka tidak akan tersedia, atau tersedia dengan cara yang lebih berbelit-belit daripada seharusnya.

Salah satu kasusnya adalah ketika menambahkan pengaturan melalui editor Pengaturan di Visual Studio, di mana hanya jenis non-generik yang tampaknya tersedia. Jika Anda pergi ke sana, Anda akan melihat bahwa semua System.Collectionskelas tersedia, tetapi tidak ada koleksi dari yang System.Collections.Genericberlaku.

Kasus lain adalah ketika instantiating jenis melalui XAML di WPF. Tidak ada dukungan untuk melewati argumen tipe dalam sintaks XML saat menggunakan skema pra-2009. Ini diperburuk oleh kenyataan bahwa skema 2006 tampaknya menjadi default untuk proyek WPF baru.

Theodoros Chatzigiannakis
sumber
1
Dalam antarmuka layanan web WCF Anda dapat menggunakan teknik ini untuk memberikan nama jenis koleksi yang tampak lebih baik (mis. PersonCollection alih-alih ArrayOfPerson atau apa pun)
Rico Suter
7

Saya pikir Anda perlu melihat beberapa sejarah .

  • C # telah digunakan lebih lama daripada tipe generiknya.
  • Saya berharap bahwa perangkat lunak yang diberikan memiliki StringList sendiri di beberapa titik dalam sejarah.
  • Ini mungkin telah diimplementasikan dengan membungkus ArrayList.
  • Kemudian seseorang untuk refactoring untuk memajukan basis kode.
  • Tetapi tidak ingin menyentuh 1001 file yang berbeda, jadi selesaikan pekerjaan itu.

Kalau tidak, Theodoros Chatzigiannakis punya jawaban terbaik, misalnya masih ada banyak alat yang tidak mengerti tipe generik. Atau mungkin bahkan campuran jawaban dan sejarahnya.

Ian
sumber
3
"C # telah digunakan lebih lama daripada tipe generiknya." Tidak juga, C # tanpa obat generik ada selama sekitar 4 tahun (Januari 2002 - November 2005), sementara C # dengan obat generik sekarang berusia 9 tahun.
svick
4

Dalam beberapa kasus tidak mungkin untuk menggunakan tipe generik, misalnya Anda tidak dapat referensi tipe generik di XAML. Dalam kasus ini, Anda dapat membuat tipe non-generik yang mewarisi dari tipe generik yang awalnya ingin Anda gunakan.

Jika memungkinkan, saya akan menghindarinya.

Tidak seperti typedef, Anda membuat tipe baru. Jika Anda menggunakan StringList sebagai parameter input ke suatu metode, penelepon Anda tidak dapat mengirimkan a List<string>.

Juga, Anda perlu menyalin secara eksplisit semua konstruktor yang ingin Anda gunakan, dalam contoh StringList yang tidak dapat Anda katakan new StringList(new[]{"a", "b", "c"}), meskipun List<T>mendefinisikan konstruktor seperti itu.

Edit:

Jawaban yang diterima berfokus pada pro ketika menggunakan jenis turunan di dalam model domain Anda. Saya setuju bahwa mereka berpotensi berguna dalam kasus ini, masih, saya pribadi akan menghindarinya bahkan di sana.

Tetapi Anda harus (menurut saya) tidak pernah menggunakannya dalam API publik karena Anda membuat hidup pemanggil Anda lebih sulit atau menyia-nyiakan kinerja (saya juga tidak terlalu suka konversi tersirat pada umumnya).

Lukas Rieger
sumber
3
Anda berkata, " Anda tidak dapat merujuk tipe generik di XAML ", tapi saya tidak yakin apa yang Anda maksud. Lihat Generik dalam XAML
pswg
1
Itu dimaksudkan sebagai contoh. Ya, itu mungkin dengan Skema XAML 2009, tetapi AFAIK tidak dengan Skema 2006, yang masih merupakan standar VS.
Lukas Rieger
1
Paragraf ketiga Anda hanya setengah benar, Anda benar-benar dapat mengizinkan ini dengan konverter implisit;)
Knerd
1
@Knerd, benar, tetapi kemudian Anda 'secara implisit' membuat objek baru. Kecuali jika Anda melakukan lebih banyak pekerjaan, ini juga akan membuat salinan data. Mungkin tidak masalah dengan daftar kecil, tetapi bersenang-senang, jika seseorang melewati daftar dengan beberapa ribu elemen =)
Lukas Rieger
@LukasRieger Anda benar: PI hanya ingin menunjukkan bahwa: P
Knerd
2

Untuk apa nilainya, inilah contoh bagaimana mewarisi dari kelas generik digunakan dalam kompiler Roslyn Microsoft, dan bahkan tanpa mengubah nama kelas. (Saya sangat bingung dengan ini sehingga saya berakhir di sini dalam pencarian saya untuk melihat apakah ini benar-benar mungkin.)

Dalam ProjectAnalysis proyek Anda dapat menemukan definisi ini:

/// <summary>
/// Common base class for C# and VB PE module builder.
/// </summary>
internal abstract class PEModuleBuilder<TCompilation, TSourceModuleSymbol, TAssemblySymbol, TTypeSymbol, TNamedTypeSymbol, TMethodSymbol, TSyntaxNode, TEmbeddedTypesManager, TModuleCompilationState> : CommonPEModuleBuilder, ITokenDeferral
    where TCompilation : Compilation
    where TSourceModuleSymbol : class, IModuleSymbol
    where TAssemblySymbol : class, IAssemblySymbol
    where TTypeSymbol : class
    where TNamedTypeSymbol : class, TTypeSymbol, Cci.INamespaceTypeDefinition
    where TMethodSymbol : class, Cci.IMethodDefinition
    where TSyntaxNode : SyntaxNode
    where TEmbeddedTypesManager : CommonEmbeddedTypesManager
    where TModuleCompilationState : ModuleCompilationState<TNamedTypeSymbol, TMethodSymbol>
{
  ...
}

Kemudian dalam analisis proyek CSharpCode ada definisi ini:

internal abstract class PEModuleBuilder : PEModuleBuilder<CSharpCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState>
{
  ...
}

Kelas PEModuleBuilder non-generik ini digunakan secara luas dalam proyek analisis CSharpCode, dan beberapa kelas dalam proyek itu mewarisi darinya, secara langsung atau tidak langsung.

Dan kemudian dalam proyek BasicCodeanalysis ada definisi ini:

Partial Friend MustInherit Class PEModuleBuilder
    Inherits PEModuleBuilder(Of VisualBasicCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState)

Karena kita dapat (semoga) berasumsi bahwa Roslyn ditulis oleh orang-orang dengan pengetahuan luas tentang C # dan bagaimana seharusnya digunakan, saya berpikir bahwa ini adalah rekomendasi dari teknik ini.

RenniePet
sumber
Iya nih. Dan juga mereka dapat digunakan untuk memperbaiki klausa mana secara langsung ketika dideklarasikan.
Sentinel
1

Ada tiga kemungkinan alasan mengapa seseorang mencoba melakukan itu di luar kepala saya:

1) Untuk menyederhanakan deklarasi:

Tentu StringListdan ListStringkira-kira sama panjangnya tetapi bayangkan Anda sedang bekerja dengan UserCollectionyang sebenarnya Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>atau contoh generik besar lainnya.

2) Untuk membantu AOT:

Terkadang Anda perlu menggunakan kompilasi Ahead Of Time, bukan Just In Time Compilation - mis. Pengembangan untuk iOS. Kadang-kadang kompiler AOT mengalami kesulitan mencari tahu semua jenis generik sebelumnya, dan ini bisa menjadi upaya untuk memberikannya petunjuk.

3) Untuk menambah fungsionalitas atau menghapus ketergantungan:

Mungkin mereka memiliki fungsi spesifik yang ingin mereka terapkan StringList(bisa disembunyikan dalam metode ekstensi, belum ditambahkan, atau historis) yang mereka tidak dapat tambahkan List<string>tanpa mewarisi darinya, atau bahwa mereka tidak ingin mencemari List<string>dengan .

Atau mereka mungkin ingin mengubah implementasi dari StringListjalur, jadi baru saja menggunakan kelas sebagai penanda untuk saat ini (biasanya Anda akan membuat / menggunakan antarmuka untuk ini misalnya IList).


Saya juga mencatat bahwa Anda telah menemukan kode ini di Irony, yang merupakan parser bahasa - jenis aplikasi yang tidak biasa. Mungkin ada beberapa alasan spesifik lainnya yang digunakan.

Contoh-contoh ini menunjukkan bahwa mungkin ada alasan yang sah untuk menulis pernyataan seperti itu - bahkan jika itu tidak terlalu umum. Seperti apa pun, pertimbangkan beberapa opsi dan pilih yang terbaik untuk Anda saat itu.


Jika Anda melihat kode sumbernya , tampaknya opsi 3 adalah alasannya - mereka menggunakan teknik ini untuk membantu menambah koleksi fungsionalitas / spesialisasi:

public class StringList : List<string> {
    public StringList() { }
    public StringList(params string[] args) {
      AddRange(args);
    }
    public override string ToString() {
      return ToString(" ");
    }
    public string ToString(string separator) {
      return Strings.JoinStrings(separator, this);
    }
    //Used in sorting suffixes and prefixes; longer strings must come first in sort order
    public static int LongerFirst(string x, string y) {
      try {//in case any of them is null
        if (x.Length > y.Length) return -1;
      } catch { }
      if (x == y) return 0;
      return 1; 
    }
NPSF3000
sumber
1
Kadang-kadang pencarian untuk menyederhanakan deklarasi (atau menyederhanakan apa pun) dapat menghasilkan peningkatan kompleksitas, bukan pengurangan kompleksitas. Setiap orang yang tahu bahasa cukup baik tahu apa List<T>itu, tetapi mereka harus memeriksa StringListdalam konteks untuk benar-benar memahami apa itu . Pada kenyataannya, hanya saja List<string>, menggunakan nama yang berbeda. Tampaknya tidak ada keuntungan semantik untuk melakukan ini dalam kasus yang begitu sederhana. Anda benar-benar hanya mengganti tipe yang terkenal dengan tipe yang sama dengan nama yang berbeda.
Craig
@Craig Saya setuju, sebagaimana dicatat oleh penjelasan saya tentang usecase (pernyataan yang lebih panjang) dan kemungkinan alasan lainnya.
NPSF3000
-1

Menurut saya itu bukan praktik yang baik.

List<string>mengkomunikasikan "daftar string umum". Jelas List<string> metode ekstensi berlaku. Diperlukan lebih sedikit kompleksitas untuk memahami; hanya Daftar yang dibutuhkan.

StringListmengkomunikasikan "kebutuhan akan kelas yang terpisah". Mungkin List<string> metode ekstensi berlaku (periksa implementasi tipe aktual untuk ini). Lebih banyak kompleksitas diperlukan untuk memahami kode; Daftar dan pengetahuan implementasi kelas turunan diperlukan.

Ruudjah
sumber
4
Metode ekstensi tetap berlaku.
Theodoros Chatzigiannakis
2
Tentu saja. Tetapi Anda tidak tahu bahwa sampai Anda memeriksa StringList dan melihatnya berasal List<string>.
Ruudjah
@TheodorosChatzigiannakis Saya bertanya-tanya apakah Ruudjah dapat menguraikan apa yang ia maksud dengan "metode penyuluhan tidak berlaku." Metode ekstensi adalah suatu kemungkinan dengan kelas apa pun . Tentu, pustaka .NET framework dikirimkan bersama banyak metode ekstensi yang dengan gratis disebut LINQ yang berlaku untuk kelas kumpulan generik, tetapi itu tidak berarti metode ekstensi tidak berlaku untuk kelas lain? Mungkin Ruudjah membuat poin yang tidak jelas pada pandangan pertama? ;-)
Craig
"Metode ekstensi tidak berlaku." Di mana Anda membaca ini? Saya mengatakan "metode ekstensi mungkin berlaku.
Ruudjah
1
Implementasi tipe tidak akan memberi tahu Anda apakah metode ekstensi berlaku atau tidak, bukan? Hanya fakta bahwa itu adalah ekstensi sarana kelas metode yang berlaku. Setiap konsumen tipe Anda dapat membuat metode ekstensi sepenuhnya terlepas dari kode Anda. Saya kira itulah yang saya maksudkan atau tanyakan. Apakah Anda hanya berbicara tentang LINQ? LINQ diimplementasikan menggunakan metode ekstensi. Tetapi tidak adanya metode ekstensi khusus LINQ untuk tipe tertentu tidak berarti metode ekstensi untuk tipe itu tidak ada, atau tidak akan ada. Mereka dapat dibuat kapan saja oleh siapa saja.
Craig