Tại sao không cấu trúc hỗ trợ thừa kế?


128

Tôi biết rằng các cấu trúc trong .NET không hỗ trợ kế thừa, nhưng không rõ tại sao chúng bị giới hạn theo cách này.

Lý do kỹ thuật nào ngăn cản các cấu trúc kế thừa từ các cấu trúc khác?


6
Tôi không thích chức năng này, nhưng tôi có thể nghĩ đến một vài trường hợp khi kế thừa cấu trúc sẽ hữu ích: bạn có thể muốn mở rộng cấu trúc Point2D thành cấu trúc Point3D bằng kế thừa, bạn có thể muốn kế thừa từ Int32 để hạn chế giá trị của nó trong khoảng từ 1 đến 100, bạn có thể muốn tạo một loại def-def có thể nhìn thấy trên nhiều tệp (thủ thuật sử dụng typeA = typeB chỉ có phạm vi tệp), v.v.
Juliet

4
Bạn có thể muốn đọc stackoverflow.com/questions/1082311/ , giải thích thêm một chút về cấu trúc và lý do tại sao chúng nên được giới hạn ở một kích thước nhất định. Nếu bạn muốn sử dụng tính kế thừa trong một cấu trúc thì có lẽ bạn nên sử dụng một lớp.
Justin

1
Và bạn có thể muốn đọc stackoverflow.com/questions/1222935/ vì nó đi sâu vào lý do tại sao nó không thể được thực hiện trong nền tảng dotNet. Họ lạnh lùng đã biến nó thành cách thức C ++, với cùng các vấn đề có thể là thảm họa đối với một nền tảng được quản lý.
Dykam

Các lớp @Justin có chi phí hiệu suất mà các cấu trúc có thể tránh được. Và trong phát triển trò chơi thực sự quan trọng. Vì vậy, trong một số trường hợp, bạn không nên sử dụng một lớp nếu bạn có thể giúp nó.
Gavin Williams

@Dykam Tôi nghĩ rằng nó có thể được thực hiện trong C #. Khó chịu là một cường điệu. Tôi có thể viết mã thảm họa ngày hôm nay bằng C # khi tôi không quen với một kỹ thuật. Vì vậy, đó không thực sự là một vấn đề. Nếu kế thừa cấu trúc có thể giải quyết một số vấn đề và cho hiệu suất tốt hơn trong các tình huống nhất định, thì tôi sẽ làm tất cả.
Gavin Williams

Câu trả lời:


121

Các loại giá trị lý do không thể hỗ trợ kế thừa là vì mảng.

Vấn đề là, vì lý do hiệu suất và GC, mảng các loại giá trị được lưu trữ "nội tuyến". Ví dụ, được đưa ra new FooType[10] {...}, nếu FooTypelà một kiểu tham chiếu, 11 đối tượng sẽ được tạo trên heap được quản lý (một cho mảng và 10 cho mỗi thể hiện loại). Nếu FooTypethay vào đó là một loại giá trị, chỉ có một phiên bản sẽ được tạo trên heap được quản lý - cho chính mảng đó (vì mỗi giá trị mảng sẽ được lưu trữ "nội tuyến" với mảng).

Bây giờ, giả sử chúng ta có sự kế thừa với các loại giá trị. Khi kết hợp với hành vi "lưu trữ nội tuyến" ở trên của các mảng, Những điều tồi tệ sẽ xảy ra, như có thể thấy trong C ++ .

Hãy xem xét mã giả C # này:

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

Theo quy tắc chuyển đổi thông thường, a Derived[]có thể chuyển đổi thành Base[](tốt hơn hoặc xấu hơn), vì vậy nếu bạn s / struct / class / g cho ví dụ trên, nó sẽ biên dịch và chạy như mong đợi, không có vấn đề gì. Nhưng nếu BaseDerivedlà các loại giá trị và mảng lưu trữ giá trị nội tuyến, thì chúng ta có một vấn đề.

Chúng tôi có một vấn đề vì Square()không biết gì về Derivednó, nó sẽ chỉ sử dụng số học con trỏ để truy cập từng phần tử của mảng, tăng thêm một lượng không đổi ( sizeof(A)). Việc lắp ráp sẽ mơ hồ như sau:

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

(Vâng, đó là sự lắp ráp gớm ghiếc, nhưng vấn đề là chúng ta sẽ tăng dần qua mảng tại các hằng số thời gian biên dịch đã biết, mà không biết rằng loại dẫn xuất đang được sử dụng.)

Vì vậy, nếu điều này thực sự xảy ra, chúng ta sẽ gặp vấn đề về tham nhũng bộ nhớ. Cụ thể, bên trong Square(), values[1].A*=2sẽ thực sự được sửa đổi values[0].B!

Cố gắng gỡ lỗi RATNG !


11
Giải pháp hợp lý cho vấn đề đó sẽ là không cho phép biểu mẫu Base [] thành Detective []. Cũng giống như việc truyền từ short [] sang int [] bị cấm, mặc dù việc truyền từ short sang int là có thể.
Niki

3
+ câu trả lời: vấn đề về thừa kế đã không nhấp vào tôi cho đến khi bạn đặt nó dưới dạng mảng. Một người dùng khác tuyên bố rằng vấn đề này có thể được giảm thiểu bằng cách "cắt" các cấu trúc đến kích thước phù hợp, nhưng tôi thấy việc cắt lát là nguyên nhân của nhiều vấn đề hơn nó giải quyết.
Juliet

4
Có, nhưng điều đó "có ý nghĩa" bởi vì chuyển đổi mảng là dành cho chuyển đổi ngầm định, không phải chuyển đổi rõ ràng. có thể rút ngắn thành int, nhưng yêu cầu diễn viên, do đó, thật hợp lý khi ngắn [] không thể chuyển đổi thành int [] (viết tắt mã chuyển đổi, như 'a.Select (x => (int) x) .ToArray ( ) '). Nếu thời gian chạy không cho phép truyền từ Base sang Derogen, nó sẽ là "mụn cóc", vì IS được phép cho các loại tham chiếu. Vì vậy, chúng ta có hai "mụn cóc" khác nhau có thể - cấm kế thừa cấu trúc hoặc cấm chuyển đổi các mảng có nguồn gốc thành mảng cơ sở.
jonp

3
Ít nhất bằng cách ngăn chặn sự kế thừa cấu trúc, chúng ta có một từ khóa riêng biệt và có thể dễ dàng nói "structs là đặc biệt", trái ngược với giới hạn "ngẫu nhiên" trong một thứ hoạt động cho một tập hợp các thứ (lớp) nhưng không phải cho một (các lớp) khác . Tôi tưởng tượng rằng giới hạn cấu trúc dễ giải thích hơn nhiều ("chúng khác nhau!").
jonp

2
cần thay đổi tên của hàm từ 'vuông' thành 'double'
John

69

Hãy tưởng tượng các cấu trúc được hỗ trợ thừa kế. Sau đó khai báo:

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

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

có nghĩa là các biến cấu trúc không có kích thước cố định và đó là lý do tại sao chúng ta có các kiểu tham chiếu.

Thậm chí tốt hơn, xem xét điều này:

BaseStruct[] baseArray = new BaseStruct[1000];

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

5
C ++ đã trả lời điều này bằng cách đưa ra khái niệm 'cắt lát', vì vậy đó là một vấn đề có thể giải quyết được. Vì vậy, tại sao cấu trúc không nên được hỗ trợ?
jonp

1
Hãy xem xét các mảng của các cấu trúc kế thừa và hãy nhớ rằng C # là ngôn ngữ được quản lý (bộ nhớ). Cắt lát hoặc bất kỳ tùy chọn tương tự nào sẽ tàn phá các nguyên tắc cơ bản của CLR.
Kenan EK

4
@jonp: Có thể giải được, vâng. Mong muốn? Đây là một thử nghiệm suy nghĩ: hãy tưởng tượng nếu bạn có một lớp cơ sở Vector2D (x, y) và lớp dẫn xuất Vector3D (x, y, z). Cả hai lớp đều có thuộc tính Độ lớn tính toán sqrt (x ^ 2 + y ^ 2) và sqrt (x ^ 2 + y ^ 2 + z ^ 2) tương ứng. Nếu bạn viết 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ',' a.Magnitude == b.Magnitude 'nên trả về cái gì? Nếu sau đó chúng ta viết 'a = (Vector3D) b', thì a.Magnitude có cùng giá trị trước khi gán như sau không? Các nhà thiết kế .NET có thể tự nói với mình, "không, chúng ta sẽ không có điều đó".
Juliet

1
Chỉ vì một vấn đề có thể được giải quyết, không có nghĩa là nó cần được giải quyết. Đôi khi, tốt nhất là tránh những tình huống phát sinh vấn đề.
Dan Diplo

@ kek444: Việc Foothừa kế cấu trúc Barkhông nên cho phép Foogán cho a Bar, nhưng khai báo một cấu trúc theo cách đó có thể cho phép một vài hiệu ứng hữu ích: (1) Tạo một thành viên có tên đặc biệt là loại Barđầu tiên FooFoobao gồm tên thành viên đó bí danh cho những thành viên trong Bar, cho phép mã mà đã sử dụng Barđể được điều chỉnh để sử dụng một Foothay vào đó, mà không cần phải thay thế tất cả các tham chiếu tới thing.BarMembervới thing.theBar.BarMember, và duy trì khả năng đọc và ghi tất cả các Bar's các lĩnh vực như một nhóm; ...
supercat

14

Các cấu trúc không sử dụng các tham chiếu (trừ khi chúng được đóng hộp, nhưng bạn nên cố gắng tránh điều đó) do đó tính đa hình không có ý nghĩa vì không có sự gián tiếp thông qua một con trỏ tham chiếu. Các đối tượng thường sống trên heap và được tham chiếu qua các con trỏ tham chiếu, nhưng các cấu trúc được phân bổ trên ngăn xếp (trừ khi chúng được đóng hộp) hoặc được phân bổ "bên trong" bộ nhớ bị chiếm bởi một loại tham chiếu trên heap.


8
người ta không cần sử dụng đa hình để tận dụng quyền thừa kế
rmeador

Vì vậy, bạn có bao nhiêu loại thừa kế khác nhau trong .NET?
John Saunders

Đa hình không tồn tại trong các cấu trúc, chỉ cần xem xét sự khác biệt giữa việc gọi ToString () khi bạn triển khai nó trên cấu trúc tùy chỉnh hoặc khi triển khai tùy chỉnh ToString () không tồn tại.
Kenan EK

Đó là bởi vì tất cả chúng đều xuất phát từ System.Object. Đó là tính đa hình của kiểu System.Object hơn là các cấu trúc.
John Saunders

Đa hình có thể có ý nghĩa với các cấu trúc được sử dụng làm tham số loại chung. Đa hình hoạt động với các cấu trúc thực hiện giao diện; vấn đề lớn nhất với các giao diện là chúng không thể hiển thị byrefs cho các trường cấu trúc. Mặt khác, điều lớn nhất tôi nghĩ sẽ hữu ích cho đến khi các cấu trúc "kế thừa" sẽ là một phương tiện để có một loại (struct hoặc class) Foocó một trường loại cấu trúc Barcó thể coi Barcác thành viên của nó là của riêng nó, do đó một Point3dlớp có thể ví dụ gói gọn một Point2d xynhưng tham chiếu đến trường Xđó là xy.Xhoặc X.
supercat

8

Đây là những gì các tài liệu nói:

Cấu trúc đặc biệt hữu ích cho các cấu trúc dữ liệu nhỏ có ngữ nghĩa giá trị. Số phức, điểm trong hệ tọa độ hoặc cặp giá trị khóa trong từ điển đều là những ví dụ hay về cấu trúc. Điểm mấu chốt của các cấu trúc dữ liệu này là chúng có ít thành viên dữ liệu, chúng không yêu cầu sử dụng tính kế thừa hoặc danh tính tham chiếu và chúng có thể được thực hiện thuận tiện bằng cách sử dụng ngữ nghĩa giá trị trong đó phép gán sao chép giá trị thay vì tham chiếu.

Về cơ bản, chúng được cho là chứa dữ liệu đơn giản và do đó không có "tính năng bổ sung" như tính kế thừa. Về mặt kỹ thuật, họ có thể hỗ trợ một số loại thừa kế hạn chế (không phải đa hình, do chúng nằm trên ngăn xếp), nhưng tôi tin rằng đó cũng là một lựa chọn thiết kế để không hỗ trợ thừa kế (như nhiều thứ khác trong .NET ngôn ngữ là.)

Mặt khác, tôi đồng ý với lợi ích của việc thừa kế và tôi nghĩ tất cả chúng ta đã đạt đến điểm mà chúng ta muốn structthừa kế từ người khác và nhận ra rằng điều đó là không thể. Nhưng tại thời điểm đó, cấu trúc dữ liệu có thể tiên tiến đến mức dù sao nó cũng phải là một lớp.


4
Đó không phải là lý do tại sao không có thừa kế.
Dykam

Tôi tin rằng sự kế thừa đang được nói đến ở đây là không thể sử dụng hai cấu trúc trong đó một kế thừa có thể hoán đổi cho nhau, nhưng sử dụng lại và thêm vào việc thực hiện cấu trúc này sang cấu trúc khác (nghĩa là tạo ra Point3Dtừ một Point2D; bạn sẽ không thể để sử dụng Point3Dthay vì một Point2D, nhưng bạn sẽ không phải thực hiện lại Point3Dtoàn bộ từ đầu.) Đó là cách tôi diễn giải nó bằng mọi cách ...
Blixt

Nói tóm lại: nó có thể hỗ trợ kế thừa mà không cần đa hình. Nó không. Tôi tin rằng đó là một sự lựa chọn thiết kế để giúp một người chọn classhơn structkhi thích hợp.
Blixt

3

Lớp như thừa kế là không thể, vì một cấu trúc được đặt trực tiếp trên ngăn xếp. Cấu trúc kế thừa sẽ lớn hơn sau đó là cha mẹ, nhưng JIT không biết như vậy và cố gắng đặt quá nhiều vào không gian quá ít. Nghe có vẻ không rõ ràng, hãy viết một ví dụ:

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

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

Nếu điều này là có thể, nó sẽ sụp đổ trên đoạn mã sau:

void DoSomething(A arg){};

...

B b;
DoSomething(b);

Không gian được phân bổ cho sizeof A, không dành cho sizeof B.


2
C ++ xử lý trường hợp này tốt, IIRC. Ví dụ của B được cắt để phù hợp với kích thước của A. Nếu đó là kiểu dữ liệu thuần túy, như các cấu trúc .NET, thì sẽ không có gì xấu xảy ra. Bạn gặp phải một chút vấn đề với phương thức trả về A và bạn đang lưu trữ giá trị trả về đó trong B, nhưng điều đó không được phép. Nói tóm lại, các nhà thiết kế .NET có thể đã xử lý vấn đề này nếu họ muốn, nhưng họ không vì một lý do nào đó.
rmeador

1
Đối với DoS Something () của bạn, không có khả năng xảy ra sự cố vì (giả sử ngữ nghĩa C ++) 'b' sẽ bị "cắt" để tạo một thể hiện A. Vấn đề là với <i> mảng </ i>. Xem xét các cấu trúc A và B hiện có của bạn và phương thức <c> DoS Something (A [] arg) {arg [1] .property = 1;} </ c>. Do các mảng của các loại giá trị lưu trữ các giá trị "nội tuyến", DoS Something (thực tế = new B [2] {}) sẽ khiến [0] .childproperty thực tế được đặt, không thực tế [1] .property. Điều này tệ đây.
jonp

2
@ John: Tôi đã không khẳng định điều đó và tôi cũng không nghĩ @jonp cũng vậy. Chúng tôi chỉ đơn thuần đề cập rằng vấn đề này đã cũ và đã được giải quyết, vì vậy các nhà thiết kế .NET đã chọn không hỗ trợ nó vì một số lý do khác ngoài tính không khả thi về kỹ thuật.
rmeador

Cần lưu ý rằng vấn đề "mảng các loại dẫn xuất" không phải là mới đối với C ++; xem parashift.com/c++-faq-lite/proper-inherribution.html#faq-21.4 (Mảng trong C ++ là xấu xa! ;-)
jonp

@ John: giải pháp cho "mảng các loại dẫn xuất và loại cơ sở không trộn lẫn" là, như thường lệ, Đừng làm điều đó. Đó là lý do tại sao các mảng trong C ++ là xấu (dễ dàng hơn cho phép hỏng bộ nhớ) và tại sao .NET không hỗ trợ kế thừa với các loại giá trị (trình biên dịch và JIT đảm bảo rằng điều đó không thể xảy ra).
jonp

3

Có một điểm tôi muốn sửa. Mặc dù lý do cấu trúc không thể được kế thừa là vì chúng sống trên ngăn xếp là đúng, nhưng đó cũng là một lời giải thích đúng một nửa. Cấu trúc, giống như bất kỳ loại giá trị nào khác có thể sống trong ngăn xếp. Bởi vì nó sẽ phụ thuộc vào nơi biến được khai báo, chúng sẽ sống trong ngăn xếp hoặc trong heap . Điều này sẽ là khi chúng là các biến cục bộ hoặc các trường ví dụ tương ứng.

Nói như vậy, Cecil Has Name đã đóng đinh nó một cách chính xác.

Tôi muốn nhấn mạnh điều này, các loại giá trị có thể sống trên ngăn xếp. Điều này không có nghĩa là họ luôn làm như vậy. Các biến cục bộ, bao gồm các tham số phương thức, sẽ. Tất cả những người khác sẽ không. Tuy nhiên, đó vẫn là lý do họ không thể được thừa kế. :-)


3

Các cấu trúc được phân bổ trên ngăn xếp. Điều này có nghĩa là ngữ nghĩa giá trị là khá nhiều miễn phí và truy cập các thành viên struct rất rẻ. Điều này không ngăn chặn đa hình.

Bạn có thể có mỗi struct bắt đầu bằng một con trỏ tới bảng chức năng ảo của nó. Đây sẽ là một vấn đề về hiệu năng (mọi cấu trúc sẽ có kích thước tối thiểu bằng một con trỏ), nhưng nó có thể thực hiện được. Điều này sẽ cho phép các chức năng ảo.

Còn việc thêm các lĩnh vực thì sao?

Chà, khi bạn phân bổ một cấu trúc trên ngăn xếp, bạn phân bổ một lượng không gian nhất định. Không gian cần thiết được xác định tại thời điểm biên dịch (cho dù trước thời hạn hoặc khi JITting). Nếu bạn thêm các trường và sau đó gán cho một loại cơ sở:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Điều này sẽ ghi đè lên một số phần chưa biết của ngăn xếp.

Thay thế là cho thời gian chạy để ngăn chặn điều này bằng cách chỉ ghi byte sizeof (A) vào bất kỳ biến A nào.

Điều gì xảy ra nếu B ghi đè một phương thức trong A và tham chiếu trường Integer2 của nó? Thời gian chạy sẽ ném MemberAccessException hoặc phương thức thay vào đó truy cập một số dữ liệu ngẫu nhiên trên ngăn xếp. Cả hai điều này đều không được phép.

Hoàn toàn an toàn khi có sự kế thừa cấu trúc, miễn là bạn không sử dụng cấu trúc đa hình hoặc miễn là bạn không thêm các trường khi kế thừa. Nhưng những điều này không hữu ích lắm.


1
Hầu hết. Không ai khác đề cập đến vấn đề cắt liên quan đến ngăn xếp, chỉ liên quan đến mảng. Và không ai khác đề cập đến các giải pháp có sẵn.
user38001

1
Tất cả các loại giá trị trong .net đều được điền bằng 0, bất kể loại của chúng hay trường nào chúng chứa. Thêm một cái gì đó như một con trỏ vtable vào một cấu trúc sẽ yêu cầu một phương tiện khởi tạo các loại với các giá trị mặc định khác không. Một tính năng như vậy có thể hữu ích cho nhiều mục đích khác nhau và việc thực hiện một điều như vậy đối với hầu hết các trường hợp có thể không quá khó, nhưng không có gì gần gũi tồn tại trong .net.
supercat

@ user38001 "Các cấu trúc được phân bổ trên ngăn xếp" - trừ khi chúng là các trường đối tượng trong trường hợp chúng được phân bổ trên heap.
David Klempfner

2

Đây dường như là một câu hỏi rất thường xuyên. Tôi cảm thấy như thêm các loại giá trị được lưu trữ "tại chỗ" nơi bạn khai báo biến; ngoài các chi tiết triển khai, điều này có nghĩa là không có tiêu đề đối tượng nói điều gì về đối tượng, chỉ có biến biết loại dữ liệu nào nằm trong đó.


Trình biên dịch biết những gì ở đó. Tham khảo C ++ này không thể là câu trả lời.
Henk Holterman

Bạn đã suy ra C ++ từ đâu? Tôi muốn nói tại chỗ vì đó là điều phù hợp nhất với hành vi, ngăn xếp là một chi tiết triển khai, để trích dẫn một bài viết trên blog của MSDN.
Cecil có tên

Vâng, đề cập đến C ++ là xấu, chỉ là suy nghĩ của tôi. Nhưng ngoài câu hỏi nếu cần thông tin về thời gian chạy, tại sao cấu trúc không nên có 'tiêu đề đối tượng'? Trình biên dịch có thể kết hợp chúng theo bất cứ cách nào nó thích. Nó thậm chí có thể ẩn một tiêu đề trên cấu trúc [Structlayout].
Henk Holterman

Vì cấu trúc là các loại giá trị, không cần thiết với tiêu đề đối tượng vì thời gian chạy luôn sao chép nội dung như đối với các loại giá trị khác (một ràng buộc). Nó sẽ không có ý nghĩa với một tiêu đề, bởi vì đó là những loại lớp tham chiếu dành cho: P
Cecil Có một cái tên

1

Cấu trúc hỗ trợ các giao diện, vì vậy bạn có thể thực hiện một số điều đa hình theo cách đó.


0

IL là một ngôn ngữ dựa trên ngăn xếp, vì vậy việc gọi một phương thức với một đối số sẽ diễn ra như sau:

  1. Đẩy đối số lên ngăn xếp
  2. Gọi phương thức.

Khi phương thức chạy, nó bật một số byte ra khỏi ngăn xếp để lấy đối số của nó. Nó biết chính xác bao nhiêu byte bật ra vì đối số là con trỏ kiểu tham chiếu (luôn là 4 byte trên 32 bit) hoặc đó là loại giá trị mà kích thước luôn được biết chính xác.

Nếu nó là một con trỏ kiểu tham chiếu thì phương thức sẽ tra cứu đối tượng trong heap và xử lý kiểu của nó, nó trỏ đến một bảng phương thức xử lý phương thức cụ thể đó cho loại chính xác đó. Nếu nó là một loại giá trị, thì không cần tra cứu bảng phương thức vì các loại giá trị không hỗ trợ kế thừa, do đó chỉ có một kết hợp phương thức / loại có thể.

Nếu các kiểu giá trị được hỗ trợ kế thừa thì sẽ có thêm chi phí trong đó loại cấu trúc cụ thể sẽ phải được đặt trên ngăn xếp cũng như giá trị của nó, điều đó có nghĩa là một loại tra cứu bảng phương thức cho trường hợp cụ thể của loại. Điều này sẽ loại bỏ lợi thế tốc độ và hiệu quả của các loại giá trị.


1
C ++ đã giải quyết được điều đó, hãy đọc câu trả lời này cho vấn đề thực sự: stackoverflow.com/questions/1222935/ Lời
Dykam
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.