Я знаю, что структуры в .NET не поддерживают наследование, но не совсем понятно, почему они ограничены таким образом.
Какая техническая причина препятствует наследованию структур от других структур?
Я знаю, что структуры в .NET не поддерживают наследование, но не совсем понятно, почему они ограничены таким образом.
Какая техническая причина препятствует наследованию структур от других структур?
Ответы:
Типы значений не могут поддерживать наследование по причине массивов.
Проблема в том, что из соображений производительности и сборки мусора массивы типов значений хранятся «встроенными». Например, 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);
По обычным правилам преобразования a Derived[]
может быть преобразован в a 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?
Foo
наследования Bar
не должно позволять Foo
присваивать a Bar
, но объявление структуры таким образом может дать несколько полезных эффектов: (1) Создайте член типа со специальным именем в Bar
качестве первого элемента Foo
и Foo
включите имена членов, которые являются псевдонимами этих членов Bar
, позволяя код, который раньше Bar
был адаптирован для использования Foo
вместо него, без необходимости заменять все ссылки на thing.BarMember
на thing.theBar.BarMember
, и сохраняя возможность читать и записывать все Bar
поля как группу; ...
Структуры не используют ссылки (если они не заключены в рамку, но вы должны попытаться избежать этого), поэтому полиморфизм не имеет смысла, поскольку нет косвенного обращения через указатель ссылки. Объекты обычно находятся в куче и на них ссылаются через ссылочные указатели, но структуры выделяются в стеке (если они не помещены в коробку) или выделяются «внутри» памяти, занятой ссылочным типом в куче.
Foo
, имеющий поле типа структуры, Bar
мог рассматривать Bar
члены как свои собственные, так что Point3d
класс может , например , инкапсулировать Point2d xy
но относятся к X
этой области или как xy.X
или X
.
Вот что говорят документы :
Структуры особенно полезны для небольших структур данных, имеющих семантику значений. Комплексные числа, точки в системе координат или пары ключ-значение в словаре - все это хорошие примеры структур. Ключом к этим структурам данных является то, что у них мало членов данных, что они не требуют использования наследования или ссылочной идентичности и что они могут быть удобно реализованы с использованием семантики значений, когда присваивание копирует значение вместо ссылки.
По сути, они должны содержать простые данные и, следовательно, не имеют «дополнительных функций», таких как наследование. Вероятно, для них было бы технически возможно поддерживать какой-то ограниченный тип наследования (не полиморфизм из-за того, что они находятся в стеке), но я считаю, что это также выбор дизайна, чтобы не поддерживать наследование (как и многие другие вещи в .NET языки есть.)
С другой стороны, я согласен с преимуществами наследования, и я думаю, что все мы достигли точки, когда хотим, чтобы нас struct
унаследовали от другого, и понимаем, что это невозможно. Но на этом этапе структура данных, вероятно, настолько развита, что в любом случае это должен быть класс.
Point3D
из Point2D
; вы не сможете использовать Point3D
вместо a Point2D
, но вам не придется заново реализовывать Point3D
полностью с нуля.) В любом случае я так интерпретировал ...
class
более , 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.
Есть момент, который я хотел бы исправить. Несмотря на то, что структуры не могут быть унаследованы, потому что они живут в стеке, это правильное объяснение, в то же время это наполовину правильное объяснение. Структуры, как и любой другой тип значения, могут находиться в стеке. Поскольку это будет зависеть от того, где объявлена переменная, они будут находиться либо в стеке, либо в куче . Это будет, если они являются локальными переменными или полями экземпляра соответственно.
Сказав это, Сесил получил имя правильно.
Я хотел бы подчеркнуть, что типы значений могут жить в стеке. Это не значит, что они всегда так делают. Локальные переменные, включая параметры метода, будут. Все остальные не будут. Тем не менее, это все еще причина, по которой они не могут быть переданы по наследству. :-)
Структуры размещаются в стеке. Это означает, что семантика значений в значительной степени бесплатна, а доступ к членам структуры очень дешев. Это не предотвращает полиморфизм.
Каждая структура может начинаться с указателя на ее таблицу виртуальных функций. Это было бы проблемой производительности (каждая структура была бы размером как минимум с указатель), но это выполнимо. Это позволит использовать виртуальные функции.
А что насчет добавления полей?
Что ж, когда вы выделяете структуру в стеке, вы выделяете определенное количество места. Требуемое пространство определяется во время компиляции (заранее или при JIT). Если вы добавляете поля, а затем присваиваете базовый тип:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
Это перезапишет неизвестную часть стека.
В качестве альтернативы среда выполнения может предотвратить это, записав только байтов sizeof (A) в любую переменную A.
Что произойдет, если B переопределит метод в A и ссылается на его поле Integer2? Либо среда выполнения выдает исключение MemberAccessException, либо метод вместо этого обращается к некоторым случайным данным в стеке. Ни то, ни другое недопустимо.
Совершенно безопасно иметь наследование структур, если вы не используете структуры полиморфно или пока вы не добавляете поля при наследовании. Но это не очень полезно.
Это кажется очень частым вопросом. Мне хочется добавить, что типы значений хранятся «на месте», где вы объявляете переменную; Помимо деталей реализации, это означает, что нет заголовка объекта, который что-то говорит об объекте, только переменная знает, какие данные там находятся.
Структуры поддерживают интерфейсы, поэтому таким образом можно делать некоторые полиморфные вещи.
IL - это стековый язык, поэтому вызов метода с аргументом происходит примерно так:
Когда метод запускается, он извлекает из стека несколько байтов, чтобы получить свой аргумент. Он точно знает , сколько байтов нужно вывести, потому что аргумент либо является указателем ссылочного типа (всегда 4 байта для 32-битного), либо это тип значения, размер которого всегда точно известен.
Если это указатель ссылочного типа, тогда метод ищет объект в куче и получает дескриптор его типа, который указывает на таблицу методов, которая обрабатывает этот конкретный метод для этого точного типа. Если это тип значения, то поиск в таблице методов не требуется, поскольку типы значений не поддерживают наследование, поэтому существует только одна возможная комбинация метода / типа.
Если типы значений поддерживают наследование, тогда возникнут дополнительные накладные расходы, так как конкретный тип структуры должен будет помещаться в стек вместе с ее значением, что будет означать своего рода поиск в таблице методов для конкретного конкретного экземпляра типа. Это устранило бы преимущества типов значений в скорости и эффективности.