Почему структуры не поддерживают наследование?

128

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

Какая техническая причина препятствует наследованию структур от других структур?

Джульетта
источник
6
Я не умираю за эту функциональность, но я могу вспомнить несколько случаев, когда было бы полезно наследование структур: вы можете расширить структуру Point2D до структуры Point3D с помощью наследования, вы можете наследовать от Int32, чтобы ограничить его значения от 1 до 100, вы можете захотеть создать определение типа, которое будет видно в нескольких файлах (трюк с использованием typeA = typeB имеет только область видимости файла) и т. д.
Джульетта
4
Возможно, вы захотите прочитать stackoverflow.com/questions/1082311/… , в котором немного больше объясняется о структурах и почему их следует ограничивать определенным размером. Если вы хотите использовать наследование в структуре, вам, вероятно, следует использовать класс.
Джастин
1
И вы, возможно, захотите прочитать stackoverflow.com/questions/1222935/…, поскольку он подробно описывает, почему это просто невозможно сделать на платформе dotNet. Они сделали это методом C ++ с теми же проблемами, которые могут иметь катастрофические последствия для управляемой платформы.
Dykam 03
Классы @Justin имеют затраты на производительность, которых можно избежать с помощью структур. И в разработке игр это действительно важно. Так что в некоторых случаях вам не следует использовать класс, если вы можете помочь.
Гэвин Уильямс
@Dykam Думаю, это можно сделать на C #. Катастрофа - это преувеличение. Я могу написать ужасный код на C # сегодня, когда я не знаком с техникой. Так что это не проблема. Если наследование структур может решить некоторые проблемы и обеспечить лучшую производительность в определенных сценариях, то я полностью за это.
Гэвин Уильямс

Ответы:

121

Типы значений не могут поддерживать наследование по причине массивов.

Проблема в том, что из соображений производительности и сборки мусора массивы типов значений хранятся «встроенными». Например, 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[]может быть преобразован в a 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!

Попробуйте отладить ТО !

jonp
источник
11
Разумным решением этой проблемы было бы запретить преобразование формы Base [] в Detived []. Точно так же, как преобразование из short [] в int [] запрещено, хотя преобразование из short в int возможно.
Ники
3
+ ответ: проблема с наследованием меня не волновала, пока не выразился в терминах массивов. Другой пользователь заявил, что эту проблему можно смягчить, «нарезав» структуры до нужного размера, но я считаю, что нарезка является причиной большего количества проблем, чем решает.
Джульетта
4
Да, но это «имеет смысл», потому что преобразования массивов предназначены для неявных преобразований, а не для явных преобразований. short в int возможно, но требует приведения, поэтому разумно, что short [] нельзя преобразовать в int [] (за исключением кода преобразования, например 'a.Select (x => (int) x) .ToArray ( ) '). Если среда выполнения запрещает приведение из базового в производное, это будет «бородавка», поскольку это разрешено для ссылочных типов. Таким образом, у нас есть два возможных "недостатка" - запретить наследование структур или запретить преобразование производных массивов в массивы базовых.
jonp 03
3
По крайней мере, предотвращая наследование структур, у нас есть отдельное ключевое слово, и мы можем легче сказать, что «структуры особенные», в отличие от «случайных» ограничений в том, что работает для одного набора вещей (классов), но не для другого (структур) , Я полагаю, что ограничение структуры гораздо легче объяснить («они разные!»).
jonp 03
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 03
1
Рассмотрим массивы наследуемых структур и помните, что C # - это язык, управляемый (памятью). Нарезка или любой аналогичный вариант нанесет ущерб основам CLR.
Kenan EK
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
То, что проблему можно решить, не означает, что ее нужно решать. Иногда лучше избегать ситуаций, в которых возникает проблема.
Дэн Дипло
@ kek444: наличие структуры Fooнаследования Barне должно позволять Fooприсваивать a Bar, но объявление структуры таким образом может дать несколько полезных эффектов: (1) Создайте член типа со специальным именем в Barкачестве первого элемента Fooи Fooвключите имена членов, которые являются псевдонимами этих членов Bar, позволяя код, который раньше Barбыл адаптирован для использования Fooвместо него, без необходимости заменять все ссылки на thing.BarMemberна thing.theBar.BarMember, и сохраняя возможность читать и записывать все Barполя как группу; ...
supercat
14

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

Мартин Ливерсаж
источник
8
не нужно использовать полиморфизм, чтобы воспользоваться преимуществом наследования
rmeador 03
Итак, сколько различных типов наследования будет в .NET?
Джон Сондерс,
Полиморфизм действительно существует в структурах, просто рассмотрите разницу между вызовом ToString (), когда вы реализуете его в настраиваемой структуре, или когда настраиваемая реализация ToString () не существует.
Kenan EK
Это потому, что все они являются производными от System.Object. Это больше полиморфизм типа System.Object, чем структур.
Джон Сондерс,
Полиморфизм может иметь смысл со структурами, используемыми в качестве параметров универсального типа. Полиморфизм работает со структурами, реализующими интерфейсы; Самая большая проблема с интерфейсами заключается в том, что они не могут предоставлять byrefs для полей структуры. В противном случае, самое важное, что я думаю, было бы полезно, поскольку "наследование" структур могло бы быть средством того, чтобы тип (структура или класс) Foo, имеющий поле типа структуры, Barмог рассматривать Barчлены как свои собственные, так что Point3dкласс может , например , инкапсулировать Point2d xyно относятся к Xэтой области или как xy.Xили X.
supercat 01
8

Вот что говорят документы :

Структуры особенно полезны для небольших структур данных, имеющих семантику значений. Комплексные числа, точки в системе координат или пары ключ-значение в словаре - все это хорошие примеры структур. Ключом к этим структурам данных является то, что у них мало членов данных, что они не требуют использования наследования или ссылочной идентичности и что они могут быть удобно реализованы с использованием семантики значений, когда присваивание копирует значение вместо ссылки.

По сути, они должны содержать простые данные и, следовательно, не имеют «дополнительных функций», таких как наследование. Вероятно, для них было бы технически возможно поддерживать какой-то ограниченный тип наследования (не полиморфизм из-за того, что они находятся в стеке), но я считаю, что это также выбор дизайна, чтобы не поддерживать наследование (как и многие другие вещи в .NET языки есть.)

С другой стороны, я согласен с преимуществами наследования, и я думаю, что все мы достигли точки, когда хотим, чтобы нас structунаследовали от другого, и понимаем, что это невозможно. Но на этом этапе структура данных, вероятно, настолько развита, что в любом случае это должен быть класс.

Blixt
источник
4
Это не причина отсутствия наследования.
Dykam 03
Я считаю, что наследование, о котором здесь говорится, не может использовать две структуры, где одна наследуется от другой взаимозаменяемо, но повторное использование и добавление к реализации одной структуры в другую (т.е. создание a Point3Dиз Point2D; вы не сможете использовать Point3Dвместо a Point2D, но вам не придется заново реализовывать Point3Dполностью с нуля.) В любом случае я так интерпретировал ...
Бликст 03
Вкратце: он может поддерживать наследование без полиморфизма. Это не так. Я считаю , что это выбор дизайна , чтобы помочь человеку выбрать classболее , structкогда это необходимо.
Blixt 03
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.

Dykam
источник
2
C ++ отлично справляется с этим случаем, IIRC. Экземпляр B нарезается по размеру A. Если это чистый тип данных, как структуры .NET, то ничего плохого не произойдет. У вас действительно есть небольшая проблема с методом, который возвращает A, и вы сохраняете это возвращаемое значение в B, но этого нельзя допускать. Короче говоря, дизайнеры .NET могли бы справиться с этим, если бы захотели, но по какой-то причине они этого не сделали.
rmeador 03
1
Для вашего DoSomething () вряд ли возникнет проблема, поскольку (при условии семантики C ++) 'b' будет "разрезано" для создания экземпляра A. Проблема с <i> массивами </i>. Рассмотрим существующие структуры A и B и метод <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Поскольку массивы типов значений хранят значения «встроенными», DoSomething (actual = new B [2] {}) приведет к установке фактического [0] .childproperty, а не фактического [1] .property. Это плохо.
jonp 03
2
@John: Я не утверждал, что это было так, и я не думаю, что @jonp тоже. Мы просто упомянули, что эта проблема старая и уже решена, поэтому разработчики .NET решили не поддерживать ее по какой-то причине, кроме технической невозможности.
rmeador 03
Следует отметить, что проблема «массивов производных типов» не нова для C ++; см. parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Массивы в C ++ - зло! ;-)
jonp 03
@John: решение проблемы «массивы производных типов и базовых типов не смешиваются», как обычно, заключается в том, чтобы этого не делать. Вот почему массивы в C ++ являются злом (легче допускать повреждение памяти) и почему .NET не поддерживает наследование с типами значений (компилятор и JIT гарантируют, что этого не произойдет).
jonp 03
3

Есть момент, который я хотел бы исправить. Несмотря на то, что структуры не могут быть унаследованы, потому что они живут в стеке, это правильное объяснение, в то же время это наполовину правильное объяснение. Структуры, как и любой другой тип значения, могут находиться в стеке. Поскольку это будет зависеть от того, где объявлена ​​переменная, они будут находиться либо в стеке, либо в куче . Это будет, если они являются локальными переменными или полями экземпляра соответственно.

Сказав это, Сесил получил имя правильно.

Я хотел бы подчеркнуть, что типы значений могут жить в стеке. Это не значит, что они всегда так делают. Локальные переменные, включая параметры метода, будут. Все остальные не будут. Тем не менее, это все еще причина, по которой они не могут быть переданы по наследству. :-)

Руи Кравейро
источник
3

Структуры размещаются в стеке. Это означает, что семантика значений в значительной степени бесплатна, а доступ к членам структуры очень дешев. Это не предотвращает полиморфизм.

Каждая структура может начинаться с указателя на ее таблицу виртуальных функций. Это было бы проблемой производительности (каждая структура была бы размером как минимум с указатель), но это выполнимо. Это позволит использовать виртуальные функции.

А что насчет добавления полей?

Что ж, когда вы выделяете структуру в стеке, вы выделяете определенное количество места. Требуемое пространство определяется во время компиляции (заранее или при JIT). Если вы добавляете поля, а затем присваиваете базовый тип:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Это перезапишет неизвестную часть стека.

В качестве альтернативы среда выполнения может предотвратить это, записав только байтов sizeof (A) в любую переменную A.

Что произойдет, если B переопределит метод в A и ссылается на его поле Integer2? Либо среда выполнения выдает исключение MemberAccessException, либо метод вместо этого обращается к некоторым случайным данным в стеке. Ни то, ни другое недопустимо.

Совершенно безопасно иметь наследование структур, если вы не используете структуры полиморфно или пока вы не добавляете поля при наследовании. Но это не очень полезно.

user38001
источник
1
Почти. Никто больше не упоминал о проблеме нарезки в отношении стека, только в отношении массивов. И никто больше не упомянул доступные решения.
user38001 03
1
Все типы значений в .net заполняются нулями при создании, независимо от их типа или полей, которые они содержат. Для добавления в структуру чего-то вроде указателя vtable потребуются средства инициализации типов ненулевыми значениями по умолчанию. Такая функция может быть полезна для различных целей, и в большинстве случаев ее реализация может быть не слишком сложной, но ничего подобного в .net не существует.
supercat
@ user38001 «Структуры размещаются в стеке» - если они не являются полями экземпляра, и в этом случае они размещаются в куче.
Дэвид Клемпфнер
2

Это кажется очень частым вопросом. Мне хочется добавить, что типы значений хранятся «на месте», где вы объявляете переменную; Помимо деталей реализации, это означает, что нет заголовка объекта, который что-то говорит об объекте, только переменная знает, какие данные там находятся.

У Сесила есть имя
источник
Компилятор знает, что там. Ссылка на C ++ не может быть ответом.
Хенк Холтерман
Откуда вы пришли к C ++? Я бы сказал на месте, потому что это то, что лучше всего соответствует поведению, стек - это деталь реализации, если цитировать статью в блоге MSDN.
Сесила есть имя
Да, упоминание C ++ - это плохо, просто ход моих мыслей. Но помимо вопроса о том, нужна ли информация о времени выполнения, почему у структур не должно быть «заголовка объекта»? Компилятор может смешать их как угодно. Он мог даже скрыть заголовок в структуре [Structlayout].
Хенк Холтерман,
Поскольку структуры являются типами значений, это не обязательно с заголовком объекта, потому что среда выполнения всегда копирует содержимое, как и для других типов значений (ограничение). Это не имело бы смысла с заголовком, потому что для него предназначены классы ссылочного типа: P
Сесила есть имя
1

Структуры поддерживают интерфейсы, поэтому таким образом можно делать некоторые полиморфные вещи.

Уятт Барнетт
источник
0

IL - это стековый язык, поэтому вызов метода с аргументом происходит примерно так:

  1. Поместите аргумент в стек
  2. Вызовите метод.

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

Если это указатель ссылочного типа, тогда метод ищет объект в куче и получает дескриптор его типа, который указывает на таблицу методов, которая обрабатывает этот конкретный метод для этого точного типа. Если это тип значения, то поиск в таблице методов не требуется, поскольку типы значений не поддерживают наследование, поэтому существует только одна возможная комбинация метода / типа.

Если типы значений поддерживают наследование, тогда возникнут дополнительные накладные расходы, так как конкретный тип структуры должен будет помещаться в стек вместе с ее значением, что будет означать своего рода поиск в таблице методов для конкретного конкретного экземпляра типа. Это устранило бы преимущества типов значений в скорости и эффективности.

Мэтт Хауэллс
источник
1
C ++ решил это, прочтите этот ответ, чтобы узнать о реальной проблеме: stackoverflow.com/questions/1222935/…
Dykam 03