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

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

какая техническая причина мешает структурам наследовать от других структур?

10 ответов


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

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

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

у нас проблема, потому что Square() ничего не знает о Derived, он будет использовать только арифметику указателя для доступа к каждому элементу массива, увеличивая на постоянную величину (sizeof(A)). Собрание будет смутно напоминать:--16-->

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?

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


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

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

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

С другой стороны, я согласен с преимуществами наследования, и я думаю, что мы все попали в точку, где мы хотим, чтобы наши 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);

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


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

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

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


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

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

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


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


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


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

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

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

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

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