C # не позволяет структурам быть производными от классов, но все ValueTypes являются производными от Object. Где проводится это различие?
Как с этим справляется среда CLR?
c#
.net
clr
value-type
reference-type
Джоан Венге
источник
источник
System.ValueType
типа в системе типов CLR.Ответы:
Ваше утверждение неверно, отсюда и ваше замешательство. C # действительно позволяет структурам быть производными от классов. Все структуры являются производными от одного и того же класса System.ValueType, который является производным от System.Object. И все перечисления являются производными от System.Enum.
ОБНОВЛЕНИЕ: в некоторых (теперь удаленных) комментариях была некоторая путаница, которая требует разъяснения. Я задам еще несколько вопросов:
Ясно да. Мы можем убедиться в этом, прочитав первую страницу спецификации:
Теперь я отмечаю, что спецификация здесь преувеличивает. Типы указателей не являются производными от объекта, и отношение производных для типов интерфейса и типов параметров типа более сложное, чем показано в этом эскизе. Однако очевидно, что все типы структур являются производными от базового типа.
Конечно. Тип структуры может переопределить
ToString
. Что это замещающее, если не виртуальный метод своего базового типа? Следовательно, он должен иметь базовый тип. Этот базовый тип - это класс.Точно нет. Это не означает, что структуры не являются производными от класса . Структуры являются производными от класса и, таким образом, наследуют наследуемые члены этого класса. Фактически, структуры должны быть производными от определенного класса: перечисления должны быть производными
Enum
, а структуры - производнымиValueType
. Поскольку они необходимы , язык C # запрещает вам указывать производное отношение в коде.Когда отношения требуются , язык дизайнер имеет варианты: (1) требовать от пользователя ввести требуемое колдовство, (2) сделать его необязательным, или (3) запретить его. У каждого есть свои плюсы и минусы, и разработчики языка C # сделали выбор по-разному в зависимости от конкретных деталей каждого из них.
Например, константные поля должны быть статическими, но запрещено говорить так, потому что это, во-первых, бессмысленное словоблудие, а во-вторых, подразумевает, что существуют нестатические константные поля. Но перегруженные операторы необходимо помечать как статические, даже если у разработчика нет выбора; В противном случае разработчикам слишком легко поверить, что перегрузка оператора является методом экземпляра. Это перевешивает опасения, что пользователь может поверить в то, что «статика» подразумевает, что, скажем, «виртуальный» также возможен.
В этом случае требование, чтобы пользователь сказал, что его структура является производной от ValueType, кажется простым лишним словоблудием и подразумевает, что структура может быть производной от другого типа. Чтобы устранить обе эти проблемы, C # запрещает указывать в коде, что структура является производной от базового типа, хотя явно это так.
Точно так же все типы делегатов являются производными
MulticastDelegate
, но C # требует, чтобы вы этого не говорили.Итак, теперь мы установили, что все структуры в C # являются производными от класса .
Многих смущают отношения наследования в C #. Отношения наследования довольно просты: если структура, класс или тип делегата D является производным от типа класса B, то наследуемые члены B также являются членами D. Это так просто.
Что означает наследование, когда мы говорим, что структура является производной от ValueType? Просто все наследуемые члены ValueType также являются членами структуры. Вот как структуры получают свою реализацию
ToString
, например; он наследуется от базового класса структуры.Да. Все частные члены базового класса также являются членами производного типа. Конечно, незаконно называть этих участников по имени, если сайт вызова не находится в домене доступности участника. То, что у вас есть участник, не означает, что вы можете им пользоваться!
Теперь продолжим исходный ответ:
Очень хорошо. :-)
Что делает тип значения типом значения, так это то, что его экземпляры копируются по значению . Что делает ссылочный тип ссылочным типом, так это то, что его экземпляры копируются по ссылке . Кажется, у вас есть некоторое убеждение, что отношения наследования между типами значений и ссылочными типами являются чем-то особенным и необычным, но я не понимаю, что это за вера. Наследование не имеет ничего общего с тем, как что-то копируется.
Рассмотрим этот вариант. Предположим, я рассказал вам следующие факты:
Есть два вида ящиков: красные и синие.
Каждый красный квадрат пуст.
Есть три специальных синих прямоугольника, которые называются 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.
Итак, давайте перефразируем ваш вопрос:
в виде
Когда вы это формулируете так, надеюсь, это очевидно. Ничто не мешает вам поместить красную коробку внутри коробки V, которая находится внутри синей коробки O. Почему это могло быть?
ДОПОЛНИТЕЛЬНОЕ ОБНОВЛЕНИЕ:
Первоначальный вопрос Джоан был о том, как это возможночто тип значения является производным от ссылочного типа. Мой первоначальный ответ на самом деле не объяснял ни один из механизмов, которые CLR использует для учета того факта, что у нас есть производное отношение между двумя вещами, которые имеют совершенно разные представления, а именно, имеют ли упомянутые данные заголовок объекта, a блок синхронизации, владеет ли он собственным хранилищем для сбора мусора и т. д. Эти механизмы сложны, слишком сложны, чтобы объяснить их одним ответом. Правила системы типов CLR несколько сложнее, чем несколько упрощенный вариант, который мы видим в C #, где, например, нет четкого различия между упакованными и неупакованными версиями типа. Введение универсальных шаблонов также привело к значительному усложнению среды CLR.
источник
Небольшая поправка: C # не позволяет настраивать структуры, производные от чего-либо, а не только от классов. Все, что может сделать структура, - это реализовать интерфейс, который сильно отличается от производного.
Я думаю, что лучший способ ответить на этот вопрос - это
ValueType
особенное. По сути, это базовый класс для всех типов значений в системе типов CLR. Трудно понять, как ответить «как CLR это обрабатывает», потому что это просто правило CLR.источник
ValueType
это особенныйValueType
тип , но стоит прямо упомянуть, что он на самом деле является ссылочным типом.Это несколько искусственная конструкция, поддерживаемая CLR, чтобы все типы можно было рассматривать как System.Object.
Типы значений являются производными от System.Object через System.ValueType , где происходит специальная обработка (например, CLR обрабатывает упаковку / распаковку и т. Д. Для любого типа, производного от ValueType).
источник
Итак, давайте попробуем это:
struct MyStruct : System.ValueType { }
Это даже не будет компилироваться. Компилятор напомнит вам: «Введите System.ValueType в списке интерфейсов, это не интерфейс».
При декомпиляции Int32, который является структурой, вы обнаружите:
public struct Int32: IComparable, IFormattable, IConvertible {}, не говоря уже о том, что она является производной от System.ValueType. Но в обозревателе объектов вы обнаруживаете, что Int32 наследуется от System.ValueType.
Итак, все это заставляет меня поверить:
источник
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()
определенный в структуре.Точно так же упакованный тип имеет скрытый метод для каждого реализованного метода интерфейса, который выполняет ту же распаковку, а затем статически вызывает метод, определенный в структуре.
Спецификация CLI
Заниматься боксом
Определение типов значений
Типы значений не наследуются
Типы значений не реализуют интерфейсы
Несуществующее ключевое слово в рамке
Примечание: спецификация здесь неверна,
boxed
ключевого слова нет .Эпилог
Я думаю, что отчасти путаница в том, как типы значений наследуются, проистекает из того факта, что C # использует синтаксис приведения для выполнения упаковки и распаковки, из-за чего кажется, что вы выполняете приведение, что на самом деле не CLR выдаст исключение InvalidCastException при попытке распаковать неправильный тип).
(object)myStruct
в C # создает новый экземпляр упакованного типа типа значения; он не выполняет никаких приведений. Точно так же(MyStruct)obj
в C # распаковывает упакованный тип, копируя часть значения; он не выполняет никаких приведений.источник