Я знаю, що структури в .NET не підтримують успадкування, але не зовсім зрозуміло, чому вони обмежені таким чином.
Яка технічна причина заважає структурам успадковувати інші структури?
Я знаю, що структури в .NET не підтримують успадкування, але не зовсім зрозуміло, чому вони обмежені таким чином.
Яка технічна причина заважає структурам успадковувати інші структури?
Відповіді:
Типи значень причини не можуть підтримувати спадкування через масиви.
Проблема полягає в тому, що з міркувань продуктивності та GC масиви типів значень зберігаються "inline". Наприклад, заданий new FooType[10] {...}
, якщо FooType
це посилання тип, на керованій купі буде створено 11 об'єктів (один для масиву та 10 для кожного екземпляра типу). Якщо FooType
замість цього значення тип, на керованій купі буде створено лише один екземпляр - для самого масиву (оскільки кожне значення масиву буде зберігатися "в рядку" з масивом).
Тепер, припустимо, у нас було спадкування з типовими значеннями. У поєднанні з вищезазначеною поведінкою масивів "вбудованого зберігання" погані речі трапляються, як це можна побачити на C ++ .
Розглянемо цей псевдо-C # код:
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);
За звичайними правилами перетворення, a Derived[]
можна перетворити на Base[]
(для кращого чи гіршого), тому якщо ви / s / struct / class / g для наведеного вище прикладу, він буде компілюватися та працювати як слід, без проблем. Але якщо Base
і Derived
є типи значень, а масиви зберігають вбудовані значення, то у нас є проблема.
У нас є проблема, тому що Square()
нічого не знаю Derived
, вона використовуватиме лише арифметику вказівника для доступу до кожного елемента масиву, збільшуючись на постійну кількість ( sizeof(A)
). Збірка була б невиразною, як:
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(Так, це гнусна збірка, але справа в тому, що ми збільшимося через масив на відомих константах часу компіляції, не знаючи, що використовується похідний тип.)
Отже, якби це насправді сталося, у нас виникли б проблеми з корупцією пам’яті. В Зокрема, в межах Square()
, values[1].A*=2
буде на самому ділі бути модифікування values[0].B
!
Спробуй налагодити ТО !
Уявіть, що структури підтримують успадкування. Потім оголосити:
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
означало б, що змінні структури не мають фіксованого розміру, і тому ми маємо типи посилань.
Ще краще, врахуйте це:
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
наслідування Bar
не повинно дозволяти Foo
присвоювати a Bar
, але оголошення цього структури може дозволити пару корисних ефектів: (1) Створіть спеціально названий член типу Bar
як перший елемент у Foo
, і Foo
включіть імена членів , які псевдонім для тих членів Bar
, що дозволяє код, котрий використовував Bar
адаптувати використовувати Foo
замість цього, без необхідності замінити всі посилання на thing.BarMember
з thing.theBar.BarMember
і зберігаючи здатність читати і писати все Bar
«s полів в групі; ...
Структури не використовують посилання (якщо тільки вони не поставлені у вікні, але ви повинні намагатися цього уникати), таким чином, поліморфізм не має сенсу, оскільки немає опосередкування через опорний вказівник. Об'єкти зазвичай живуть на купі і посилаються на них за допомогою опорних покажчиків, але структури виділяються на стеку (якщо вони не поставлені у вікні) або виділяються «всередині» пам’яті, зайнятою опорним типом на купі.
Foo
який має тип структури структури, щоб Bar
мати можливість вважати Bar
членів як власні, так що Point3d
клас може , наприклад , инкапсулировать Point2d xy
але відносяться до X
цієї області або як xy.X
або X
.
Ось що кажуть документи :
Структури особливо корисні для малих структур даних, які мають значення значення семантики. Складні числа, точки в системі координат або пари ключових значень у словнику - все хороші приклади структур. Ключовим фактором цих структур даних є те, що в них мало членів даних, що вони не потребують використання спадкового або референтного ідентичності, і що вони можуть бути зручно реалізовані, використовуючи значення семантики, коли присвоєння копіює значення замість посилання.
В основному, вони повинні зберігати прості дані і тому не мають "додаткових функцій", таких як успадкування. Напевно, технічно можливо їм підтримати якийсь обмежений вид успадкування (не поліморфізм, через те, що вони знаходяться на стеці), але я вважаю, що це також вибір дизайну, щоб не підтримувати спадщину (як і багато інших речей у .NET мови.)
З іншого боку, я погоджуюся з перевагами спадкування, і я думаю, що ми всі потрапили в той момент, коли хочемо, щоб наше struct
успадкувало від іншого, і розуміємо, що це неможливо. Але на той момент структура даних, напевно, настільки вдосконалена, що вона все одно повинна бути класом.
Point3D
з а Point2D
; ви б не змогли використовувати Point3D
замість Point2D
, але вам не доведеться повторно виконувати Point3D
з нуля.) Ось так я це все-таки інтерпретував ...
class
більш , struct
коли це необхідно.
Клас на кшталт успадкування неможливий, оскільки структура закладається безпосередньо на стек. Спадкова структура була б більшою, ніж її батьківська, але JIT цього не знає, і намагається поставити занадто багато місця на занадто менше місця. Звучить трохи незрозуміло, напишемо приклад:
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
Якщо це стане можливим, воно вийде з ладу за таким фрагментом:
void DoSomething(A arg){};
...
B b;
DoSomething(b);
Простір виділяється для розміру A, а не для розміру B.
Є пункт, який я хотів би виправити. Навіть незважаючи на те, що причини структури не можуть бути успадковані через те, що вони живуть на стеці - це правильне, це одночасно наполовину правильне пояснення. Структури, як і будь-який інший тип значення, можуть жити в стеку. Оскільки це буде залежати від того, де буде оголошена змінна, вони будуть або жити в стеці або в купі . Це буде тоді, коли вони є локальними змінними або полями екземпляра відповідно.
Сказавши це, Сесіл має ім'я, як правильно його прибив.
Я хотів би наголосити на цьому, типи значень можуть жити на стеці. Це не означає, що вони завжди так роблять. Локальні змінні, включаючи параметри методу, будуть. Усі інші не будуть. Тим не менш, це все ще залишається причиною їх успадкування. :-)
Конструкції виділяються на стеку. Це означає, що семантика значень майже безкоштовна, а доступ до членів структури є дуже дешевим. Це не запобігає поліморфізму.
Ви можете розпочати кожну структуру з вказівника на її віртуальну таблицю функцій. Це було б проблемою з продуктивністю (кожна структура мала би принаймні розмір вказівника), але це можливо. Це дозволило б віртуальним функціям.
А як щодо додавання полів?
Добре, коли ви виділяєте структуру на стек, ви виділяєте певну кількість простору. Необхідний простір визначається під час компіляції (чи достроково, чи під час JITting). Якщо ви додали поля, а потім призначите базовий тип:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Це замінить деяку невідому частину стеку.
Альтернативою є те, що час виконання цього завдання запобігає лише запису байтів sizeof (A) в будь-яку змінну A.
Що станеться, якщо B замінить метод у A та посилається на його поле Integer2? Або час виконання викидає MemberAccessException, або метод замість цього отримує доступ до деяких випадкових даних у стеку. Жодне з них не допустиме.
Цілком безпечно мати успадкування структури, якщо ви не використовуєте структури поліморфно або до тих пір, поки не будете додавати поля при успадкуванні. Але це не дуже корисно.
Це здається дуже частим питанням. Мені здається, що типи значень зберігаються "на місці", де ти оголошуєш змінну; крім деталей про реалізацію, це означає, що немає жодного заголовка об'єкта, який би щось говорив про об'єкт, лише змінна знає, який тип даних там знаходиться.
Структури підтримують інтерфейси, тому ви можете робити деякі поліморфні речі таким чином.
IL - мова на основі стека, тому виклик методу з аргументом відбувається приблизно так:
Коли метод запускається, він вискакує кілька байт зі стека, щоб отримати його аргумент. Він точно знає , скільки байтів вискакує, тому що аргумент є або вказівником типу посилань (завжди 4 байти на 32-бітному), або типом значення, для якого розмір завжди точно відомий.
Якщо це покажчик типу опорного типу, тоді метод шукає об'єкт у купі та отримує його ручку типу, яка вказує на таблицю методів, яка обробляє цей конкретний метод саме для цього типу. Якщо це тип значення, то ніякий пошук у таблиці методів не потрібен, оскільки типи значень не підтримують успадкування, тому існує лише одна можлива комбінація методу / типу.
Якщо типи значень підтримують успадкування, то буде додаткові накладні витрати в тому, що конкретний тип структури повинен бути розміщений на стеці, а також його значення, що означало б якесь пошук таблиці методів для конкретного конкретного екземпляра типу. Це дозволило б усунути переваги швидкості та ефективності типових типів.