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?
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?
Câu trả lời:
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 FooType
là 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 FooType
thay 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 Base
và Derived
là 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ề Derived
nó, 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*=2
sẽ thực sự được sửa đổi values[0].B
!
Cố gắng gỡ lỗi RATNG !
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?
Foo
thừa kế cấu trúc Bar
không nên cho phép Foo
gá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 Foo
và Foo
bao 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 Foo
thay vào đó, mà không cần phải thay thế tất cả các tham chiếu tới thing.BarMember
vớ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; ...
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.
Foo
có một trường loại cấu trúc Bar
có thể coi Bar
các thành viên của nó là của riêng nó, do đó một Point3d
lớp có thể ví dụ gói gọn một Point2d xy
nhưng tham chiếu đến trường X
đó là xy.X
hoặc X
.
Đâ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 struct
thừ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.
Point3D
từ một Point2D
; bạn sẽ không thể để sử dụng Point3D
thay vì một Point2D
, nhưng bạn sẽ không phải thực hiện lại Point3D
toàn bộ từ đầu.) Đó là cách tôi diễn giải nó bằng mọi cách ...
class
hơn struct
khi thích hợp.
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.
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ế. :-)
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.
Đâ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 đó.
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 đó.
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:
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ị.