it-swarm.com.ru

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

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

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

117
Juliet

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

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

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

118
jonp

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

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?
66
Kenan E. K.

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

14
Martin Liversage

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

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

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

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

8
Blixt

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

3
Dykam

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

Говоря это, у Сесила Имя есть это правильно.

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

3
Rui Craveiro

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

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

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

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

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

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

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

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

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

3
user38001

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

2
Cecil Has a Name

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

1
Wyatt Barnett

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

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

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

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

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

0
Matt Howells