Sepertinya saya ingat membaca sesuatu tentang betapa buruknya struct untuk mengimplementasikan antarmuka di CLR melalui C #, tetapi sepertinya saya tidak dapat menemukan apa pun tentang itu. Apa itu buruk? Apakah ada konsekuensi yang tidak diinginkan dari melakukan hal itu?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
IComparable<T>
danIEquatable<T>
. Menyimpan structFoo
dalam variabel tipeIComparable<Foo>
akan membutuhkan tinju, tetapi jika tipe generikT
dibatasiIComparable<T>
satu dapat membandingkannya dengan yang lainT
tanpa harus mengotak salah satu, dan tanpa harus tahu apa-apa tentangT
selain itu mengimplementasikan kendala. Perilaku yang menguntungkan seperti itu hanya dimungkinkan oleh kemampuan struct untuk mengimplementasikan antarmuka. Yang telah dikatakan ...Karena tidak ada orang lain yang secara eksplisit memberikan jawaban ini, saya akan menambahkan yang berikut ini:
Menerapkan antarmuka pada struct tidak memiliki konsekuensi negatif apa pun.
Setiap variabel dari jenis interface yang digunakan untuk mengadakan struct akan menghasilkan nilai kemas struct yang sedang digunakan. Jika struct tidak dapat diubah (hal yang baik) maka ini paling buruk merupakan masalah kinerja kecuali Anda:
Keduanya tidak mungkin terjadi, sebaliknya Anda cenderung melakukan salah satu dari yang berikut:
Generik
Mungkin banyak alasan yang masuk akal untuk struct yang mengimplementasikan antarmuka adalah agar mereka dapat digunakan dalam konteks umum dengan batasan . Ketika digunakan dengan cara ini variabel seperti ini:
class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T> { private readonly T a; public bool Equals(Foo<T> other) { return this.a.Equals(other.a); } }
new()
atauclass
digunakan.Maka this.a BUKAN referensi antarmuka sehingga tidak menyebabkan kotak apa pun yang ditempatkan di dalamnya. Selanjutnya ketika kompilator c # mengkompilasi kelas generik dan perlu memasukkan pemanggilan metode instans yang ditentukan pada instans parameter Tipe T, ia dapat menggunakan opcode yang dibatasi :
Ini menghindari tinju dan karena tipe nilai yang mengimplementasikan antarmuka harus mengimplementasikan metode tersebut, sehingga tinju tidak akan terjadi. Dalam contoh di atas,
Equals()
pemanggilan dilakukan tanpa kotak di this.a 1 .API gesekan rendah
Sebagian besar struct harus memiliki semantik seperti primitif di mana nilai identik bitwise dianggap sama dengan 2 . Runtime akan menyediakan perilaku seperti itu secara implisit
Equals()
tetapi ini bisa lambat. Juga persamaan implisit ini tidak diekspos sebagai implementasiIEquatable<T>
dan dengan demikian mencegah struct digunakan dengan mudah sebagai kunci untuk Kamus kecuali mereka secara eksplisit mengimplementasikannya sendiri. Oleh karena itu, umum bagi banyak tipe struktur publik untuk menyatakan bahwa mereka mengimplementasikanIEquatable<T>
(di manaT
mereka sendiri) untuk membuat ini lebih mudah dan berkinerja lebih baik serta konsisten dengan perilaku dari banyak tipe nilai yang ada dalam CLR BCL.Semua primitif dalam penerapan BCL minimal:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(Dan dengan demikianIEquatable
)Banyak juga yang menerapkan
IFormattable
, lebih lanjut banyak tipe nilai yang ditentukan Sistem seperti DateTime, TimeSpan dan Panduan menerapkan banyak atau semua ini juga. Jika Anda mengimplementasikan jenis yang sama 'berguna secara luas' seperti struct bilangan kompleks atau nilai tekstual lebar tetap, maka mengimplementasikan banyak antarmuka umum ini (dengan benar) akan membuat struct Anda lebih berguna dan dapat digunakan.Pengecualian
Jelas jika antarmuka sangat menyiratkan mutabilitas (seperti
ICollection
) maka mengimplementasikannya adalah ide yang buruk karena itu berarti Anda membuat struct bisa berubah (mengarah ke jenis kesalahan yang sudah dijelaskan di mana modifikasi terjadi pada nilai kotak daripada aslinya ) atau Anda membingungkan pengguna dengan mengabaikan implikasi dari metode sepertiAdd()
atau melempar pengecualian.Banyak antarmuka TIDAK menyiratkan mutabilitas (seperti
IFormattable
) dan berfungsi sebagai cara idiomatik untuk mengekspos fungsionalitas tertentu dengan cara yang konsisten. Seringkali pengguna struct tidak akan peduli dengan overhead tinju untuk perilaku seperti itu.Ringkasan
Jika dilakukan dengan bijaksana, pada tipe nilai yang tidak berubah, implementasi antarmuka yang berguna adalah ide yang bagus
Catatan:
1: Perhatikan bahwa compiler dapat menggunakan ini saat menjalankan metode virtual pada variabel yang diketahui dari tipe struct tertentu tetapi diperlukan untuk memanggil metode virtual. Sebagai contoh:
List<int> l = new List<int>(); foreach(var x in l) ;//no-op
Pencacah yang dikembalikan oleh List adalah sebuah struct, sebuah pengoptimalan untuk menghindari alokasi saat menghitung daftar (Dengan beberapa konsekuensi yang menarik ). Namun semantik foreach menentukan bahwa jika enumerator mengimplementasikan
IDisposable
makaDispose()
akan dipanggil setelah iterasi selesai. Jelas memiliki ini terjadi melalui panggilan kotak akan menghilangkan manfaat dari enumerator menjadi struct (sebenarnya akan lebih buruk). Lebih buruk lagi, jika panggilan buang mengubah status enumerator dengan cara tertentu, maka ini akan terjadi pada instance dalam kotak dan banyak bug halus mungkin diperkenalkan dalam kasus yang kompleks. Oleh karena itu, IL yang dipancarkan dalam situasi seperti ini adalah:Dengan demikian penerapan IDisposable tidak menyebabkan masalah kinerja dan aspek mutable (disesalkan) dari enumerator dipertahankan jika metode Dispose benar-benar melakukan sesuatu!
2: double dan float adalah pengecualian untuk aturan ini di mana nilai NaN tidak dianggap sama.
sumber
struct
keinterface
.Dalam beberapa kasus, mungkin baik untuk sebuah struct untuk mengimplementasikan antarmuka (jika tidak pernah berguna, diragukan pembuat .net akan menyediakannya). Jika sebuah struct mengimplementasikan antarmuka read-only seperti
IEquatable<T>
, menyimpan struct di lokasi penyimpanan (variabel, parameter, elemen array, dll.) Dari tipeIEquatable<T>
akan membutuhkan kotak (setiap tipe struct sebenarnya mendefinisikan dua jenis hal: penyimpanan tipe lokasi yang berperilaku sebagai tipe nilai dan tipe objek heap yang berperilaku sebagai tipe kelas; yang pertama secara implisit dapat diubah menjadi yang kedua - "tinju" - dan yang kedua dapat dikonversi ke yang pertama melalui cast eksplisit-- "unboxing"). Dimungkinkan untuk mengeksploitasi implementasi struktur dari sebuah antarmuka tanpa tinju, bagaimanapun, menggunakan apa yang disebut generik terbatas.Misalnya, jika seseorang memiliki metode
CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
, metode seperti itu dapat memanggilthing1.Compare(thing2)
tanpa harus boxthing1
atauthing2
. Jikathing1
kebetulan, misalnya, anInt32
, run-time akan mengetahui bahwa ketika ia menghasilkan kode untukCompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
. Karena ia akan mengetahui jenis yang tepat dari hal yang menghosting metode dan hal yang diteruskan sebagai parameter, ia tidak perlu mengemas keduanya.Masalah terbesar dengan struct yang mengimplementasikan antarmuka adalah bahwa struct yang disimpan di lokasi dengan tipe antarmuka
Object
, atauValueType
(sebagai lawan dari lokasi jenisnya sendiri) akan berperilaku sebagai objek kelas. Untuk antarmuka hanya-baca, ini umumnya bukan masalah, tetapi untuk antarmuka yang bermutasi sepertiIEnumerator<T>
itu dapat menghasilkan beberapa semantik yang aneh.Perhatikan, misalnya, kode berikut:
List<String> myList = [list containing a bunch of strings] var enumerator1 = myList.GetEnumerator(); // Struct of type List<String>.IEnumerator enumerator1.MoveNext(); // 1 var enumerator2 = enumerator1; enumerator2.MoveNext(); // 2 IEnumerator<string> enumerator3 = enumerator2; enumerator3.MoveNext(); // 3 IEnumerator<string> enumerator4 = enumerator3; enumerator4.MoveNext(); // 4
Pernyataan bertanda # 1 akan menjadi prima
enumerator1
untuk membaca elemen pertama. Status pencacah itu akan disalin keenumerator2
. Pernyataan bertanda # 2 akan memajukan salinan itu untuk membaca elemen kedua, tetapi tidak akan mempengaruhienumerator1
. Keadaan pencacah kedua itu kemudian akan disalin keenumerator3
, yang akan dilanjutkan dengan pernyataan bertanda # 3. Kemudian, karenaenumerator3
danenumerator4
keduanya merupakan tipe referensi, REFERENSI keenumerator3
kemudian akan disalin keenumerator4
, sehingga pernyataan yang ditandai akan secara efektif memajukan keduanyaenumerator3
danenumerator4
.Beberapa orang mencoba untuk berpura-pura bahwa tipe nilai dan tipe referensi adalah keduanya
Object
, tapi itu tidak benar. Jenis nilai nyata dapat dikonversiObject
, tetapi bukan contoh darinya. Sebuah contohList<String>.Enumerator
yang disimpan di lokasi tipe itu adalah tipe-nilai dan berperilaku sebagai tipe nilai; menyalinnya ke lokasi tipeIEnumerator<String>
akan mengubahnya menjadi tipe referensi, dan itu akan berperilaku sebagai tipe referensi . Yang terakhir adalah sejenisObject
, tetapi yang pertama tidak.BTW, beberapa catatan lagi: (1) Secara umum, tipe kelas yang bisa berubah harus memiliki
Equals
persamaan referensi pengujian metode mereka , tetapi tidak ada cara yang layak untuk struct kotak untuk melakukannya; (2) meskipun namanya,ValueType
adalah tipe kelas, bukan tipe nilai; semua jenis berasal dariSystem.Enum
adalah jenis nilai, seperti semua jenis yang berasal dariValueType
dengan pengecualianSystem.Enum
, namun keduaValueType
danSystem.Enum
beberapa jenis kelas.sumber
Struktur diimplementasikan sebagai tipe nilai dan kelas adalah tipe referensi. Jika Anda memiliki variabel bertipe Foo, dan Anda menyimpan sebuah instance dari Fubar di dalamnya, itu akan "mengemasnya" menjadi tipe referensi, sehingga mengalahkan keuntungan menggunakan struct di tempat pertama.
Satu-satunya alasan saya melihat untuk menggunakan struct alih-alih kelas adalah karena itu akan menjadi tipe nilai dan bukan tipe referensi, tetapi struct tidak dapat mewarisi dari kelas. Jika Anda memiliki struct mewarisi sebuah antarmuka, dan Anda melewati antarmuka, Anda kehilangan sifat tipe nilai dari struct. Sebaiknya jadikan saja kelas jika Anda membutuhkan antarmuka.
sumber
(Yah, tidak ada yang penting untuk ditambahkan tetapi belum memiliki kecakapan mengedit jadi begini ..)
Sangat Aman. Tidak ada yang ilegal dengan mengimplementasikan antarmuka pada struct. Bagaimanapun Anda harus mempertanyakan mengapa Anda ingin melakukannya.
Namun mendapatkan referensi antarmuka ke struct akan BOX itu. Jadi penalti performa dan sebagainya.
Satu-satunya skenario valid yang dapat saya pikirkan saat ini diilustrasikan dalam posting saya di sini . Saat Anda ingin mengubah status struct yang disimpan dalam koleksi, Anda harus melakukannya melalui antarmuka tambahan yang terekspos pada struct.
sumber
Int32
ke metode yang menerima tipe generikT:IComparable<Int32>
(yang bisa berupa parameter tipe generik dari metode tersebut, atau kelas metode), metode itu akan dapat menggunakanCompare
metode tersebut pada objek yang diteruskan tanpa mengotakkannya.Saya pikir masalahnya adalah hal itu menyebabkan tinju karena struct adalah tipe nilai sehingga ada sedikit penalti kinerja.
Tautan ini menunjukkan mungkin ada masalah lain dengannya ...
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
sumber
Tidak ada konsekuensi untuk struct yang mengimplementasikan antarmuka. Misalnya struktur sistem bawaan mengimplementasikan antarmuka seperti
IComparable
danIFormattable
.sumber
Ada sangat sedikit alasan bagi tipe nilai untuk mengimplementasikan antarmuka. Karena Anda tidak bisa membuat subkelas tipe nilai, Anda selalu bisa merujuknya sebagai tipe konkretnya.
Kecuali tentu saja, Anda memiliki beberapa struct yang semuanya mengimplementasikan antarmuka yang sama, itu mungkin sedikit berguna, tetapi pada titik itu saya akan merekomendasikan menggunakan kelas dan melakukannya dengan benar.
Tentu saja, dengan mengimplementasikan sebuah antarmuka, Anda mengotak-atik struct, jadi sekarang berada di heap, dan Anda tidak akan dapat meneruskannya dengan nilai lagi ... Ini benar-benar memperkuat pendapat saya bahwa Anda sebaiknya menggunakan kelas saja dalam situasi ini.
sumber
IComparable
-atik nilainya. Dengan hanya memanggil metode yang mengharapkanIComparable
dengan tipe nilai yang mengimplementasikannya, Anda akan secara implisit mengemas tipe nilai.IComparable<T>
untuk dipanggil pada struktur tipeT
tanpa tinju.Struktur seperti kelas yang ada di tumpukan. Saya tidak melihat alasan mengapa mereka harus "tidak aman".
sumber