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

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

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

Ответ 1

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

Проблема заключается в том, что для характеристик производительности и GC массивы типов значений хранятся "inline". Например, если задано new FooType[10] {...}, если FooType является ссылочным типом, в управляемой куче будет создано 11 объектов (один для массива и 10 для каждого экземпляра типа). Если FooType - это тип значения, в управляемой куче будет создан только один экземпляр - для самого массива (поскольку каждое значение массива будет храниться "inline" с массивом).

Теперь предположим, что у нас есть наследование типов значений. В сочетании с описанным выше "встроенным хранилищем" поведение массивов происходит "Bad Things", как можно видеть в С++.

Рассмотрим этот псевдо-код С#:

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 - типы значений, а массивы хранят значения inline, тогда у нас есть проблема.

У нас есть проблема, потому что 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!

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

Ответ 2

Представьте, что наследование поддерживается на основе структур. Затем объявив:

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?

Ответ 3

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

Ответ 4

Здесь документы говорят:

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

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

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

Ответ 5

Класс, подобный наследованию, невозможен, поскольку структура укладывается непосредственно в стек. Наследующая структура будет больше, чем родительская, но 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);

Пространство выделяется для sizeof A, а не для sizeof B.

Ответ 6

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

Говоря это, Сесил имеет имя, пригвоздив его правильно.

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

Ответ 7

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

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

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

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

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

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

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

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

  • Вставьте аргумент в стек
  • Вызвать метод.

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

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

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