C # не позволяет структурам быть производными от классов, но все ValueTypes являются производными от Object. Где проводится это различие?
Как с этим справляется среда CLR?
C # не позволяет структурам быть производными от классов, но все ValueTypes являются производными от Object. Где проводится это различие?
Как с этим справляется среда CLR?
Ответы:
C # не позволяет структурам быть производными от классов
Ваше утверждение неверно, отсюда и ваше замешательство. C # действительно позволяет структурам быть производными от классов. Все структуры являются производными от одного и того же класса System.ValueType, который является производным от System.Object. И все перечисления являются производными от System.Enum.
ОБНОВЛЕНИЕ: в некоторых (теперь удаленных) комментариях была некоторая путаница, которая требует разъяснения. Я задам еще несколько вопросов:
Являются ли структуры производными от базового типа?
Ясно да. Мы можем убедиться в этом, прочитав первую страницу спецификации:
Все типы C #, включая примитивные типы, такие как int и double, наследуются от одного корневого типа объекта.
Теперь я отмечаю, что спецификация здесь преувеличивает. Типы указателей не являются производными от объекта, и отношение производных для типов интерфейса и типов параметров типа более сложное, чем показано в этом эскизе. Однако очевидно, что все типы структур являются производными от базового типа.
Есть ли другие способы узнать, что типы структур являются производными от базового типа?
Конечно. Тип структуры может переопределить ToString
. Что это замещающее, если не виртуальный метод своего базового типа? Следовательно, он должен иметь базовый тип. Этот базовый тип - это класс.
Могу ли я получить определяемую пользователем структуру из выбранного мной класса?
Точно нет. Это не означает, что структуры не являются производными от класса . Структуры являются производными от класса и, таким образом, наследуют наследуемые члены этого класса. Фактически, структуры должны быть производными от определенного класса: перечисления должны быть производными Enum
, а структуры - производными ValueType
. Поскольку они необходимы , язык C # запрещает вам указывать производное отношение в коде.
Зачем это запрещать?
Когда отношения требуются , язык дизайнер имеет варианты: (1) требовать от пользователя ввести требуемое колдовство, (2) сделать его необязательным, или (3) запретить его. У каждого есть свои плюсы и минусы, и разработчики языка C # сделали выбор по-разному в зависимости от конкретных деталей каждого из них.
Например, константные поля должны быть статическими, но запрещено говорить так, потому что это, во-первых, бессмысленное словоблудие, а во-вторых, подразумевает, что существуют нестатические константные поля. Но перегруженные операторы необходимо помечать как статические, даже если у разработчика нет выбора; В противном случае разработчикам слишком легко поверить, что перегрузка оператора является методом экземпляра. Это перевешивает опасения, что пользователь может поверить в то, что «статика» подразумевает, что, скажем, «виртуальный» также возможен.
В этом случае требование, чтобы пользователь сказал, что его структура является производной от ValueType, кажется простым лишним словоблудием и подразумевает, что структура может быть производной от другого типа. Чтобы устранить обе эти проблемы, C # запрещает указывать в коде, что структура является производной от базового типа, хотя явно это так.
Точно так же все типы делегатов являются производными MulticastDelegate
, но C # требует, чтобы вы этого не говорили.
Итак, теперь мы установили, что все структуры в C # являются производными от класса .
Какая связь между наследованием и производным от класса ?
Многих смущают отношения наследования в C #. Отношения наследования довольно просты: если структура, класс или тип делегата D является производным от типа класса B, то наследуемые члены B также являются членами D. Это так просто.
Что означает наследование, когда мы говорим, что структура является производной от ValueType? Просто все наследуемые члены ValueType также являются членами структуры. Вот как структуры получают свою реализацию ToString
, например; он наследуется от базового класса структуры.
Все наследуемые члены? Конечно нет. Передаются ли частные члены по наследству?
Да. Все частные члены базового класса также являются членами производного типа. Конечно, незаконно называть этих участников по имени, если сайт вызова не находится в домене доступности участника. То, что у вас есть участник, не означает, что вы можете им пользоваться!
Теперь продолжим исходный ответ:
Как с этим справляется среда CLR?
Очень хорошо. :-)
Что делает тип значения типом значения, так это то, что его экземпляры копируются по значению . Что делает ссылочный тип ссылочным типом, так это то, что его экземпляры копируются по ссылке . Кажется, у вас есть некоторое убеждение, что отношения наследования между типами значений и ссылочными типами являются чем-то особенным и необычным, но я не понимаю, что это за вера. Наследование не имеет ничего общего с тем, как что-то копируется.
Рассмотрим этот вариант. Предположим, я рассказал вам следующие факты:
Есть два вида ящиков: красные и синие.
Каждый красный квадрат пуст.
Есть три специальных синих прямоугольника, которые называются O, V и E.
О нет ни в одной коробке.
V находится внутри O.
E находится внутри V.
Никакого другого синего ящика внутри V.
Внутри E. нет синей коробки.
Каждый красный квадрат находится либо в V, либо в E.
Каждый синий квадрат, кроме O, находится внутри синего квадрата.
Синие прямоугольники - ссылочные типы, красные прямоугольники - типы значений, O - System.Object, V - System.ValueType, E - System.Enum, а «внутреннее» отношение - «производное от».
Это совершенно последовательный и простой набор правил, который вы могли бы легко реализовать самостоятельно, если бы у вас было много картона и много терпения. Красный или синий ящик не имеет ничего общего с тем, что он внутри; в реальном мире вполне возможно поместить красную коробку в синюю. В среде CLR совершенно законно создать тип значения, наследующий от ссылочного типа, если это либо System.ValueType, либо System.Enum.
Итак, давайте перефразируем ваш вопрос:
Как ValueTypes происходит от Object (ReferenceType) и по-прежнему остается ValueTypes?
в виде
Как это возможно, что каждое красное поле (типы значений) находится внутри (происходит от) поля O (System.Object), которое является синим прямоугольником (ссылочный тип) и по-прежнему является красным прямоугольником (тип значения)?
Когда вы это формулируете так, надеюсь, это очевидно. Ничто не мешает вам поместить красную коробку внутри коробки V, которая находится внутри синей коробки O. Почему это могло быть?
ДОПОЛНИТЕЛЬНОЕ ОБНОВЛЕНИЕ:
Первоначальный вопрос Джоан был о том, как это возможночто тип значения является производным от ссылочного типа. Мой первоначальный ответ на самом деле не объяснял ни один из механизмов, которые CLR использует для учета того факта, что у нас есть производное отношение между двумя вещами, которые имеют совершенно разные представления, а именно, имеют ли упомянутые данные заголовок объекта, a блок синхронизации, владеет ли он собственным хранилищем для сбора мусора и т. д. Эти механизмы сложны, слишком сложны, чтобы объяснить их одним ответом. Правила системы типов CLR несколько сложнее, чем несколько упрощенный вариант, который мы видим в C #, где, например, нет четкого различия между упакованными и неупакованными версиями типа. Введение универсальных шаблонов также привело к значительному усложнению среды CLR.
Небольшая поправка: C # не позволяет настраивать структуры, производные от чего-либо, а не только от классов. Все, что может сделать структура, - это реализовать интерфейс, который сильно отличается от производного.
Я думаю, что лучший способ ответить на этот вопрос - это ValueType
особенное. По сути, это базовый класс для всех типов значений в системе типов CLR. Трудно понять, как ответить «как CLR это обрабатывает», потому что это просто правило CLR.
ValueType
это особенный ValueType
тип , но стоит прямо упомянуть, что он на самом деле является ссылочным типом.
Это несколько искусственная конструкция, поддерживаемая CLR, чтобы все типы можно было рассматривать как System.Object.
Типы значений являются производными от System.Object через System.ValueType , где происходит специальная обработка (например, CLR обрабатывает упаковку / распаковку и т. Д. Для любого типа, производного от ValueType).
Ваше утверждение неверно, отсюда и ваше замешательство. C # действительно позволяет структурам быть производными от классов. Все структуры являются производными от одного и того же класса System.ValueType.
Итак, давайте попробуем это:
struct MyStruct : System.ValueType
{
}
Это даже не будет компилироваться. Компилятор напомнит вам: «Введите System.ValueType в списке интерфейсов, это не интерфейс».
При декомпиляции Int32, который является структурой, вы обнаружите:
public struct Int32: IComparable, IFormattable, IConvertible {}, не говоря уже о том, что она является производной от System.ValueType. Но в обозревателе объектов вы обнаруживаете, что Int32 наследуется от System.ValueType.
Итак, все это заставляет меня поверить:
Я думаю, что лучший способ ответить на этот вопрос - это особенность ValueType. По сути, это базовый класс для всех типов значений в системе типов CLR. Трудно понять, как ответить «как CLR справляется с этим», потому что это просто правило CLR.
ValueType
, оно использует это для определения двух типов объектов: типа объекта кучи, который ведет себя как ссылочный тип и как тип места хранения, который фактически находится вне системы наследования типов. Поскольку эти два типа вещей используются во взаимоисключающих контекстах, одни и те же дескрипторы типов могут использоваться для обоих. На уровне CLR структура определяется как класс, родительский System.ValueType
System.ValueType
), и запрещает классам указывать, что они наследуют, System.ValueType
потому что любой класс, который был объявлен таким образом, будет вести себя как тип значения.
Тип значения в штучной упаковке фактически является ссылочным типом (он ходит как один и крякает как один, так что фактически это один). Я бы предположил, что ValueType на самом деле не является базовым типом типов значений, а скорее является базовым ссылочным типом, в который типы значений могут быть преобразованы при приведении к типу Object. Сами типы значений без упаковки находятся за пределами иерархии объектов.
Из всех ответов ответ @supercat ближе всего к фактическому ответу. Поскольку другие ответы на самом деле не отвечают на вопрос и прямо делают неверные утверждения (например, что типы значений наследуются от чего-либо), я решил ответить на вопрос.
Этот ответ основан на моем собственном реверс-инжиниринге и спецификации CLI.
struct
и class
являются ключевыми словами C #. Что касается CLI, все типы (классы, интерфейсы, структуры и т. Д.) Определяются определениями классов.
Например, тип объекта (известный в C # как class
) определяется следующим образом:
.class MyClass
{
}
Интерфейс определяется определением класса с interface
семантическим атрибутом:
.class interface MyInterface
{
}
Причина, по которой структуры могут наследовать от System.ValueType
типов значений и по-прежнему быть типами значений, заключается в том, что ... они этого не делают.
Типы значений - это простые структуры данных. Типы значений не наследуются ни от чего и не могут реализовывать интерфейсы. Типы значений не являются подтипами какого-либо типа и не имеют никакой информации о типе. Учитывая адрес памяти типа значения, невозможно определить, что представляет собой тип значения, в отличие от ссылочного типа, который имеет информацию о типе в скрытом поле.
Если представить себе следующую структуру C #:
namespace MyNamespace
{
struct MyValueType : ICloneable
{
public int A;
public int B;
public int C;
public object Clone()
{
// body omitted
}
}
}
Ниже приводится определение этой структуры в классе IL:
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable
{
.field public int32 A;
.field public int32 B;
.field public int32 C;
.method public final hidebysig newslot virtual instance object Clone() cil managed
{
// body omitted
}
}
Так что здесь происходит? Он явно расширяется System.ValueType
, что является типом объекта / ссылки, и реализует System.ICloneable
.
Объяснение заключается в том, что когда определение класса расширяется, System.ValueType
оно фактически определяет 2 вещи: тип значения и соответствующий тип значения в штучной упаковке. Члены определения класса определяют представление как для типа значения, так и для соответствующего упакованного типа. Расширяется и реализуется не тип значения, а соответствующий тип в штучной упаковке. В extends
и implements
ключевых словах относятся только к коробочному типу.
Чтобы уточнить, определение класса выше выполняет 2 вещи:
System.ValueType
и реализующий его System.ICloneable
.Также обратите внимание, что любое расширение определения класса System.ValueType
также внутренне запечатано, независимо от того, указано sealed
ключевое слово или нет.
Поскольку типы значений представляют собой простые структуры, не наследуются, не реализуются и не поддерживают полиморфизм, они не могут использоваться с остальной системой типов. Чтобы обойти это, поверх типа значения CLR также определяет соответствующий ссылочный тип с теми же полями, известный как упакованный тип. Таким образом, хотя тип значения не может быть передан методам, принимающим object
, его соответствующий тип в штучной упаковке может .
Теперь, если бы вы определили метод на C #, например
public static void BlaBla(MyNamespace.MyValueType x)
,
вы знаете, что метод примет тип значения MyNamespace.MyValueType
.
Выше мы узнали, что определение класса, struct
являющееся результатом ключевого слова в C #, на самом деле определяет как тип значения, так и тип объекта. Однако мы можем ссылаться только на определенный тип значения. Несмотря на то, что спецификация CLI заявляет, что ключевое слово ограничения boxed
может использоваться для ссылки на упакованную версию типа, этого ключевого слова не существует (см. ECMA-335, II.13.1 Ссылки на типы значений). Но давайте на мгновение представим, что это так.
При обращении к типам в IL поддерживается несколько ограничений, среди которых class
и valuetype
. Если мы используем, valuetype MyNamespace.MyType
мы указываем определение класса типа значения с именем MyNamespace.MyType. Точно так же мы можем использовать class MyNamespace.MyType
определение класса объекта типа MyNamespace.MyType. Это означает, что в IL вы можете иметь тип значения (структуру) и тип объекта (класс) с одним и тем же именем и при этом различать их. Теперь, если бы boxed
ключевое слово, указанное в спецификации CLI, было действительно реализовано, мы могли бы использовать boxed MyNamespace.MyType
для указания упакованного типа определения класса типа значения под названием MyNamespace.MyType.
Итак, .method static void Print(valuetype MyNamespace.MyType test) cil managed
принимает тип значения, определенный определением класса типа значения с именем MyNamespace.MyType
,
while .method static void Print(class MyNamespace.MyType test) cil managed
принимает тип объекта, определенный определением класса типа объекта с именем MyNamespace.MyType
.
аналогично, если бы boxed
было ключевое слово, .method static void Print(boxed MyNamespace.MyType test) cil managed
примет упакованный тип типа значения, определенного определением класса с именем MyNamespace.MyType
.
Вы бы тогда быть в состоянии создать экземпляр упакованного типа , как и любой другой тип объекта и передавать его любой способ , который принимает System.ValueType
, object
или в boxed MyNamespace.MyValueType
качестве аргумента, и это было бы, для всех намерений и целей, работа , как и любого другого ссылочного типа. Это НЕ тип значения, а соответствующий тип значения в рамке.
Итак, подведем итог и ответим на вопрос:
Типы значений не являются ссылочными типами и не наследуются от System.ValueType
любого другого типа, и они не могут реализовывать интерфейсы. Соответствующие упакованные типы, которые также определены , наследуются от интерфейсов System.ValueType
и могут реализовывать их.
.class
Определение определяет различные действия в зависимости от обстоятельств.
interface
семантический атрибут указан, определение класса определяет интерфейс.interface
семантический атрибут не указан и определение не расширяется System.ValueType
, определение класса определяет тип объекта (класс).interface
семантический атрибут не указан, а определение действительно распространяется System.ValueType
, определение класса определяет тип значения и его соответствующий коробочный тип (структура).В этом разделе предполагается 32-разрядный процесс
Как уже упоминалось, типы значений не имеют информации о типе, и поэтому невозможно определить, что представляет собой тип значения, по его местоположению в памяти. Структура описывает простой тип данных и содержит только те поля, которые она определяет:
public struct MyStruct
{
public int A;
public short B;
public int C;
}
Если мы представим, что экземпляр MyStruct был размещен по адресу 0x1000, то это макет памяти:
0x1000: int A;
0x1004: short B;
0x1006: 2 byte padding
0x1008: int C;
По умолчанию структуры используют последовательный макет. Поля выравниваются по границам своего размера. Для этого добавлены отступы.
Если мы определим класс точно так же, как:
public class MyClass
{
public int A;
public short B;
public int C;
}
Представляя тот же адрес, структура памяти выглядит следующим образом:
0x1000: Pointer to object header
0x1004: int A;
0x1008: int C;
0x100C: short B;
0x100E: 2 byte padding
0x1010: 4 bytes extra
По умолчанию для классов используется автоматическая компоновка, и JIT-компилятор упорядочит их в наиболее оптимальном порядке. Поля выравниваются по границам своего размера. Для этого добавлены отступы. Не знаю почему, но у каждого класса всегда есть дополнительные 4 байта в конце.
Смещение 0 содержит адрес заголовка объекта, который содержит информацию о типе, таблицу виртуальных методов и т. Д. Это позволяет среде выполнения определять, что представляют данные по адресу, в отличие от типов значений.
Таким образом, типы значений не поддерживают наследование, интерфейсы и полиморфизм.
Типы значений не имеют таблиц виртуальных методов и, следовательно, не поддерживают полиморфизм. Однако их соответствующий тип в штучной упаковке делает .
Когда у вас есть экземпляр структуры и вы пытаетесь вызвать виртуальный метод, подобный ToString()
определенному System.Object
, среда выполнения должна поместить эту структуру в коробку.
MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
Однако, если структура переопределяет, ToString()
вызов будет статически привязан, и среда выполнения будет вызывать MyStruct.ToString()
без упаковки и без просмотра каких-либо таблиц виртуальных методов (у структур их нет). По этой причине он также может встроить ToString()
вызов.
Если структура переопределяет ToString()
и помещена в рамку, то вызов будет разрешен с использованием таблицы виртуальных методов.
System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct.
Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
Однако помните, что ToString()
это определено в структуре и, следовательно, работает со значением структуры, поэтому ожидает тип значения. Упакованный тип, как и любой другой класс, имеет заголовок объекта. Если ToString()
метод, определенный в структуре, был вызван непосредственно с упакованным типом в this
указателе, при попытке доступа к полю A
в MyStruct
он получил бы доступ к смещению 0, которое в упакованном типе было бы указателем заголовка объекта. Таким образом, у упакованного типа есть скрытый метод, который фактически переопределяет ToString()
. Этот скрытый метод распаковывает (только вычисление адреса, как и unbox
инструкция IL) упакованный тип, а затем статически вызывает ToString()
определенный в структуре.
Точно так же упакованный тип имеет скрытый метод для каждого реализованного метода интерфейса, который выполняет ту же распаковку, а затем статически вызывает метод, определенный в структуре.
I.8.2.4 Для каждого типа значения CTS определяет соответствующий ссылочный тип, называемый упакованным типом. Обратное неверно: как правило, ссылочные типы не имеют соответствующего типа значения. Представление значения в штучной упаковке (значение в штучной упаковке) - это место, где может быть сохранено значение типа значения. Упакованный тип - это тип объекта, а упакованное значение - это объект.
I.8.9.7 Не все типы, определенные определением класса, являются объектными (см. §I.8.2.3); в частности, типы значений не являются объектными типами, но они определяются с использованием определения класса. Определение класса для типа значения определяет как (распакованный) тип значения, так и связанный с ним упакованный тип (см. §I.8.2.4). Члены определения класса определяют представление обоих.
II.10.1.3 Семантические атрибуты типа определяют, должен ли быть определен тип интерфейса, класса или значения. Атрибут interface определяет интерфейс. Если этот атрибут отсутствует и определение расширяет (прямо или косвенно) System.ValueType, а определение не для System.Enum, должен быть определен тип значения (§II.13). В противном случае должен быть определен класс (§II.11).
I.8.9.10 В распакованной форме типы значений не наследуются ни от одного типа. Типы значений в штучной упаковке должны наследовать непосредственно от System.ValueType, если они не являются перечислениями; в этом случае они должны наследоваться от System.Enum. Типы значений в штучной упаковке должны быть запечатаны.
II.13 Распакованные типы значений не считаются подтипами другого типа, и недопустимо использовать инструкцию isinst (см. Раздел III) для распакованных типов значений. Однако инструкция isinst может использоваться для упакованных типов значений.
I.8.9.10 Тип значения не наследуется; скорее базовый тип, указанный в определении класса, определяет базовый тип упакованного типа.
I.8.9.7 Типы значений не поддерживают контракты интерфейса, но связанные с ними упакованные типы поддерживают.
II.13 Типы значений должны реализовывать ноль или более интерфейсов, но это имеет значение только в их коробочной форме (§II.13.3).
I.8.2.4 Интерфейсы и наследование определены только для ссылочных типов. Таким образом, хотя определение типа значения (§I.8.9.7) может определять оба интерфейса, которые должны быть реализованы типом значения и классом (System.ValueType или System.Enum), от которого оно наследуется, они применяются только к упакованным значениям. .
II.13.1 На распакованную форму типа значения следует ссылаться с помощью ключевого слова valuetype, за которым следует ссылка на тип. Для ссылки на упакованную форму типа значения следует использовать ключевое слово в рамке, за которым следует ссылка на тип.
Примечание: спецификация здесь неверна, boxed
ключевого слова нет .
Я думаю, что отчасти путаница в том, как типы значений наследуются, проистекает из того факта, что C # использует синтаксис приведения для выполнения упаковки и распаковки, из-за чего кажется, что вы выполняете приведение, что на самом деле не CLR выдаст исключение InvalidCastException при попытке распаковать неправильный тип).
(object)myStruct
в C # создает новый экземпляр упакованного типа типа значения; он не выполняет никаких приведений. Точно так же (MyStruct)obj
в C # распаковывает упакованный тип, копируя часть значения; он не выполняет никаких приведений.
System.ValueType
типа в системе типов CLR.