Почему структуры не поддерживают наследование? - PullRequest
117 голосов
/ 03 августа 2009

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

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

Ответы [ 10 ]

118 голосов
/ 03 августа 2009

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

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

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

66 голосов
/ 03 августа 2009

Представьте себе структуры поддерживаемого наследования. Тогда объявив:

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?
14 голосов
/ 03 августа 2009

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

8 голосов
/ 03 августа 2009

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

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

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

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

3 голосов
/ 04 августа 2009

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

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

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

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

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

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

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

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

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

3 голосов
/ 03 августа 2009

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

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

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

3 голосов
/ 03 августа 2009

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

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

1 голос
/ 03 августа 2009

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

0 голосов
/ 03 августа 2009

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...