Apa alasan menggunakan antarmuka versus tipe yang dibatasi secara umum

15

Dalam bahasa berorientasi objek yang mendukung parameter tipe umum (juga dikenal sebagai templat kelas, dan polimorfisme parametrik, meskipun tentu saja setiap nama memiliki konotasi yang berbeda), sering kali mungkin untuk menentukan batasan tipe pada parameter tipe, sehingga diturunkan. dari tipe lain. Sebagai contoh, ini adalah sintaks dalam C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

Apa alasan untuk menggunakan tipe antarmuka aktual dari tipe yang dibatasi oleh antarmuka tersebut? Misalnya, apa alasan untuk membuat metode tanda tangan I2 ExampleMethod(I2 value)?

GregRos
sumber
4
templat kelas (C ++) adalah sesuatu yang sama sekali berbeda dan jauh lebih kuat daripada generik sangat sedikit. Meskipun bahasa memiliki generik meminjam sintaks template untuk mereka.
Deduplicator
Metode antarmuka adalah panggilan tidak langsung, sedangkan metode tipe dapat berupa panggilan langsung. Jadi yang terakhir bisa lebih cepat dari yang sebelumnya, dan dalam kasus refparameter tipe nilai, mungkin sebenarnya memodifikasi tipe nilai.
user541686
@Dupuplikator: Menimbang bahwa generik lebih tua dari templat, saya gagal melihat bagaimana generik dapat meminjam apa pun dari templat sama sekali, sintaksis atau lainnya.
Jörg W Mittag
3
@ JörgWMittag: Saya menduga bahwa dengan "bahasa berorientasi objek yang mendukung generik", Deduplicator mungkin lebih memahami "Java dan C #" daripada "ML dan Ada". Maka pengaruh dari C ++ pada yang pertama jelas, meskipun tidak semua bahasa memiliki generik atau parametrik polimorfisme yang dipinjam dari C ++.
Steve Jessop
2
@SteveJessop: ML, Ada, Eiffel, Haskell mendahului templat C ++, Scala, F #, OCaml datang setelahnya, dan tidak ada yang berbagi sintaksis C ++. (Menariknya, bahkan D, yang banyak meminjam dari C ++, terutama templat, tidak membagikan sintaksis C ++.) "Java dan C #" adalah pandangan yang agak sempit tentang "bahasa yang memiliki generik", saya pikir.
Jörg W Mittag

Jawaban:

21

Menggunakan versi parametrik memberi

  1. Informasi lebih lanjut kepada pengguna fungsi
  2. Batasi jumlah program yang dapat Anda tulis (pemeriksaan bug gratis)

Sebagai contoh acak, anggaplah kita memiliki metode yang menghitung akar persamaan kuadrat

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

Dan kemudian Anda ingin itu bekerja pada jenis lain seperti hal-hal selain int. Anda dapat menulis sesuatu seperti

Num solve(Num a, Num b, Num c){
  ...
}

Masalahnya adalah ini tidak mengatakan apa yang Anda inginkan. Ia mengatakan

Beri saya 3 hal yang angka seperti (tidak harus dengan cara yang sama) dan saya akan memberi Anda semacam nomor

Kita tidak bisa melakukan sesuatu seperti int sol = solve(a, b, c)jika a, b, dan cadalah ints karena kita tidak tahu bahwa metode ini akan mengembalikan intpada akhirnya! Ini mengarah pada beberapa tarian canggung dengan downcasting dan berdoa jika kita ingin menggunakan solusi dalam ekspresi yang lebih besar.

Di dalam fungsi, seseorang mungkin memberi kita pelampung, bigint, dan derajat dan kita harus menambahkan dan melipatgandakannya bersama-sama. Kami ingin secara statis menolak ini karena operasi antara 3 kelas ini akan menjadi omong kosong. Derajatnya mod 360 sehingga tidak akan terjadi a.plus(b) = b.plus(a)dan keriuhan serupa akan muncul.

Jika kita menggunakan parametrik polimorfisme dengan subtyping, kita dapat mengesampingkan semua ini karena tipe kita sebenarnya mengatakan apa yang kita maksud

<T : Num> T solve(T a, T b, T c)

Atau dengan kata-kata "Jika Anda memberi saya beberapa jenis yang merupakan angka, saya dapat menyelesaikan persamaan dengan koefisien-koefisien itu".

Ini muncul di banyak tempat lain juga. Sumber lain baik contoh adalah fungsi yang abstrak lebih semacam wadah, ala reverse, sort, map, dll

Daniel Gratzer
sumber
8
Singkatnya, versi generik menjamin bahwa ketiga input (dan output) akan menjadi nomor yang sama .
MathematicalOrchid
Namun, ini gagal ketika Anda tidak mengontrol jenis yang dimaksud (dan karenanya tidak dapat menambahkan antarmuka ke dalamnya). Untuk generalitas maksimum, Anda harus menerima antarmuka yang ditentukan oleh tipe argumen (misalnya Num<int>) sebagai argumen tambahan. Anda selalu dapat mengimplementasikan antarmuka untuk semua jenis melalui delegasi. Ini pada dasarnya adalah jenis kelas Haskell, kecuali jauh lebih membosankan untuk digunakan karena Anda harus secara eksplisit melewati antarmuka.
Doval
16

Apa alasan untuk menggunakan tipe antarmuka aktual dari tipe yang dibatasi oleh antarmuka tersebut?

Karena itulah yang kamu butuhkan ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

dua tanda tangan yang jelas berbeda. Yang pertama mengambil semua jenis yang mengimplementasikan antarmuka dan satu-satunya jaminan yang dibuat adalah bahwa nilai kembali memenuhi antarmuka.

Yang kedua mengambil semua jenis yang mengimplementasikan antarmuka dan menjamin bahwa itu akan mengembalikan setidaknya jenis itu lagi (daripada sesuatu yang memenuhi antarmuka yang kurang ketat).

Terkadang, Anda menginginkan jaminan yang lebih lemah. Terkadang Anda menginginkan yang lebih kuat.

Telastyn
sumber
Bisakah Anda memberi contoh bagaimana Anda akan menggunakan versi jaminan yang lebih lemah?
GregRos
4
@GregRos - Misalnya, dalam beberapa kode parser yang saya tulis. Saya punya fungsi Oryang mengambil dua Parserobjek (kelas dasar abstrak, tetapi prinsipnya berlaku) dan mengembalikan yang baru Parser(tetapi dengan jenis yang berbeda). Pengguna akhir tidak boleh tahu atau peduli apa jenis betonnya.
Telastyn
Dalam C # saya membayangkan mengembalikan T selain yang dilewatkan hampir tidak mungkin (tanpa rasa sakit refleksi) tanpa kendala baru juga membuat jaminan kuat Anda tidak berguna dengan sendirinya.
NtscCobalt
1
@NtscCobalt: Ini lebih berguna ketika Anda menggabungkan pemrograman generik parametrik dan antarmuka. Misalnya apa yang dilakukan LINQ setiap saat (menerima IEnumerable<T>, mengembalikan yang lain, IEnumerable<T>misalnya, sebenarnya OrderedEnumerable<T>)
Ben Voigt
2

Penggunaan generik terbatas untuk parameter metode dapat memungkinkan metode untuk kembali jenisnya berdasarkan hal yang dilewatkan. Dalam. NET mereka dapat memiliki keuntungan tambahan juga. Diantara mereka:

  1. Suatu metode yang menerima generik terbatas sebagai parameter refatau outdapat melewati variabel yang memenuhi kendala; sebaliknya, metode non-generik dengan parameter tipe antarmuka akan terbatas pada menerima variabel dari tipe antarmuka yang tepat.

  2. Metode dengan tipe T parameter generik dapat menerima koleksi generik dari T. Metode yang menerima IList<T> where T:IAnimalakan dapat menerima List<SiameseCat>, tetapi metode yang ingin dan IList<Animal>tidak akan dapat melakukannya.

  3. Kendala terkadang dapat menentukan antarmuka dalam hal tipe generik, misalnya where T:IComparable<T>.

  4. Struktur yang mengimplementasikan antarmuka dapat disimpan sebagai tipe nilai ketika diteruskan ke metode yang menerima parameter generik terbatas, tetapi harus dikotakkan bila dilewatkan sebagai tipe antarmuka. Ini dapat memiliki efek besar pada kecepatan.

  5. Parameter generik dapat memiliki beberapa kendala, sementara tidak ada cara lain untuk menentukan parameter "beberapa tipe yang mengimplementasikan IFoo dan IBar". Kadang-kadang ini bisa menjadi pedang bermata dua, karena kode yang telah menerima parameter tipe IFooakan merasa sangat sulit untuk meneruskannya ke metode seperti itu yang mengharapkan generik berlipat ganda, bahkan jika instance yang dipermasalahkan akan memenuhi semua kendala.

Jika dalam situasi tertentu tidak akan ada keuntungan menggunakan generik, maka cukup menerima parameter dari jenis antarmuka. Penggunaan generik akan memaksa sistem tipe dan JITter untuk melakukan pekerjaan ekstra, jadi jika tidak ada manfaatnya, orang tidak seharusnya melakukannya. Di sisi lain, sangat umum bahwa setidaknya satu dari keuntungan di atas akan berlaku.

supercat
sumber