Yapılar neden kalıtımı desteklemiyor?


128

.NET'teki yapıların kalıtımı desteklemediğini biliyorum, ancak neden bu şekilde sınırlı oldukları tam olarak açık değil .

Yapıların diğer yapılardan devralmasını engelleyen teknik neden nedir?


6
Bu işlevsellik için ölmüyorum, ancak yapı kalıtımının yararlı olacağı birkaç durum düşünebilirim: bir Point2D yapısını kalıtımla bir Point3D yapısına genişletmek isteyebilirsiniz, değerleri sınırlamak için Int32'den miras almak isteyebilirsiniz. 1 ile 100 arasında, birden fazla dosyada görülebilen bir type-def oluşturmak isteyebilirsiniz (typeA = typeB hilesinin yalnızca dosya kapsamı vardır), vb.
Juliet

4
Yapılar ve neden belirli bir boyutla sınırlandırılmaları gerektiği hakkında biraz daha fazla açıklama yapan stackoverflow.com/questions/1082311/… sayfasını okumak isteyebilirsiniz . Bir yapıda kalıtımı kullanmak istiyorsanız, muhtemelen bir sınıf kullanıyor olmalısınız.
Justin

1
Ve dotNet platformunda neden yapılamadığını derinlemesine inceleyerek stackoverflow.com/questions/1222935/… ' i okumak isteyebilirsiniz . Yönetilen bir platform için felaketle sonuçlanabilecek aynı problemlerle bunu C ++ yolu haline getirdiler.
Dykam

@Justin Sınıfları, yapıların önleyebileceği performans maliyetlerine sahiptir. Ve oyun geliştirmede gerçekten önemli olan. Bu yüzden bazı durumlarda, eğer yardım edebiliyorsanız bir sınıfı kullanmamalısınız.
Gavin Williams

@Dykam C # ile yapılabileceğini düşünüyorum. Felaket bir abartıdır. Bir tekniğe aşina olmadığım zamanlarda bugün C # ile feci kod yazabilirim. Yani bu gerçekten bir sorun değil. Yapı kalıtımı bazı sorunları çözebilir ve belirli senaryolar altında daha iyi performans sağlayabilirse, o zaman ben varım.
Gavin Williams

Yanıtlar:


121

Değer türlerinin kalıtımı destekleyememesinin nedeni dizilerdir.

Sorun, performans ve GC nedenleriyle, değer türü dizilerinin "satır içi" olarak depolanmasıdır. Örneğin, belirli bir new FooType[10] {...}halinde, FooTypebir referans türüdür 11 nesneler yönetilen yığın (her birine, örneğin dizi için bir, ve 10) üzerinde oluşturulan,. Eğer FooTypebunun yerine bir değer türü, sadece bir örneği Yönetilen öbekte oluşturulacak - dizinin kendisi (her dizi değeri olarak dizi ile "satır içi" saklanacaktır).

Şimdi, değer türleriyle kalıtımımız olduğunu varsayalım. Dizilerin yukarıdaki "satır içi depolama" davranışıyla birleştirildiğinde , C ++ ' da görülebileceği gibi Kötü Şeyler meydana gelir .

Bu sözde C # kodunu düşünün:

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);

Normal dönüştürme kurallarına göre, a Derived[], a'ya dönüştürülebilir Base[](daha iyi veya daha kötü), bu nedenle yukarıdaki örnek için s / struct / class / g yaparsanız, beklendiği gibi sorunsuz bir şekilde derlenir ve çalışır. Ama eğer Baseve Deriveddeğer türleri vardır ve diziler o zaman bir sorun var, satır içi değerleri depolar.

Bir sorunumuz var çünkü Square()hakkında hiçbir şey bilmiyoruz Derived, dizinin her öğesine erişmek için yalnızca işaretçi aritmetiğini kullanacak ve sabit bir miktar ( sizeof(A)) ile artacak . Meclis belli belirsiz şöyle olurdu:

for (int i = 0; i < values.Length; ++i)
{
    A* value = (A*) (((char*) values) + i * sizeof(A));
    value->A *= 2;
}

(Evet, bu iğrenç bir derleme, ancak önemli olan, türetilmiş bir türün kullanıldığına dair herhangi bir bilgi olmadan, bilinen derleme zamanı sabitlerinde dizi boyunca artış yapacağımızdır.)

Yani, bu gerçekten olsaydı, bellek bozulması sorunlarımız olurdu. Spesifik olarak, içinde Square(), aslında değiştirici values[1].A*=2olurdu !values[0].B

BU hata ayıklamayı deneyin !


11
Bu sorunun mantıklı çözümü, Base [] 'den Detived []' ye döküm formuna izin vermemek olacaktır. Tıpkı short [] 'dan int []' ye çevrim yasaktır, ancak kısadan int'e çevrim mümkündür.
Niki

3
+ cevap: kalıtımla ilgili sorun, siz onu diziler açısından ifade edene kadar benimle uyuşmadı. Başka bir kullanıcı, yapıları uygun boyuta "dilimleyerek" bu sorunun hafifletilebileceğini belirtti, ancak dilimlemeyi çözdüğünden daha fazla sorunun nedeni olarak görüyorum.
Juliet

4
Evet, ancak bu "mantıklıdır" çünkü dizi dönüştürmeleri açık dönüştürmeler için değil örtük dönüştürmeler içindir. short'dan int'e mümkündür, ancak bir çevirme gerektirir, bu nedenle short [] 'nın int []' ye dönüştürülememesi mantıklıdır ('a.Select (x => (int) x) gibi dönüşüm kodunun kısaltması) .ToArray ( ) '). Çalışma zamanı Base'den Derived'e geçişe izin vermediyse, bu bir "siğil" olur, çünkü referans türleri için izin verilir. Yani iki farklı "siğil" var - yapı kalıtımını yasaklayın veya türetilmiş dizilerin temel dizilere dönüştürülmesini yasaklayın.
jonp

3
En azından yapı kalıtımını önleyerek ayrı bir anahtar kelimeye sahibiz ve daha kolay bir şekilde "yapılar özeldir" diyebiliriz, bir şeyler kümesi (sınıflar) için işe yarayan ancak bir başkası (yapılar) için işe yaramayan bir şeyde "rastgele" bir sınırlamaya sahip olmanın aksine . Yapı sınırlamasının açıklanmasının çok daha kolay olduğunu düşünüyorum ("onlar farklılar!").
jonp

2
işlevin adını "kare" yerine "çift" olarak değiştirmeniz gerekiyor
John

69

Yapıların kalıtımı desteklediğini düşünün. Ardından şunu beyan edin:

BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.

a = b; //?? expand size during assignment?

yapı değişkenlerinin sabit boyuta sahip olmadığı anlamına gelir ve bu yüzden referans türlerimiz var.

Daha da iyisi, şunu düşünün:

BaseStruct[] baseArray = new BaseStruct[1000];

baseArray[500] = new InheritedStruct(); //?? morph/resize the array?

5
C ++ bunu 'dilimleme' kavramını tanıtarak cevapladı, bu yüzden çözülebilir bir problem bu. Öyleyse, yapı kalıtımı neden desteklenmemeli?
jonp

1
Devralınabilir yapı dizilerini düşünün ve C # 'ın (bellek) tarafından yönetilen bir dil olduğunu unutmayın. Dilimleme veya benzeri herhangi bir seçenek, CLR'nin temellerine zarar verir.
Kenan EK

4
@jonp: Çözülebilir, evet. İstenen? İşte bir düşünce deneyi: Temel bir Vector2D (x, y) sınıfınız ve türetilmiş Vector3D (x, y, z) sınıfınız olup olmadığını düşünün. Her iki sınıf da sırasıyla sqrt (x ^ 2 + y ^ 2) ve sqrt (x ^ 2 + y ^ 2 + z ^ 2) hesaplayan bir Magnitude özelliğine sahiptir. 'Vector3D a = Vector3D (5, 10, 15) yazarsanız; Vector2D b = a; ',' a.Magnitude == b.Magnitude 'ne dönmelidir? O zaman 'a = (Vector3D) b' yazarsak, a.Magnitude atamadan önce olduğu gibi atamadan sonra aynı değere mi sahip olur? .NET tasarımcıları muhtemelen kendilerine "hayır, bunların hiçbirine sahip olmayacağız" dediler.
Juliet

1
Bir sorunun çözülebilmesi, çözülmesi gerektiği anlamına gelmez. Bazen sorunun ortaya çıktığı durumlardan kaçınmak en iyisidir.
Dan Diplo

@ kek444: struct Having Fookaynaklanan devralma Barbir izin vermemelidir Foobir atanacak Bar(1) türünde bir özel olarak adlandırılmış üyesini oluşturun: ama bir yapı ilan yolu yararlı etkilerin birkaç olanak verebilecek Barilk öğe olarak Foove sahip Foobulunmaktadır bu üyelere takma ad veren üye isimleri, eskiden a kullanmak için uyarlanmış Barolan koda , tüm referansları ile değiştirmek zorunda kalmadan izin verir ve bir grup olarak tüm alanlarını okuma ve yazma yeteneğini muhafaza eder ; ...BarFoothing.BarMemberthing.theBar.BarMemberBar
supercat

14

Yapılar referanslar kullanmazlar (kutu içine alınmadıkları sürece, ancak bundan kaçınmaya çalışmalısınız), bu nedenle, bir referans işaretçisi aracılığıyla hiçbir indirme olmadığı için polimorfizm anlamlı değildir. Nesneler normalde öbek üzerinde yaşar ve referans işaretçileri aracılığıyla başvurulur, ancak yapılar yığın üzerinde tahsis edilir (kutuya alınmadıkları sürece) veya öbek üzerinde bir referans türü tarafından kullanılan belleğin "içinde" tahsis edilir.


8
kalıtımdan yararlanmak için polimorfizm kullanmaya gerek yoktur
rmeador

Öyleyse, .NET'te kaç farklı kalıtım türünüz olur?
John Saunders

Çok biçimlilik yapılarda mevcuttur, özel bir yapı üzerinde uyguladığınızda veya özel bir ToString () uygulaması olmadığında ToString () 'yi çağırmak arasındaki farkı düşünün.
Kenan EK

Çünkü hepsi System.Object'ten türetilmiştir. Yapılardan çok System.Object türünün polimorfizmi.
John Saunders

Çok biçimlilik, genel tip parametreleri olarak kullanılan yapılarla anlamlı olabilir. Çok biçimlilik, arabirimleri uygulayan yapılarla çalışır; arabirimlerle ilgili en büyük sorun, byref'leri yapı alanlarına maruz bırakamamalarıdır. Aksi takdirde, ben düşünüyorum en büyük şey uzakta kadar yararlı olacaktır yapılar "miras" bir tür (yapı veya sınıf) sahip bir araç olacaktır Fooyapı türünde bir alan vardır Barbakımdan mümkün Barböylece kendi gibi 'ın üyeleri Bir Point3dsınıf, örneğin a'yı kapsülleyebilir Point2d xyancak Xbu alana ya xy.Xda olarak atıfta bulunabilir X.
supercat

8

İşte dokümanlar şöyle diyor:

Yapılar, özellikle değer semantiği olan küçük veri yapıları için kullanışlıdır. Karmaşık sayılar, bir koordinat sistemindeki noktalar veya bir sözlükteki anahtar-değer çiftlerinin tümü, yapıların iyi örnekleridir. Bu veri yapılarının anahtarı, az sayıda veri üyesine sahip olmaları, kalıtım veya referans kimlik kullanımını gerektirmemeleri ve atamanın referans yerine değeri kopyaladığı değer semantiği kullanılarak uygun şekilde uygulanabilmeleridir.

Temel olarak, basit verileri tutmaları gerekir ve bu nedenle kalıtım gibi "ekstra özelliklere" sahip değildirler. Muhtemelen sınırlı bir tür kalıtımı desteklemeleri teknik olarak mümkün olacaktır (yığında olmaları nedeniyle polimorfizm değil), ancak kalıtımı desteklememenin de bir tasarım seçeneği olduğuna inanıyorum (.NET'teki diğer birçok şey gibi) diller.)

Öte yandan, mirasın faydalarına katılıyorum ve sanırım hepimiz bir structbaşkasından miras almamızı istediğimiz noktaya geldik ve bunun mümkün olmadığını anladık . Ancak bu noktada, veri yapısı muhtemelen o kadar gelişmiştir ki zaten bir sınıf olmalıdır.


4
Mirasın olmamasının nedeni bu değil.
Dykam

İki yapılar kullanmak mümkün değildir miras varlık hakkında burada konuştuk inanıyoruz nerede bir birbirinin diğerinden devralır, ancak yeniden kullanma ve (yani oluşturarak başka bir yapının uygulanmasına ekleyerek Point3Dbir gelen Point2D, siz mümkün olmaz a Point3Dyerine a kullanmak için Point2D, ama Point3Dtamamen sıfırdan yeniden uygulamak zorunda kalmazsınız .) Ben de böyle yorumladım ...
Blixt

Kısacası: Bu olabilir polimorfizm olmadan devralmayı destekler. Öyle değil. Bunun bir kişi seçmek için bir tasarım seçimi inanmak classüzerinde structuygun olduğunda.
Blixt

3

Yapı doğrudan yığının üzerine yerleştirildiği için sınıf benzeri kalıtım mümkün değildir. Miras alan bir yapı, ebeveynden daha büyük olacaktır, ancak JIT bunu bilmiyor ve çok daha az alana çok fazla şey koymaya çalışıyor. Biraz belirsiz geliyor, bir örnek yazalım:

struct A {
    int property;
} // sizeof A == sizeof int

struct B : A {
    int childproperty;
} // sizeof B == sizeof int * 2

Bu mümkün olursa, aşağıdaki kod parçacığında çökebilir:

void DoSomething(A arg){};

...

B b;
DoSomething(b);

Alan, B boyutu için değil, A boyutu için ayrılmıştır.


2
C ++ bu durumu gayet iyi ele alıyor, IIRC. B örneği, bir A boyutuna sığacak şekilde dilimlenir. NET yapıları gibi saf bir veri türündeyse, o zaman kötü bir şey olmaz. A döndüren bir yöntemle biraz sorunla karşılaşırsınız ve bu dönüş değerini bir B'de depoluyorsunuz, ancak buna izin verilmemelidir. Kısacası, .NET tasarımcıları olabilir onlar isterse bu ile ele alınmıştır, ancak nedense yoktu.
rmeador

1
DoSomething () 'iniz için, (C ++ anlambilimini varsayarsak)' b ', bir A örneği oluşturmak için "dilimlenmiş" olacağından, muhtemelen bir problem olmayacaktır. Sorun <i> dizilerle </i>. Mevcut A ve B yapılarınızı ve bir <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c> yöntemini düşünün. Değer türleri dizileri "satır içi" değerleri depoladığından, DoSomething (gerçek = yeni B [2] {}) gerçek [0] .child özelliğinin ayarlanmasına neden olur, gerçek [1]. Bu kötü.
jonp

2
@John: Öyle olduğunu iddia etmiyordum ve @jonp'un da olduğunu sanmıyorum. Sadece bu sorunun eski olduğunu ve çözüldüğünü söylüyorduk, bu nedenle .NET tasarımcıları teknik yetersizlik dışında bir nedenle onu desteklememeyi seçtiler.
rmeador

"Türetilmiş türlerin dizileri" sorununun C ++ için yeni olmadığına dikkat edilmelidir; bkz. parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (C ++ dizileri kötüdür! ;-)
jonp

@John: "Türetilmiş tür dizileri ve temel türler karıştırılmaz" sorunun çözümü, her zamanki gibi Yapma Sorunu. Bu nedenle C ++ 'daki diziler kötüdür (daha kolay bellek bozulmasına izin verir) ve neden .NET, değer türleriyle mirası desteklemez (derleyici ve JIT bunun olmamasını sağlar).
jonp

3

Düzeltmek istediğim bir nokta var. Yapıların miras alınamamasının nedeni yığın üzerinde yaşadıkları için doğru olanı olsa da, aynı zamanda yarım doğru açıklamadır. Başka bir değer türü gibi yapılar, olabilir istif içinde yaşamaktadır. Değişkenin nerede açıklandığına bağlı olacağından, bunlar ya yığın içinde ya da yığın içinde yaşayacaklardır . Bu, sırasıyla yerel değişkenler veya örnek alanları olduklarında olacaktır.

Bunu söylerken, Cecil Has a Name'i doğru bir şekilde çiviledi.

Ben değer türleri, bu vurgulamak isteriz olabilir yığın yaşıyor. Bu, her zaman böyle yaptıkları anlamına gelmez. Yöntem parametreleri dahil yerel değişkenler olacaktır. Diğerleri olmayacak. Yine de, miras alınamamalarının nedeni hala budur. :-)


3

Yapılar yığın üzerinde tahsis edilir. Bu, değer semantiğinin hemen hemen ücretsiz olduğu ve yapı üyelerine erişmenin çok ucuz olduğu anlamına gelir. Bu, polimorfizmi engellemez.

Her yapının sanal işlev tablosuna bir işaretçi ile başlamasını sağlayabilirsiniz. Bu bir performans sorunu olabilir (her yapı en azından bir işaretçi boyutunda olacaktır), ancak yapılabilir. Bu, sanal işlevlere izin verir.

Alan eklemeye ne dersiniz?

Peki, yığına bir yapı ayırdığınızda, belirli bir alan ayırırsınız. Gerekli alan derleme zamanında belirlenir (ister vaktinden önce ister JITting sırasında). Alan ekler ve ardından bir temel türe atarsanız:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Bu, yığının bilinmeyen bazı bölümlerinin üzerine yazacaktır.

Alternatif, çalışma zamanının bunu, herhangi bir A değişkenine yalnızca sizeof (A) bayt yazarak engellemesidir.

B, A'daki bir yöntemi geçersiz kılarsa ve Tamsayı2 alanına başvurursa ne olur? Çalışma zamanı bir MemberAccessException oluşturur veya yöntem bunun yerine yığındaki bazı rasgele verilere erişir. Bunların hiçbirine izin verilmez.

Yapıları polimorfik olarak kullanmadığınız veya devralırken alan eklemediğiniz sürece, yapı kalıtımına sahip olmak tamamen güvenlidir. Ancak bunlar çok da kullanışlı değil.


1
Neredeyse. Yığınla ilgili olarak dilimleme probleminden başka hiç kimse bahsetmedi, sadece dizilere atıfta bulunarak. Ve başka kimse mevcut çözümlerden bahsetmedi.
user38001

1
.Net'teki tüm değer türleri, türlerine veya hangi alanları içerdiklerine bakılmaksızın kreasyonda sıfır doldurulur. Bir yapıya vtable işaretçisi gibi bir şey eklemek, sıfır olmayan varsayılan değerlere sahip türleri başlatmak için bir araç gerektirir. Böyle bir özellik, çeşitli amaçlar için yararlı olabilir ve böyle bir şeyi çoğu durumda uygulamak çok zor olmayabilir, ancak .net'te yakın hiçbir şey yoktur.
supercat

@ user38001 "Yapılar yığın üzerinde tahsis edilir" - örnek alanları olmadıkça, bu durumda öbek üzerinde tahsis edilirler.
David Klempfner

2

Bu çok sık sorulan bir soru gibi görünüyor. Değer türlerinin değişkeni tanımladığınız yerde "yerinde" saklandığını eklemek istiyorum; ayrı uygulama ayrıntıları gelen, olduğu bu araçlar hiçbir nesne hakkında bir şeyler söylüyor nesne başlığı, sadece değişken tür verilerin orada ikamet bilir.


Derleyici orada ne olduğunu bilir. C ++ 'ya başvurmak, cevap olamaz.
Henk Holterman

C ++ 'yı nereden çıkardınız? Yerinde demeyi tercih ederim çünkü davranışla en iyi eşleşen şey budur, yığın bir uygulama ayrıntısıdır, bir MSDN blog makalesinden alıntı yapmak için.
Cecil Has a Name

Evet, C ++ 'dan bahsetmek kötüydü, sadece benim düşünce trenim. Ancak çalışma zamanı bilgisine ihtiyaç olup olmadığı sorusunun yanı sıra, yapıların neden bir 'nesne başlığı' olmaması gerekir? Derleyici onları istediği şekilde ezebilir. Hatta bir [Structlayout] yapısında bir başlığı gizleyebilir.
Henk Holterman

Yapılar değer türleri olduğundan, bir nesne başlığında gerekli değildir çünkü çalışma zamanı her zaman içeriği diğer değer türlerinde olduğu gibi kopyalar (bir kısıtlama). Bir başlık ile mantıklı olmaz çünkü referans türü sınıflar bunun içindir: P
Cecil Has a Name

1

Yapılar arayüzleri destekler, bu yüzden bu şekilde bazı polimorfik şeyler yapabilirsiniz.


0

IL, yığın tabanlı bir dildir, bu nedenle bir yöntemi bir argümanla çağırmak şuna benzer:

  1. Argümanı yığının üzerine itin
  2. Yöntemi çağırın.

Yöntem çalıştığında, argümanını almak için yığından bazı baytlar çıkarır. Bağımsız değişken ya bir başvuru türü işaretçisi (32 bit üzerinde her zaman 4 bayt) ya da boyutun her zaman tam olarak bilindiği bir değer türü olduğu için tam olarak kaç baytın açılacağını bilir .

Bu bir başvuru türü işaretçisi ise, yöntem öbekte nesneyi arar ve tam olarak bu tür için belirli yöntemi işleyen bir yöntem tablosuna işaret eden tür tutamacını alır. Değer türü ise, değer türleri mirası desteklemediğinden, yöntem tablosuna bakmaya gerek yoktur, bu nedenle yalnızca bir olası yöntem / tür kombinasyonu vardır.

Değer türleri kalıtımı destekliyorsa, o zaman yapının belirli türünün değerinin yanı sıra yığına yerleştirilmesi gerektiğinden fazladan ek yük olacaktır, bu da türün belirli somut örneği için bir tür yöntem tablosu araması anlamına gelecektir. Bu, değer türlerinin hız ve verimlilik avantajlarını ortadan kaldıracaktır.


1
C ++ bunu çözdü, gerçek sorun için şu cevabı okuyun: stackoverflow.com/questions/1222935/…
Dykam
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.