Чому структури не підтримують успадкування?


128

Я знаю, що структури в .NET не підтримують успадкування, але не зовсім зрозуміло, чому вони обмежені таким чином.

Яка технічна причина заважає структурам успадковувати інші структури?


6
Я не вмираю за цю функціональність, але я можу придумати кілька випадків, коли успадкування структури було б корисним: ви можете захопити структуру Point2D до структури Point3D з успадкуванням, можливо, ви захочете успадкувати від Int32, щоб обмежити її значення між 1 і 100, можливо, ви захочете створити тип-def, який можна побачити в декількох файлах (трюк Використання typeA = typeB має лише область файлу) тощо.
Джульєтта,

4
Ви можете прочитати stackoverflow.com/questions/1082311/… , де пояснюється трохи більше про структури та чому їх слід обмежувати певним розміром. Якщо ви хочете використовувати спадщину в структурі, то, ймовірно, ви повинні використовувати клас.
Джастін

1
І ви, можливо, захочете прочитати stackoverflow.com/questions/1222935/…, як це іде в глибину, чому це просто не вдалося зробити на платформі dotNet. Вони холодні зробили це способом C ++, з тими ж проблемами, які можуть бути згубними для керованої платформи.
Дікам

@Justin Класи мають витрати на продуктивність, яких можна уникнути. І в розробці ігор це дуже важливо. Тому в деяких випадках вам не слід користуватися класом, якщо ви можете йому допомогти.
Гевін Вільямс

@Dykam Я думаю, що це можна зробити на C #. Катастрофічне - це перебільшення. Я можу написати катастрофічний код сьогодні на C #, коли я не знайомий з технікою. Тож насправді це не проблема. Якщо успадкування структури може вирішити деякі проблеми та забезпечити кращу ефективність за певних сценаріїв, то я все за це.
Гевін Вільямс

Відповіді:


121

Типи значень причини не можуть підтримувати спадкування через масиви.

Проблема полягає в тому, що з міркувань продуктивності та 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!

Спробуй налагодити ТО !


11
Розумним рішенням цієї проблеми було б забороняти форму Base [] для Detived []. Подібно до того, як передавання від короткого [] до int [] заборонено, хоча передача від короткого до int можливе.
Нікі

3
+ відповідь: проблема зі спадщиною не натискала на мене, поки ви не поставили її з точки зору масивів. Інший користувач заявив, що цю проблему можна усунути шляхом "нарізки" конструкцій відповідного розміру, але я вважаю, що нарізка є причиною більшої кількості проблем, ніж вона вирішує.
Джульєтта

4
Так, але це "має сенс", оскільки перетворення масиву призначені для неявних конверсій, а не явних перетворень. короткий до int можливий, але вимагає кастингу, тому розумно, що короткий [] не може бути перетворений на int [] (короткий код конверсії, як 'a.Select (x => (int) x) .ToArray ( ) '). Якщо час виконання заборонив бити з "Base" на "Derived", це було б "бородавкою", як це IS дозволено для еталонних типів. Таким чином, у нас можливі дві різні "бородавки" - заборонити успадкування структури або забороняти перетворення масивів-похідних до масивів-бази.
jonp

3
Принаймні, запобігаючи спадкову структуру, ми маємо окреме ключове слово і можемо легше сказати "структури є спеціальними", на відміну від "випадкового" обмеження в тому, що працює для одного набору речей (класів), але не для іншого (структури) . Я думаю, що обмеження структури набагато простіше пояснити ("вони різні!").
jonp

2
потрібно змінити назву функції з 'квадрата' на 'подвійне'
Іван

69

Уявіть, що структури підтримують успадкування. Потім оголосити:

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?

5
C ++ відповів на це, ввівши поняття "нарізання", тому це є вирішуваною проблемою. Отже, чому не слід підтримувати спадкову структуру?
jonp

1
Розгляньте масиви спадкових структур і пам’ятайте, що C # є мовою, що керується (пам'яттю). Нарізання або будь-який подібний варіант може спричинити загрозу основам CLR.
Кенан Е.К.

4
@jonp: Рішуче, так. Бажано? Ось продуманий експеримент: уявіть, чи є у вас базовий клас Vector2D (x, y) та похідний клас Vector3D (x, y, z). Обидва класи мають властивість Magnitude, яка обчислює sqrt (x ^ 2 + y ^ 2) і sqrt (x ^ 2 + y ^ 2 + z ^ 2) відповідно. Якщо ви пишете 'Vector3D a = Vector3D (5, 10, 15); Vector2D b = a; ', що повинно повертатися "a.Magnitude == b.Magnitude"? Якщо ми тоді запишемо 'a = (Vector3D) b', чи має a.Magnitude те ж значення перед призначенням, як і після? Дизайнери .NET, напевно, сказали собі: "ні, у нас цього нічого не буде".
Джульєтта

1
Тільки тому, що проблему можна вирішити, не означає, що її слід вирішувати. Іноді просто краще уникати ситуацій, коли виникає проблема.
Dan Diplo

@ kek444: Наявність структури Fooнаслідування Barне повинно дозволяти Fooприсвоювати a Bar, але оголошення цього структури може дозволити пару корисних ефектів: (1) Створіть спеціально названий член типу Barяк перший елемент у Foo, і Fooвключіть імена членів , які псевдонім для тих членів Bar, що дозволяє код, котрий використовував Barадаптувати використовувати Fooзамість цього, без необхідності замінити всі посилання на thing.BarMemberз thing.theBar.BarMemberі зберігаючи здатність читати і писати все Bar«s полів в групі; ...
supercat

14

Структури не використовують посилання (якщо тільки вони не поставлені у вікні, але ви повинні намагатися цього уникати), таким чином, поліморфізм не має сенсу, оскільки немає опосередкування через опорний вказівник. Об'єкти зазвичай живуть на купі і посилаються на них за допомогою опорних покажчиків, але структури виділяються на стеку (якщо вони не поставлені у вікні) або виділяються «всередині» пам’яті, зайнятою опорним типом на купі.


8
нікому не потрібно використовувати поліморфізм, щоб скористатися спадщиною
rmeador

Отже, у вас було б багато різних видів успадкування в .NET?
Джон Сондерс

Поліморфізм існує в структурах, просто врахуйте різницю між викликом ToString (), коли ви реалізуєте його на користувальницькій структурі, або коли спеціальна реалізація ToString () не існує.
Кенан Е.К.

Це тому, що всі вони походять від System.Object. Це скоріше поліморфізм типу System.Object, ніж структур.
Джон Сондерс

Поліморфізм може мати значення при структурах, що використовуються як параметри загального типу. Поліморфізм працює зі структурами, що реалізують інтерфейси; Найбільша проблема з інтерфейсами полягає в тому, що вони не можуть піддавати byrefs структурованим полям. В іншому випадку, найбільше, на мою думку, було б корисно, оскільки "успадкування" структур було б засобом наявності типу (структура чи класу), Fooякий має тип структури структури, щоб Barмати можливість вважати Barчленів як власні, так що Point3dклас може , наприклад , инкапсулировать Point2d xyале відносяться до Xцієї області або як xy.Xабо X.
supercat

8

Ось що кажуть документи :

Структури особливо корисні для малих структур даних, які мають значення значення семантики. Складні числа, точки в системі координат або пари ключових значень у словнику - все хороші приклади структур. Ключовим фактором цих структур даних є те, що в них мало членів даних, що вони не потребують використання спадкового або референтного ідентичності, і що вони можуть бути зручно реалізовані, використовуючи значення семантики, коли присвоєння копіює значення замість посилання.

В основному, вони повинні зберігати прості дані і тому не мають "додаткових функцій", таких як успадкування. Напевно, технічно можливо їм підтримати якийсь обмежений вид успадкування (не поліморфізм, через те, що вони знаходяться на стеці), але я вважаю, що це також вибір дизайну, щоб не підтримувати спадщину (як і багато інших речей у .NET мови.)

З іншого боку, я погоджуюся з перевагами спадкування, і я думаю, що ми всі потрапили в той момент, коли хочемо, щоб наше structуспадкувало від іншого, і розуміємо, що це неможливо. Але на той момент структура даних, напевно, настільки вдосконалена, що вона все одно повинна бути класом.


4
Це не є причиною відсутності спадщини.
Дікам

Я вважаю, що спадщина, про яку йдеться тут, не в змозі використовувати дві структури, коли одна успадковує іншу взаємозамінно, а повторне використання та додавання до реалізації однієї структури до іншої (тобто створення Point3Dз а Point2D; ви б не змогли використовувати Point3Dзамість Point2D, але вам не доведеться повторно виконувати Point3Dз нуля.) Ось так я це все-таки інтерпретував ...
Blixt

Коротше кажучи: це могло б підтримати спадщину без поліморфізму. Це не так. Я вважаю , що це вибір дизайну , щоб допомогти людині вибрати classбільш , structколи це необхідно.
Blixt

3

Клас на кшталт успадкування неможливий, оскільки структура закладається безпосередньо на стек. Спадкова структура була б більшою, ніж її батьківська, але 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.


2
C ++ обробляє цей випадок просто чудово, IIRC. Екземпляр B нарізаний так, щоб він відповідав розміру А. Якщо це чистий тип даних, як .NET структури, то нічого поганого не станеться. У вас виникають проблеми з методом, який повертає A, і ви зберігаєте це повернене значення у B, але це не повинно бути дозволено. Коротше кажучи, дизайнери .NET могли б з цим впоратися, але вони чомусь цього не зробили.
rmeador

1
Для вашого DoSomething (), ймовірно, не буде проблеми, оскільки (припустимо, що C ++ семантика) 'b' буде "нарізано", щоб створити екземпляр A. Проблема полягає в <i> масивах </i>. Розглянемо існуючі структури A і B та метод <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Оскільки масиви типів значень зберігають значення "inline", DoSomething (фактичний = новий B [2] {}) призведе до встановлення фактичного [0] .childproperty, а не фактичного [1] .властивості. Це погано.
jonp

2
@John: Я не стверджував, що це було, і я не думаю, що @jonp також не був. Ми просто згадували, що ця проблема є давньою і вирішена, тому дизайнери .NET вирішили не підтримувати її з інших причин, крім технічної нездійсненності.
rmeador

Слід зазначити, що випуск "масивів похідних типів" не є новим для C ++; дивіться parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Масиви в C ++ - це зло !;-)
jonp

@John: рішення "масивів похідних типів та базових типів не змішуються" є проблемою, як зазвичай, "Не робити цього". Ось чому масиви в C ++ є злими (легше допускає пошкодження пам’яті), і чому .NET не підтримує успадкування з типом значень (компілятор і JIT гарантують, що цього не може відбутися).
jonp

3

Є пункт, який я хотів би виправити. Навіть незважаючи на те, що причини структури не можуть бути успадковані через те, що вони живуть на стеці - це правильне, це одночасно наполовину правильне пояснення. Структури, як і будь-який інший тип значення, можуть жити в стеку. Оскільки це буде залежати від того, де буде оголошена змінна, вони будуть або жити в стеці або в купі . Це буде тоді, коли вони є локальними змінними або полями екземпляра відповідно.

Сказавши це, Сесіл має ім'я, як правильно його прибив.

Я хотів би наголосити на цьому, типи значень можуть жити на стеці. Це не означає, що вони завжди так роблять. Локальні змінні, включаючи параметри методу, будуть. Усі інші не будуть. Тим не менш, це все ще залишається причиною їх успадкування. :-)


3

Конструкції виділяються на стеку. Це означає, що семантика значень майже безкоштовна, а доступ до членів структури є дуже дешевим. Це не запобігає поліморфізму.

Ви можете розпочати кожну структуру з вказівника на її віртуальну таблицю функцій. Це було б проблемою з продуктивністю (кожна структура мала би принаймні розмір вказівника), але це можливо. Це дозволило б віртуальним функціям.

А як щодо додавання полів?

Добре, коли ви виділяєте структуру на стек, ви виділяєте певну кількість простору. Необхідний простір визначається під час компіляції (чи достроково, чи під час JITting). Якщо ви додали поля, а потім призначите базовий тип:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Це замінить деяку невідому частину стеку.

Альтернативою є те, що час виконання цього завдання запобігає лише запису байтів sizeof (A) в будь-яку змінну A.

Що станеться, якщо B замінить метод у A та посилається на його поле Integer2? Або час виконання викидає MemberAccessException, або метод замість цього отримує доступ до деяких випадкових даних у стеку. Жодне з них не допустиме.

Цілком безпечно мати успадкування структури, якщо ви не використовуєте структури поліморфно або до тих пір, поки не будете додавати поля при успадкуванні. Але це не дуже корисно.


1
Майже. Ніхто інший не згадував про проблему зрізання у зв'язку із стеком, лише стосовно посилань на масиви. І ніхто не згадав про доступні рішення.
user38001

1
Усі типи значень у .net заповнюються нульовим значенням у креації, незалежно від їх типу або поля, яке вони містять. Додавання чогось на зразок vtable pointer до структури потребує засобів ініціалізації типів з ненульовими значеннями за замовчуванням. Така функція може бути корисною для різних цілей, і реалізація такої речі в більшості випадків може бути не надто складною, але нічого не існує в .net.
supercat

@ user38001 "Структури виділяються на стеку" - якщо тільки вони не є полями екземплярів, у цьому випадку вони виділяються на купі.
Девід Клемффнер

2

Це здається дуже частим питанням. Мені здається, що типи значень зберігаються "на місці", де ти оголошуєш змінну; крім деталей про реалізацію, це означає, що немає жодного заголовка об'єкта, який би щось говорив про об'єкт, лише змінна знає, який тип даних там знаходиться.


Компілятор знає, що там є. Посилання на C ++ це не може бути відповіддю.
Хенк Холтерман

Звідки ви зробили висновок C ++? Я б хотів сказати на місці, тому що саме це відповідає поведінці найкращим чином, стек - це деталізація реалізації, щоб процитувати статтю в блозі MSDN.
Сесіль має ім’я

Так, згадувати C ++ було погано, лише мій потяг думок. Але окрім питання, якщо потрібна інформація про час виконання, чому у структур не має "заголовка об'єкта"? Компілятор може збивати їх у будь-який спосіб, який йому подобається. Він навіть може приховати заголовок структури [Structlayout].
Хенк Холтерман

Оскільки структури - це типові значення, це не обов'язково із заголовком об’єкта, оскільки час виконання завжди копіює вміст, як для інших типів значень (обмеження). Не було б сенсу із заголовком, тому що для цього потрібні класи опорного типу: P
Cecil має ім’я

1

Структури підтримують інтерфейси, тому ви можете робити деякі поліморфні речі таким чином.


0

IL - мова на основі стека, тому виклик методу з аргументом відбувається приблизно так:

  1. Висуньте аргумент на стек
  2. Викличте метод.

Коли метод запускається, він вискакує кілька байт зі стека, щоб отримати його аргумент. Він точно знає , скільки байтів вискакує, тому що аргумент є або вказівником типу посилань (завжди 4 байти на 32-бітному), або типом значення, для якого розмір завжди точно відомий.

Якщо це покажчик типу опорного типу, тоді метод шукає об'єкт у купі та отримує його ручку типу, яка вказує на таблицю методів, яка обробляє цей конкретний метод саме для цього типу. Якщо це тип значення, то ніякий пошук у таблиці методів не потрібен, оскільки типи значень не підтримують успадкування, тому існує лише одна можлива комбінація методу / типу.

Якщо типи значень підтримують успадкування, то буде додаткові накладні витрати в тому, що конкретний тип структури повинен бути розміщений на стеці, а також його значення, що означало б якесь пошук таблиці методів для конкретного конкретного екземпляра типу. Це дозволило б усунути переваги швидкості та ефективності типових типів.


1
C ++ вирішив , що ця відповідь на реальну проблему: stackoverflow.com/questions/1222935 / ...
Dykam
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.