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

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

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

10 ответов

Решение

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

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

По обычным правилам конвертации, 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?

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

Вот что говорят доктора:

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

По сути, они должны содержать простые данные и поэтому не имеют "дополнительных функций", таких как наследование. Вероятно, для них технически возможно поддерживать ограниченный тип наследования (не полиморфизм из-за того, что они находятся в стеке), но я считаю, что это также выбор дизайна, чтобы не поддерживать наследование (как и многие другие вещи в.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);

Пространство выделено для размера 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 - это основанный на стеке язык, поэтому вызов метода с аргументом выглядит примерно так:

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

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

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

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

Другие вопросы по тегам