Apakah aman bagi struct untuk mengimplementasikan antarmuka?

94

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(); } }
nawfal
sumber

Jawaban:

45

Ada beberapa hal yang terjadi dalam pertanyaan ini ...

Sebuah struct mungkin untuk mengimplementasikan sebuah antarmuka, tetapi ada kekhawatiran yang muncul dengan casting, mutabilitas, dan kinerja. Lihat posting ini untuk lebih jelasnya: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Secara umum, struct harus digunakan untuk objek yang memiliki semantik tipe-nilai. Dengan mengimplementasikan antarmuka pada struct, Anda dapat mengalami masalah tinju karena struct dilemparkan bolak-balik antara struct dan antarmuka. Sebagai hasil dari tinju tersebut, operasi yang mengubah keadaan internal struct mungkin tidak berfungsi dengan baik.

Scott Dorman
sumber
3
"Akibat tinju tersebut, operasi yang mengubah keadaan internal struct mungkin tidak berjalan dengan baik." Beri contoh dan dapatkan jawabannya.
2
@ Will: Tidak yakin apa yang Anda maksud dalam komentar Anda. Posting blog yang saya referensikan memiliki contoh yang menunjukkan di mana memanggil metode antarmuka pada struct tidak benar-benar mengubah nilai internal.
Scott Dorman
12
@ScottDorman: Dalam beberapa kasus, memiliki struktur yang mengimplementasikan antarmuka dapat membantu menghindari tinju. Contoh utamanya adalah IComparable<T>dan IEquatable<T>. Menyimpan struct Foodalam variabel tipe IComparable<Foo>akan membutuhkan tinju, tetapi jika tipe generik Tdibatasi IComparable<T>satu dapat membandingkannya dengan yang lain Ttanpa harus mengotak salah satu, dan tanpa harus tahu apa-apa tentang Tselain itu mengimplementasikan kendala. Perilaku yang menguntungkan seperti itu hanya dimungkinkan oleh kemampuan struct untuk mengimplementasikan antarmuka. Yang telah dikatakan ...
supercat
3
... mungkin akan lebih baik jika ada cara untuk menyatakan bahwa antarmuka tertentu hanya dianggap berlaku untuk struktur yang tidak dikotak, karena ada beberapa konteks di mana tidak mungkin objek kelas atau struktur kotak memiliki yang diinginkan perilaku.
supercat
2
"struct harus digunakan untuk objek yang memiliki semantik tipe nilai. ... operasi yang mengubah keadaan internal struct mungkin tidak berfungsi dengan baik." Bukankah masalah sebenarnya adalah fakta bahwa semantik tipe-nilai dan mutatabilitas tidak bercampur dengan baik?
jpmc26
185

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);
    }
}
  1. Aktifkan penggunaan struct sebagai parameter tipe
    • selama tidak ada kendala lain seperti new()atau classdigunakan.
  2. 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 Tmereka 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)
    ;//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 IDisposablemaka 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.

ShuggyCoUk
sumber
1
Situs egheadcafe.com telah dipindahkan, tetapi tidak berfungsi dengan baik dengan mempertahankan kontennya. Saya mencoba, tetapi tidak dapat menemukan dokumen asli eggheadcafe.com/software/aspnet/31702392/… , kurang pengetahuan tentang OP. (PS +1 untuk ringkasan yang luar biasa).
Abel
2
Ini adalah jawaban yang bagus, tapi saya rasa Anda dapat memperbaikinya dengan memindahkan "Ringkasan" ke atas sebagai "TL; DR". Memberikan kesimpulan terlebih dahulu membantu pembaca mengetahui ke mana arah tujuan Anda.
Hans
Harus ada peringatan compiler saat mentransmisikan a structke interface.
Jalal
8

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 tipe IEquatable<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 memanggil thing1.Compare(thing2)tanpa harus box thing1atau thing2. Jika thing1kebetulan, misalnya, an Int32, run-time akan mengetahui bahwa ketika ia menghasilkan kode untuk CompareTwoThings<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, atau ValueType(sebagai lawan dari lokasi jenisnya sendiri) akan berperilaku sebagai objek kelas. Untuk antarmuka hanya-baca, ini umumnya bukan masalah, tetapi untuk antarmuka yang bermutasi seperti IEnumerator<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 enumerator1untuk membaca elemen pertama. Status pencacah itu akan disalin ke enumerator2. Pernyataan bertanda # 2 akan memajukan salinan itu untuk membaca elemen kedua, tetapi tidak akan mempengaruhi enumerator1. Keadaan pencacah kedua itu kemudian akan disalin ke enumerator3, yang akan dilanjutkan dengan pernyataan bertanda # 3. Kemudian, karena enumerator3dan enumerator4keduanya merupakan tipe referensi, REFERENSI ke enumerator3kemudian akan disalin ke enumerator4, sehingga pernyataan yang ditandai akan secara efektif memajukan keduanya enumerator3 dan enumerator4.

Beberapa orang mencoba untuk berpura-pura bahwa tipe nilai dan tipe referensi adalah keduanya Object, tapi itu tidak benar. Jenis nilai nyata dapat dikonversi Object, tetapi bukan contoh darinya. Sebuah contoh List<String>.Enumeratoryang disimpan di lokasi tipe itu adalah tipe-nilai dan berperilaku sebagai tipe nilai; menyalinnya ke lokasi tipe IEnumerator<String>akan mengubahnya menjadi tipe referensi, dan itu akan berperilaku sebagai tipe referensi . Yang terakhir adalah sejenis Object, tetapi yang pertama tidak.

BTW, beberapa catatan lagi: (1) Secara umum, tipe kelas yang bisa berubah harus memiliki Equalspersamaan referensi pengujian metode mereka , tetapi tidak ada cara yang layak untuk struct kotak untuk melakukannya; (2) meskipun namanya, ValueTypeadalah tipe kelas, bukan tipe nilai; semua jenis berasal dari System.Enumadalah jenis nilai, seperti semua jenis yang berasal dari ValueTypedengan pengecualian System.Enum, namun kedua ValueTypedan System.Enumbeberapa jenis kelas.

supercat
sumber
3

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.

dotnetengineer
sumber
Apakah ini berfungsi seperti ini untuk primitif yang mengimplementasikan antarmuka juga?
aoetalks
3

(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.

Gishu
sumber
Jika seseorang meneruskan Int32ke metode yang menerima tipe generik T:IComparable<Int32>(yang bisa berupa parameter tipe generik dari metode tersebut, atau kelas metode), metode itu akan dapat menggunakan Comparemetode tersebut pada objek yang diteruskan tanpa mengotakkannya.
supercat
0

Tidak ada konsekuensi untuk struct yang mengimplementasikan antarmuka. Misalnya struktur sistem bawaan mengimplementasikan antarmuka seperti IComparabledan IFormattable.

Joseph Daigle
sumber
0

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.

FlySwat
sumber
Seberapa sering Anda melewatkan IComparable sekitar alih-alih implementasi konkret?
FlySwat
Anda tidak perlu mengotak IComparable-atik nilainya. Dengan hanya memanggil metode yang mengharapkan IComparabledengan tipe nilai yang mengimplementasikannya, Anda akan secara implisit mengemas tipe nilai.
Andrew Hare
1
@AndrewHare: Obat generik terbatas memungkinkan metode IComparable<T>untuk dipanggil pada struktur tipe Ttanpa tinju.
supercat
-10

Struktur seperti kelas yang ada di tumpukan. Saya tidak melihat alasan mengapa mereka harus "tidak aman".

Sklivvz
sumber
Kecuali mereka kekurangan warisan.
FlySwat
7
Saya harus tidak setuju dengan setiap bagian dari jawaban ini; mereka tidak selalu tinggal di tumpukan, dan salinan-semantik sangat berbeda dengan kelas.
Marc Gravell
1
Mereka tidak bisa diubah, penggunaan struct yang berlebihan akan membuat ingatan Anda sedih :(
Teoman shipahi
1
@Teomanshipahi Penggunaan instance kelas yang berlebihan akan membuat pengumpul sampah Anda marah.
IllidanS4 mendukung Monica
4
Untuk seseorang yang memiliki 20k + rep, jawaban ini tidak bisa diterima.
Krythic