Mengapa struct tidak mendukung pewarisan?

128

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?

Juliet
sumber
6
Saya tidak sekarat untuk fungsi ini, tetapi saya dapat memikirkan beberapa kasus ketika pewarisan struct akan berguna: Anda mungkin ingin memperluas struct Point2D ke struct Point3D dengan warisan, Anda mungkin ingin mewarisi dari Int32 untuk membatasi nilainya antara 1 dan 100, Anda mungkin ingin membuat tipe-def yang terlihat di banyak file (trik Menggunakan typeA = typeB hanya memiliki cakupan file), dll.
Juliet
4
Anda mungkin ingin membaca stackoverflow.com/questions/1082311/… , yang menjelaskan lebih banyak tentang struct dan mengapa mereka harus dibatasi ke ukuran tertentu. Jika Anda ingin menggunakan warisan dalam sebuah struct maka Anda mungkin harus menggunakan sebuah kelas.
Justin
1
Dan Anda mungkin ingin membaca stackoverflow.com/questions/1222935/… karena menjelaskan mengapa hal itu tidak dapat dilakukan di platform dotNet. Mereka dingin telah membuatnya dengan cara C ++, dengan masalah yang sama yang dapat menjadi bencana bagi platform yang dikelola.
Dykam
@Justin Classes memiliki biaya kinerja yang dapat dihindari oleh struct. Dan dalam pengembangan game, itu sangat penting. Jadi dalam beberapa kasus, Anda tidak boleh menggunakan kelas jika Anda dapat membantu.
Gavin Williams
@Dyam Saya pikir itu bisa dilakukan di C #. Bencana itu dilebih-lebihkan. Saya dapat menulis kode bencana hari ini di C # ketika saya tidak terbiasa dengan teknik. Jadi itu sebenarnya bukan masalah. Jika pewarisan struct dapat memecahkan beberapa masalah dan memberikan kinerja yang lebih baik dalam skenario tertentu, maka saya mendukungnya.
Gavin Williams

Jawaban:

121

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 FooTypemerupakan 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 BasedanDerived 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 !

jonp
sumber
11
Solusi yang masuk akal untuk masalah itu adalah dengan melarang cast formulir Base [] ke Detived []. Sama seperti casting dari pendek [] ke int [] dilarang, meskipun casting dari pendek ke int dimungkinkan.
Niki
3
+ jawaban: masalah dengan warisan tidak cocok dengan saya sampai Anda memasukkannya ke dalam bentuk array. Pengguna lain menyatakan bahwa masalah ini dapat diatasi dengan "mengiris" struct ke ukuran yang sesuai, tetapi saya melihat mengiris sebagai penyebab lebih banyak masalah daripada memecahkannya.
Juliet
4
Ya, tapi itu "masuk akal" karena konversi larik adalah untuk konversi implisit, bukan konversi eksplisit. short to int dimungkinkan, tetapi membutuhkan cast, jadi masuk akal bahwa short [] tidak dapat dikonversi menjadi int [] (singkatan dari kode konversi, seperti 'a.Select (x => (int) x) .ToArray ( ) '). Jika runtime melarang cast dari Base ke Derived, itu akan menjadi "kutukan", karena IS diperbolehkan untuk tipe referensi. Jadi kita memiliki dua kemungkinan "kutil" yang berbeda - melarang pewarisan struct atau melarang konversi array-of-diturunkan menjadi array-of-base.
jonp
3
Setidaknya dengan mencegah pewarisan struct kita memiliki kata kunci terpisah dan dapat lebih mudah mengatakan "struct adalah khusus", sebagai lawan memiliki batasan "acak" dalam sesuatu yang berfungsi untuk satu set hal (kelas) tetapi tidak untuk yang lain (struct) . Saya membayangkan bahwa batasan struct jauh lebih mudah untuk dijelaskan ("mereka berbeda!").
jonp
2
perlu mengubah nama fungsi dari 'square' menjadi 'double'
John
69

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?
Kenan EK
sumber
5
C ++ menjawab ini dengan memperkenalkan konsep 'mengiris', jadi itu masalah yang bisa diselesaikan. Jadi, mengapa pewarisan struct tidak didukung?
jonp
1
Pertimbangkan array dari struct yang diwariskan, dan ingat bahwa C # adalah bahasa yang dikelola (memori). Mengiris atau opsi serupa akan mendatangkan malapetaka pada fundamental CLR.
Kenan EK
4
@ Jonp: Dapat dipecahkan, ya. Diinginkan? Inilah eksperimen pemikiran: bayangkan jika Anda memiliki kelas dasar Vector2D (x, y) dan kelas turunan Vector3D (x, y, z). Kedua kelas memiliki properti Magnitude yang masing-masing menghitung akar (x ^ 2 + y ^ 2) dan akar (x ^ 2 + y ^ 2 + z ^ 2). Jika Anda menulis 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', apa yang seharusnya dikembalikan' a.Magnitude == b.Magnitude '? Jika kita kemudian menulis 'a = (Vector3D) b', apakah a.Magnitude memiliki nilai yang sama sebelum tugas seperti setelahnya? Para desainer NET mungkin berkata pada diri mereka sendiri, "tidak, kami tidak akan memiliki semua itu".
Juliet
1
Hanya karena sebuah masalah bisa diselesaikan, bukan berarti masalah itu harus diselesaikan. Terkadang yang terbaik adalah menghindari situasi di mana masalah muncul.
Dan Diplo
@ kek444: Memiliki struct Fooinherit Barseharusnya tidak mengizinkan a Foountuk ditugaskan ke a Bar, tetapi mendeklarasikan struct seperti itu dapat memungkinkan beberapa efek yang berguna: (1) Buat anggota dengan nama khusus dari tipe Barsebagai item pertama Foo, dan Foosertakan nama anggota yang alias ke anggota tersebut Bar, mengizinkan kode yang telah digunakan Baruntuk diadaptasi untuk menggunakan a Foo, tanpa harus mengganti semua referensi thing.BarMemberdengan thing.theBar.BarMember, dan mempertahankan kemampuan untuk membaca dan menulis semua Barbidang sebagai grup; ...
supercat
14

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.

Martin Liversage
sumber
8
seseorang tidak perlu menggunakan polimorfisme untuk memanfaatkan warisan
rmeador
Jadi, Anda akan mengetahui berapa banyak jenis warisan di .NET?
John Saunders
Polimorfisme memang ada di struct, cukup pertimbangkan perbedaan antara memanggil ToString () saat Anda menerapkannya pada struct kustom atau saat implementasi kustom ToString () tidak ada.
Kenan EK
Itu karena semuanya berasal dari System.Object. Ini lebih merupakan polimorfisme dari tipe System.Object daripada struct.
John Saunders
Polimorfisme bisa bermakna dengan struktur yang digunakan sebagai parameter tipe generik. Polimorfisme bekerja dengan struct yang mengimplementasikan antarmuka; masalah terbesar dengan antarmuka adalah bahwa mereka tidak dapat mengekspos byrefs ke bidang struct. Jika tidak, hal terbesar yang saya pikir akan membantu sejauh "mewarisi" struct akan menjadi sarana memiliki tipe (struct atau kelas) Fooyang memiliki bidang tipe struktur Bardapat menganggap Baranggotanya sebagai miliknya, sehingga sebuah Point3dkelas misalnya bisa merangkum a Point2d xytetapi merujuk ke Xbidang itu sebagai salah satu xy.Xatau X.
supercat
8

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 structmewarisi dari orang lain, dan menyadari bahwa itu tidak mungkin. Tetapi pada titik itu, struktur datanya mungkin sangat maju sehingga harus tetap menjadi kelas.

Blixt
sumber
4
Itu bukanlah alasan mengapa tidak ada warisan.
Dykam
Saya percaya warisan yang dibicarakan di sini tidak dapat menggunakan dua struct di mana satu mewarisi dari yang lain secara bergantian, tetapi menggunakan kembali dan menambah implementasi satu struct ke yang lain (yaitu membuat Point3Ddari a Point2D; Anda tidak akan dapat menggunakan a Point3Dalih - alih a Point2D, tetapi Anda tidak perlu menerapkan ulang Point3Dseluruhnya dari awal.) Begitulah cara saya menafsirkannya ...
Blixt
Singkatnya: dapat mendukung pewarisan tanpa polimorfisme. Tidak. Saya percaya itu adalah pilihan desain untuk membantu seseorang memilih classpada structsaat yang tepat.
Blixt
3

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.

Dykam
sumber
2
C ++ menangani kasus ini dengan baik, IIRC. Instance dari B diiris agar sesuai dengan ukuran A. Jika itu adalah tipe data murni, seperti struct .NET, maka tidak ada hal buruk yang akan terjadi. Anda benar-benar mengalami sedikit masalah dengan metode yang mengembalikan A dan Anda menyimpan nilai pengembalian itu dalam B, tetapi itu seharusnya tidak diizinkan. Singkatnya, desainer .NET dapat menangani ini jika mereka mau, tetapi mereka tidak melakukannya karena alasan tertentu.
rmeador
1
Untuk DoSomething () Anda, kemungkinan tidak akan ada masalah karena (dengan asumsi semantik C ++) 'b' akan "dipotong" untuk membuat instance A. Masalahnya adalah dengan <i> array </i>. Pertimbangkan struct A dan B Anda yang sudah ada, serta metode <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Karena array tipe nilai menyimpan nilai "inline", DoSomething (aktual = baru B [2] {}) akan menyebabkan [0] .childproperty aktual disetel, bukan [1] .properti aktual. Ini buruk.
jonp
2
@ John: Saya tidak menegaskannya, dan menurut saya @jonp juga tidak. Kami hanya menyebutkan bahwa masalah ini sudah lama dan telah dipecahkan, jadi desainer .NET memilih untuk tidak mendukungnya karena alasan lain selain ketidaklayakan teknis.
rmeador
Perlu dicatat bahwa masalah "array tipe turunan" bukanlah hal baru untuk C ++; lihat parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Array dalam C ++ jahat! ;-)
jonp
@ John: solusi untuk masalah "array dari tipe turunan dan tipe basis tidak bercampur" adalah, seperti biasa, Jangan Lakukan Itu. Itulah sebabnya array di C ++ jahat (lebih mudah mengizinkan kerusakan memori), dan mengapa .NET tidak mendukung pewarisan dengan tipe nilai (compiler dan JIT memastikan bahwa itu tidak dapat terjadi).
jonp
3

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. :-)

Rui Craveiro
sumber
3

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.

pengguna38001
sumber
1
Hampir. Tidak ada orang lain yang menyebutkan masalah pemotongan dalam referensi ke tumpukan, hanya mengacu pada array. Dan tidak ada orang lain yang menyebutkan solusi yang tersedia.
pengguna38001
1
Semua jenis nilai dalam .net diisi nol pada pembuatan, apa pun jenisnya atau kolom apa yang dikandungnya. Menambahkan sesuatu seperti pointer vtable ke struct akan membutuhkan sarana untuk menginisialisasi tipe dengan nilai default bukan nol. Fitur seperti itu mungkin berguna untuk berbagai tujuan, dan mengimplementasikan hal seperti itu untuk kebanyakan kasus mungkin tidak terlalu sulit, tetapi tidak ada yang mirip di .net.
supercat
@ user38001 "Struct dialokasikan di stack" - kecuali mereka adalah field instance yang dalam hal ini mereka dialokasikan di heap.
David Klempfner
2

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.

Cecil Memiliki Nama
sumber
Kompilator tahu apa yang ada di sana. Mengacu pada C ++ ini bukanlah jawabannya.
Henk Holterman
Dari mana Anda menyimpulkan C ++? Saya akan mengatakan di tempat karena itulah yang paling cocok dengan perilaku, tumpukan adalah detail implementasi, untuk mengutip artikel blog MSDN.
Cecil Memiliki Nama
Ya, menyebutkan C ++ itu buruk, hanya pemikiran saya. Tetapi selain dari pertanyaan jika info runtime diperlukan, mengapa struct tidak memiliki 'header objek'? Kompiler dapat menggabungkannya sesuka hati. Ia bahkan bisa menyembunyikan sebuah header pada struct [Structlayout].
Henk Holterman
Karena struct adalah tipe nilai, maka tidak perlu dengan header objek karena run-time selalu menyalin konten seperti untuk tipe nilai lainnya (batasan). Ini tidak masuk akal dengan sebuah header, karena itulah kelas tipe referensi untuk: P
Cecil Has a Name
1

Struktur mendukung antarmuka, jadi Anda dapat melakukan beberapa hal polimorfik dengan cara itu.

Wyatt Barnett
sumber
0

IL adalah bahasa berbasis tumpukan, jadi memanggil metode dengan argumen berjalan seperti ini:

  1. Dorong argumen ke tumpukan
  2. Panggil metode tersebut.

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.

Matt Howells
sumber
1
C ++ telah menyelesaikannya, baca jawaban ini untuk masalah sebenarnya: stackoverflow.com/questions/1222935/…
Dykam