Подтвердить что ты не робот

Как ValueTypes происходят из Object (ReferenceType) и все еще являются ValueTypes?

С# не позволяет строить structs из классов, но все ValueTypes получаются из Object. Где это различие сделано?

Как CLR обрабатывает это?

4b9b3361

Ответ 1

С# не позволяет строить структуры из классов

Ваше утверждение неверно, поэтому ваше замешательство. С# позволяет структурам выводиться из классов. Все структуры производятся от того же класса System.ValueType, который происходит из System.Object. И все перечисления происходят из System.Enum.

ОБНОВЛЕНИЕ: В некоторых (теперь удаленных) комментариях была некоторая путаница, которая требует разъяснений. Я задам несколько дополнительных вопросов:

Создаются ли структуры из базового типа?

Просто да. Мы видим это, читая первую страницу спецификации:

Все типы С#, включая примитивные типы, такие как int и double, наследуют от одного типа объектов root.

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

Существуют ли другие способы узнать, что типы struct получают из базового типа?

Конечно. Тип структуры может переопределять ToString. Что это за переопределение, если не виртуальный метод его базового типа? Поэтому он должен иметь базовый тип. Этот базовый тип является классом.

Можно ли получить пользовательскую структуру из класса по своему выбору?

Просто нет. Это не означает, что структуры не выводятся из класса. Структуры происходят от класса и тем самым наследуют наследуемых членов этого класса. На самом деле, структуры должны выводиться из определенного класса: требуется перечислить из Enum, структуры должны выводиться из ValueType. Поскольку они требуются, язык С# запрещает вам указывать отношения деривации в коде.

Зачем это запрещено?

Когда требуется взаимодействие, разработчик языка имеет опции: (1) требует от пользователя ввода необходимого заклинания, (2) сделать его необязательным или (3) запретить его. У каждого есть плюсы и минусы, а разработчики языка С# выбрали по-разному в зависимости от конкретных деталей каждого.

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

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

Аналогично, все типы делегатов происходят из MulticastDelegate, но С# требует, чтобы вы этого не говорили.

Итак, теперь мы установили, что все структуры в С# получены из класса.

Какова связь между наследованием и деривацией из класса?

Многие люди путаются отношениями наследования в С#. Отношение наследования довольно просто: если тип, класс или делегат типа D происходит от класса B, тогда наследуемые члены B также являются членами D. Это так просто.

Что это означает в отношении наследования, когда мы говорим, что структура происходит из ValueType? Просто, что все наследуемые члены ValueType также являются членами структуры. Так, например, структуры получают реализацию ToString, например; он наследуется от базового класса структуры.

Все наследуемые члены? Наверняка нет. Наследуются ли частные члены?

Да. Все частные члены базового класса также являются членами производного типа. Неправильно вызывать этих членов по имени, конечно, если сайт вызова не находится в домене доступности члена. Просто потому, что у вас есть член, это не значит, что вы можете его использовать!

Теперь мы продолжим исходный ответ:


Как CLR обрабатывает это?

Чрезвычайно хорошо.: -)

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

Посмотрите на это так. Предположим, я рассказал вам следующие факты:

  • Существует два вида коробок: красный коробки и синие коробки.

  • Каждый красный ящик пуст.

  • Есть три специальных синих прямоугольника, называемых O, V и E.

  • O не находится внутри какого-либо поля.

  • 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 использует для учета того факта, что у нас есть отношение деривации между двумя вещами, которые имеют совершенно разные представления, а именно, имеют ли упомянутые данные заголовок объекта, блок синхронизации, независимо от того, владеет ли он собственным хранилищем для сбора мусора и т.д. Эти механизмы сложны, слишком сложны для объяснения в одном ответе. Правила системы CLR довольно немного сложнее, чем несколько упрощенный вкус, который мы видим на С#, где нет сильного различия между коробочными и unboxed версиями типа, например. Введение дженериков также вызвало дополнительную сложность, добавляемую в CLR. Подробные сведения см. В спецификации CLI, уделяя особое внимание правилам бокса и ограниченным виртуальным вызовам.

Ответ 2

Это несколько искусственная конструкция, поддерживаемая CLR, чтобы позволить всем типам обрабатываться как System.Object.

Типы значений происходят из System.Object через System.ValueType, где происходит специальная обработка (например: CLR обрабатывает бокс/распаковку, и т.д. для любого типа, полученного из ValueType).

Ответ 3

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

Я думаю, что лучший способ ответить на этот вопрос: ValueType является особенным. Это, по сути, базовый класс для всех типов значений в системе CLR. Трудно понять, как ответить "как CLR справляется с этим", потому что это просто правило CLR.

Ответ 4

  Ваше утверждение неверно, следовательно, ваше замешательство. С# позволяет структурам наследовать от классов. Все структуры происходят из одного класса System.ValueType

Итак, давайте попробуем это:

 struct MyStruct :  System.ValueType
 {
 }

Это даже не скомпилируется. Компилятор напомнит вам "Тип 'System.ValueType' в списке интерфейсов не является интерфейсом".

Когда вы декомпилируете Int32, который является структурой, вы найдете:

публичная структура Int32: IComparable, IFormattable, IConvertible {}, не упоминая, что оно получено из System.ValueType. Но в объектном браузере вы обнаружите, что Int32 наследуется от System.ValueType.

Так что все это заставляет меня поверить:

Я думаю, что лучший способ ответить на это - ValueType особенный. По сути, это базовый класс для всех типов значений в системе типов CLR. Трудно понять, как ответить "как CLR справляется с этим", потому что это просто правило CLR.

Ответ 5

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

Ответ 6

Обоснование

Из всех ответов @supercat ответ наиболее близок к реальному ответу. Поскольку другие ответы на самом деле не отвечают на этот вопрос и прямо заявляют о неправильных утверждениях (например, типы значений наследуются от чего-либо), я решил ответить на вопрос.

  Пролог

Этот ответ основан на моем собственном реверс-инжиниринге и спецификации CLI.

struct и class являются ключевыми словами С#. Что касается интерфейса командной строки, все типы (классы, интерфейсы, структуры и т.д.) Определяются определениями классов.

Например, тип объекта (известный в С# как class) определяется следующим образом:

.class MyClass
{
}

Интерфейс определяется определением класса с семантическим атрибутом interface:

.class interface MyInterface
{
}

А как насчет типов значений?

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

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

Если мы представим следующую структуру С#:

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 вещи:

  1. Определяет тип значения с 3 полями (и одним методом). Он ничего не наследует и не реализует никаких интерфейсов (типы значений не могут этого делать).
  2. Определяет тип объекта (коробочный тип) с 3 полями (и реализует один метод интерфейса), наследуя от System.ValueType и реализуя интерфейс System.ICloneable.

Также обратите внимание, что любое определение класса, расширяющее System.ValueType, также является запечатанным, независимо от того, указано ключевое слово sealed или нет.

Поскольку типы значений являются просто простыми структурами, не наследуют, не реализуют и не поддерживают полиморфизм, их нельзя использовать с остальной частью системы типов. Чтобы обойти это, поверх типа значения CLR также определяет соответствующий ссылочный тип с такими же полями, известный как коробочный тип. Поэтому, хотя тип значения не может быть передан методам, принимающим object, его соответствующий тип в штучной упаковке может.

  Теперь, если вы должны определить метод в С#, как

public static void BlaBla(MyNamespace.MyValueType x),

вы знаете, что метод примет тип значения MyNamespace.MyValueType.

Выше мы узнали, что определение класса, которое следует из ключевого слова struct в С#, на самом деле определяет как тип значения, так и тип объекта. Мы можем ссылаться только на определенный тип значения. Несмотря на то, что в спецификации CLI указано, что ключевое слово ограничения boxed может использоваться для ссылки на коробочную версию типа, это ключевое слово не существует (см. ECMA-335, II.13.1 Ссылки на типы значений). Но давайте представим, что это на мгновение.

При обращении к типам в IL поддерживается несколько ограничений, среди которых class и valuetype. Если мы используем valuetype MyNamespace.MyType, мы указываем определение класса типа значения MyNamespace.MyType. Аналогично, мы можем использовать class MyNamespace.MyType, чтобы указать определение класса типа объекта с именем MyNamespace.MyType. Это означает, что в IL вы можете иметь тип значения (struct) и тип объекта (class) с тем же именем и при этом различать их. Теперь, если бы ключевое слово boxed, указанное в спецификации CLI, было фактически реализовано, мы могли бы использовать boxed MyNamespace.MyType, чтобы указать коробочный тип определения класса значения типа MyNamespace.MyType.

Итак, .method static void Print(valuetype MyNamespace.MyType test) cil managed принимает тип значения, определенный в определении класса типа значения с именем MyNamespace.MyType,

в то время как .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, определение класса определяет тип значения и его соответствующий тип в штучной упаковке (struct).

Расположение памяти

В этом разделе предполагается 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-компилятор расположит их в наиболее оптимальном порядке. Поля выровнены по границам своего размера. Padding добавлен, чтобы удовлетворить это. Я не уверен, почему, но каждый класс всегда имеет дополнительные 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(). Этот скрытый метод распаковывает (только вычисление адреса, как инструкция IL unbox) упакованного типа, затем статически вызывает ToString(), определенный в структуре.

Аналогично, у упакованного типа есть скрытый метод для каждого реализованного метода интерфейса, который выполняет такую же распаковку, а затем статически вызывает метод, определенный в структуре.

  Спецификация CLI

Бокс

I.8.2.4 Для каждого типа значения CTS определяет соответствующий ссылочный тип, называемый упакованным типом. Обратное неверно: как правило, ссылочные типы не имеют соответствующего типа значения. Представление значения в штучной упаковке (в штучной упаковке) является местом, где может храниться значение типа значения. Тип в штучной упаковке - это тип объекта, а в штучной упаковке - это объект.

Определение типов значений

I.8.9.7 Не все типы, определенные определением класса, являются типами объектов (см. §I.8.2.3); в частности, типы значений не являются типами объектов, но они определяются с использованием определения класса. Определение класса для типа значения определяет как (неупакованный) тип значения, так и связанный с ним упакованный тип (см. §I.8.2.4). Члены определения класса определяют представление обоих.

II.10.1.3 Семантические атрибуты типа определяют, должны ли быть определены интерфейс, класс или тип значения. Атрибут интерфейса указывает интерфейс. Если этот атрибут отсутствует, и определение расширяет (прямо или косвенно) 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 отсутствует.

Эпилог

Я думаю, что часть путаницы того, как типы значений, кажется, наследуют, проистекает из того факта, что С# использует синтаксис приведения типов для выполнения упаковки и распаковки, что создает впечатление, что вы выполняете приведение типов, что на самом деле не так (хотя CLR сгенерирует исключение InvalidCastException при попытке распаковать неправильный тип). (object)myStruct в С# создает новый экземпляр типа в штучной упаковке типа значения; он не выполняет никаких бросков. Аналогично, (MyStruct)obj в С# распаковывает упакованный тип, копируя часть значения; он не выполняет никаких бросков.