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:
- menggunakan objek yang dihasilkan untuk tujuan penguncian (ide yang sangat buruk)
- menggunakan semantik persamaan referensi dan mengharapkannya berfungsi untuk dua nilai dalam kotak dari struct yang sama.
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);
}
}
- Aktifkan penggunaan struct sebagai parameter tipe
- selama tidak ada kendala lain seperti
new()
atau class
digunakan.
- Izinkan menghindari tinju pada struct yang digunakan dengan cara ini.
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 :
Jika tipe ini adalah tipe nilai dan tipe ini mengimplementasikan metode maka ptr akan diteruskan tanpa modifikasi sebagai penunjuk 'ini' ke instruksi metode panggilan, untuk implementasi metode dengan tipe ini.
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 implementasi IEquatable<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 mengimplementasikan IEquatable<T>
(di mana T
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 demikian IEquatable
)
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 seperti Add()
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)
;
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
maka Dispose()
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:
IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0
IL_0007: tidak
IL_0008: ldloc.0
IL_0009: callvirt System.Collections.Generic.List.GetEnumerator
IL_000E: stloc.2
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02
IL_0013: panggil System.Collections.Generic.List.get_Current
IL_0018: stloc.1
IL_0019: ldloca.s 02
IL_001B: panggil System.Collections.Generic.List.MoveNext
IL_0020: stloc.3
IL_0021: ldloc.3
IL_0022: brtrue.s IL_0011
IL_0024: tinggalkan IL_0035
IL_0026: ldloca.s 02
IL_0028: dibatasi. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: tidak
IL_0034: akhirnya
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.