Saya tahu bahwa struct di .NET tidak mendukung pewarisan, tetapi tidak begitu jelas mengapa mereka dibatasi dengan cara ini.
Alasan teknis apa yang mencegah struct mewarisi dari struct lain?
Saya tahu bahwa struct di .NET tidak mendukung pewarisan, tetapi tidak begitu jelas mengapa mereka dibatasi dengan cara ini.
Alasan teknis apa yang mencegah struct mewarisi dari struct lain?
Jawaban:
Alasan tipe nilai tidak dapat mendukung pewarisan adalah karena array.
Masalahnya adalah, untuk alasan kinerja dan GC, array tipe nilai disimpan "inline". Misalnya, new FooType[10] {...}
jika FooType
merupakan tipe referensi, 11 objek akan dibuat pada heap terkelola (satu untuk array, dan 10 untuk setiap instance tipe). JikaFooType
merupakan tipe nilai, hanya satu instance yang akan dibuat pada heap terkelola - untuk array itu sendiri (karena setiap nilai array akan disimpan "sebaris" dengan array).
Sekarang, misalkan kita memiliki warisan dengan tipe nilai. Jika digabungkan dengan perilaku array "penyimpanan inline" di atas, Hal-hal Buruk akan terjadi, seperti yang dapat dilihat di C ++ .
Pertimbangkan kode pseudo-C # ini:
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
Dengan aturan konversi normal, a Derived[]
dapat dikonversi menjadi a Base[]
(lebih baik atau lebih buruk), jadi jika Anda s / struct / class / g untuk contoh di atas, itu akan dikompilasi dan berjalan seperti yang diharapkan, tanpa masalah. Tetapi jika Base
danDerived
adalah tipe nilai, dan array menyimpan nilai secara inline, maka kita memiliki masalah.
Kami memiliki masalah karena Square()
tidak tahu apa-apa Derived
, itu hanya akan menggunakan aritmatika pointer untuk mengakses setiap elemen array, bertambah dengan jumlah konstan ( sizeof(A)
). Sidang akan samar-samar seperti:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(Ya, itu adalah rakitan yang menjijikkan, tetapi intinya adalah kita akan menambah melalui array pada konstanta waktu kompilasi yang diketahui, tanpa mengetahui bahwa tipe turunan sedang digunakan.)
Jadi, jika ini benar-benar terjadi, kami akan mengalami masalah kerusakan memori. Secara khusus, di dalam Square()
, sebenarnyavalues[1].A*=2
akan berubah !values[0].B
Coba debug ITU !
Bayangkan struct mendukung warisan. Kemudian menyatakan:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
berarti variabel struct tidak memiliki ukuran tetap, dan itulah mengapa kami memiliki tipe referensi.
Lebih baik lagi, pertimbangkan ini:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
inherit Bar
seharusnya tidak mengizinkan a Foo
untuk ditugaskan ke a Bar
, tetapi mendeklarasikan struct seperti itu dapat memungkinkan beberapa efek yang berguna: (1) Buat anggota dengan nama khusus dari tipe Bar
sebagai item pertama Foo
, dan Foo
sertakan nama anggota yang alias ke anggota tersebut Bar
, mengizinkan kode yang telah digunakan Bar
untuk diadaptasi untuk menggunakan a Foo
, tanpa harus mengganti semua referensi thing.BarMember
dengan thing.theBar.BarMember
, dan mempertahankan kemampuan untuk membaca dan menulis semua Bar
bidang sebagai grup; ...
Struktur tidak menggunakan referensi (kecuali jika diberi kotak, tetapi Anda harus mencoba menghindarinya) sehingga polimorfisme tidak bermakna karena tidak ada tipuan melalui penunjuk referensi. Objek biasanya berada di heap dan direferensikan melalui pointer referensi, tetapi struct dialokasikan pada stack (kecuali jika dimasukkan dalam kotak) atau dialokasikan "di dalam" memori yang ditempati oleh tipe referensi di heap.
Foo
yang memiliki bidang tipe struktur Bar
dapat menganggap Bar
anggotanya sebagai miliknya, sehingga sebuah Point3d
kelas misalnya bisa merangkum a Point2d xy
tetapi merujuk ke X
bidang itu sebagai salah satu xy.X
atau X
.
Inilah yang dikatakan dokumen :
Struktur sangat berguna untuk struktur data kecil yang memiliki semantik nilai. Bilangan kompleks, titik dalam sistem koordinat, atau pasangan nilai kunci dalam kamus adalah contoh yang baik dari struct. Kunci dari struktur data ini adalah bahwa mereka memiliki sedikit anggota data, bahwa mereka tidak memerlukan penggunaan pewarisan atau identitas referensial, dan bahwa mereka dapat dengan mudah diimplementasikan menggunakan semantik nilai di mana tugas menyalin nilai alih-alih referensi.
Pada dasarnya, mereka seharusnya menyimpan data sederhana dan karenanya tidak memiliki "fitur tambahan" seperti warisan. Mungkin secara teknis mungkin bagi mereka untuk mendukung beberapa jenis warisan terbatas (bukan polimorfisme, karena mereka berada di tumpukan), tetapi saya percaya ini juga merupakan pilihan desain untuk tidak mendukung warisan (seperti banyak hal lain di .NET bahasa adalah.)
Di sisi lain, saya setuju dengan manfaat warisan, dan saya pikir kita semua telah mencapai titik di mana kita ingin struct
mewarisi dari orang lain, dan menyadari bahwa itu tidak mungkin. Tetapi pada titik itu, struktur datanya mungkin sangat maju sehingga harus tetap menjadi kelas.
Point3D
dari a Point2D
; Anda tidak akan dapat menggunakan a Point3D
alih - alih a Point2D
, tetapi Anda tidak perlu menerapkan ulang Point3D
seluruhnya dari awal.) Begitulah cara saya menafsirkannya ...
class
pada struct
saat yang tepat.
Class seperti inheritance tidak dimungkinkan, karena struct diletakkan langsung di stack. Struct yang mewarisi akan lebih besar dari induknya, tetapi JIT tidak mengetahuinya, dan mencoba untuk menempatkan terlalu banyak ruang terlalu sedikit. Kedengarannya agak tidak jelas, mari kita tulis contoh:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
Jika ini memungkinkan, itu akan mogok pada cuplikan berikut:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
Ruang dialokasikan untuk ukuran A, bukan untuk ukuran B.
Ada satu hal yang ingin saya perbaiki. Meskipun alasan struct tidak dapat diwariskan adalah karena mereka tinggal di tumpukan adalah yang benar, itu adalah penjelasan yang setengah benar. Structs, seperti jenis nilai lainnya, dapat hidup di tumpukan. Karena itu akan tergantung di mana variabel dideklarasikan, mereka akan tinggal di tumpukan atau di heap . Ini akan terjadi ketika mereka masing-masing adalah variabel lokal atau bidang contoh.
Mengatakan itu, Cecil Memiliki Nama yang tepat.
Saya ingin menekankan hal ini, tipe nilai dapat hidup di tumpukan. Ini tidak berarti mereka selalu melakukannya. Variabel lokal, termasuk parameter metode, akan. Yang lainnya tidak. Namun demikian, itu tetap menjadi alasan mereka tidak dapat diwariskan. :-)
Struktur dialokasikan di tumpukan. Ini berarti nilai semantiknya cukup gratis, dan mengakses anggota struct sangat murah. Ini tidak mencegah polimorfisme.
Anda bisa memulai setiap struct dengan pointer ke tabel fungsi virtualnya. Ini akan menjadi masalah kinerja (setiap struct setidaknya berukuran sebuah pointer), tetapi itu bisa dilakukan. Ini akan memungkinkan fungsi virtual.
Bagaimana dengan menambahkan bidang?
Nah, saat Anda mengalokasikan struct di stack, Anda mengalokasikan sejumlah ruang. Ruang yang diperlukan ditentukan pada waktu kompilasi (baik sebelumnya atau saat JITting). Jika Anda menambahkan bidang dan kemudian menetapkan ke tipe dasar:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Ini akan menimpa beberapa bagian tumpukan yang tidak diketahui.
Alternatifnya adalah runtime untuk mencegah hal ini dengan hanya menulis ukuran byte (A) ke variabel A.
Apa yang terjadi jika B mengganti metode di A dan mereferensikan bidang Integer2? Entah runtime akan menampilkan MemberAccessException, atau metode tersebut mengakses beberapa data acak di stack. Tak satu pun dari ini diizinkan.
Sangat aman untuk memiliki pewarisan struct, selama Anda tidak menggunakan struct secara polimorfis, atau selama Anda tidak menambahkan bidang saat mewarisi. Tapi ini tidak terlalu berguna.
Ini sepertinya pertanyaan yang sangat sering. Saya merasa ingin menambahkan bahwa tipe nilai disimpan "di tempat" di mana Anda mendeklarasikan variabel; Selain detail implementasi, ini berarti tidak ada header objek yang mengatakan sesuatu tentang objek, hanya variabel yang mengetahui jenis data apa yang ada di sana.
Struktur mendukung antarmuka, jadi Anda dapat melakukan beberapa hal polimorfik dengan cara itu.
IL adalah bahasa berbasis tumpukan, jadi memanggil metode dengan argumen berjalan seperti ini:
Ketika metode ini dijalankan, ia mengeluarkan beberapa byte dari tumpukan untuk mendapatkan argumennya. Ia tahu persis berapa banyak byte yang akan muncul karena argumennya adalah penunjuk tipe referensi (selalu 4 byte pada 32-bit) atau itu adalah tipe nilai yang ukurannya selalu diketahui dengan tepat.
Jika ini adalah penunjuk tipe referensi maka metode akan mencari objek di heap dan mendapatkan penangan tipenya, yang menunjuk ke tabel metode yang menangani metode tertentu untuk tipe yang tepat tersebut. Jika ini adalah tipe nilai, maka pencarian ke tabel metode tidak diperlukan karena tipe nilai tidak mendukung pewarisan, jadi hanya ada satu kombinasi metode / tipe yang mungkin.
Jika tipe nilai mendukung pewarisan maka akan ada biaya tambahan dalam tipe tertentu dari struct harus ditempatkan di tumpukan serta nilainya, yang berarti semacam pencarian tabel metode untuk contoh konkret tertentu dari tipe tersebut. Ini akan menghilangkan keunggulan kecepatan dan efisiensi jenis nilai.