System.Int32 содержит... другой System.Int32

Я использовал отражение для проверки содержимого System.Int32 и обнаружил, что оно содержит еще одно System.Int32.

System.Int32 m_value;

Я не понимаю, как это возможно.

Этот int действительно является "целым числом поддержки" того, что у вас есть: если вы пометите int и используете отражение, чтобы изменить значение его поля m_value, вы фактически измените значение целого числа:

object testInt = 4;
Console.WriteLine(testInt); // yields 4

typeof(System.Int32)
    .GetField("m_value", BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(testInt, 5);
Console.WriteLine(testInt); // yields 5

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

Ответы

Ответ 1

Как уже отмечалось, 32-битовое целое может существовать в двух вариантах. Четыре байта в любом месте в памяти или в регистре CPU (а не только в стеке), быстрая версия. И он может быть встроен в System.Object, в коробке. Декларация для System.Int32 совместима с последней. При вставке он имеет типичный заголовок объекта, за которым следуют 4 байта, который сохраняет значение. И эти 4 байта точно соответствуют члену m_value. Может быть, вы видите, почему здесь нет конфликта: m_value - это быстрая, без коробки версия. Потому что нет такой вещи, как коробочное целое число.

Как компилятор языка, так и компилятор JIT прекрасно знают о свойствах Int32. Компилятор отвечает за принятие решения о том, когда целое число должно быть в коробке и распаковано, оно генерирует соответствующие инструкции IL для этого. И он знает, какие IL-инструкции доступны, что позволяет целому числу работать без бокса в первую очередь. Легко понять из методов, реализованных System.Int32, он не имеет переопределения для оператора ==(), например. Это сделано с помощью кода операции CEQ. Но у него есть переопределение для Equals(), требуемое для переопределения метода Object.Equals(), когда целое число помещается в бокс. Ваш компилятор должен иметь такое же понимание.

Ответ 2

Посмотрите thread за трудоемкое обсуждение этой тайны.

Ответ 3

Магия на самом деле находится в боксе/распаковке.

System.Int32 (и его псевдоним int) - это тип значения, что означает, что он обычно выделяется в стеке. CLR принимает ваше объявление System.Int32 и просто превращает его в 32 бита пространства стека.

Однако, когда вы пишете object testInt = 4;, компилятор автоматически помещает ваше значение 4 в ссылку, так как object является ссылочным типом. То, что у вас есть, - это ссылка, указывающая на System.Int32, которая в настоящее время составляет 32 бита места в куче. Но ссылка на System.Int32 с автоматической коробкой называется (... ждать ее...) System.Int32.

Что делает ваш образец кода, это создание ссылки System.Int32 и изменение значения System.Int32, на которое он указывает. Это объясняет странное поведение.

Ответ 4

Обоснование

Другие ответы являются неосведомленными и/или вводящими в заблуждение.

  Пролог

Это может помочь понять это, сначала прочитав мой ответ на Как ValueTypes наследуются от Object (ReferenceType) и по-прежнему являются ValueTypes?

  Так что происходит?

System.Int32 - это структура, которая содержит 32-разрядное целое число со знаком. Он не содержит себя. Для ссылки на тип значения в IL используется синтаксис valuetype [assembly]Namespace.TypeName.

II.7.2 Встроенные типы Встроенные типы CLI имеют соответствующие типы значений, определенные в библиотеке базовых классов. На них должны ссылаться подписи только с использованием их специальных кодировок (т.е. без использования синтаксиса типа общего назначения TypeReference). Раздел я определяет встроенные типы.

Это означает, что если у вас есть метод, который принимает 32-разрядное целое число, вы не должны использовать обычный синтаксис valuetype [mscorlib]System.Int32, а использовать специальную кодировку для 32-разрядного целого числа со знаком со знаком int32.

В С# это означает, что независимо от того, введете ли вы System.Int32 или int, они будут скомпилированы в int32, а не в valuetype [mscorlib]System.Int32.

Возможно, вы слышали, что int является псевдонимом для System.Int32 в С#, но в действительности оба являются псевдонимами встроенного типа значения CLS int32.

Так что пока структура, как

public struct MyStruct
{
    internal MyStruct m_value;
}

Действительно скомпилируется (и, следовательно, будет недействительным):

.class public sequential ansi sealed beforefieldinit MyStruct extends [mscorlib]System.ValueType
{
    .field assembly valuetype MyStruct m_value;
}

namespace System
{
    public struct Int32
    {
        internal int m_value;
    }
}

Вместо этого компилируется в (игнорируя интерфейсы):

.class public sequential ansi sealed beforefieldinit System.Int32 extends [mscorlib]System.ValueType
{
    .field assembly int32 m_value;
}

Компилятору С# не требуется особый случай для компиляции System.Int32, поскольку спецификация CLI предусматривает, что все ссылки на System.Int32 заменяются специальной кодировкой для встроенного типа значения CLS int32. Ergo. System.Int32 - это структура, которая не содержит другого System.Int32, но int32. В IL вы можете иметь 2 перегрузки метода, одна из которых принимает System.Int32, а другая - int32, и они могут сосуществовать:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 2:0:0:0
}
.assembly test {}
.module test.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000001
.class MyNamespace.Program
{
    .method static void Main() cil managed
    {
        .entrypoint
        ldc.i4.5
        call int32 MyNamespace.Program::Lol(valuetype [mscorlib]System.Int32) // Call the one taking the System.Int32 type.
        call int32 MyNamespace.Program::Lol(int32) // Call the overload taking the built in int32 type.
        call void [mscorlib]System.Console::Write(int32)
        call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
        pop
        ret
    }

    .method static int32 Lol(valuetype [mscorlib]System.Int32 x) cil managed
    {
        ldarg.0
        ldc.i4.1
        add
        ret
    }

    .method static int32 Lol(int32 x) cil managed
    {
        ldarg.0
        ldc.i4.1
        add
        ret
    }
}

Декомпиляторы, такие как ILSpy, dnSpy,.NET Reflector и т.д., могут вводить в заблуждение. Они (на момент написания) будут декомпилировать как int32, так и System.Int32 как ключевое слово С# int или тип System.Int32, потому что так мы определяем целые числа в С#.

Но int32 является встроенным типом значения для 32-разрядных целых чисел со знаком (т.е. VES имеет прямую поддержку этих типов с такими инструкциями, как add, sub, ldc.i4.x и т.д.); System.Int32 - соответствующий тип значения, определенный в библиотеке классов. Соответствующий тип System.Int32 используется для бокса и для таких методов, как ToString(), CompareTo() и т.д.

  Если вы пишете программу на чистом IL, вы можете создать свой собственный тип значения, который содержит int32 точно таким же образом, где вы все еще используете int32, но вызываете методы для пользовательского "соответствующего" типа значения.

.class MyNamespace.Program
{
    .method hidebysig static void  Main(string[] args) cil managed
    {
        .entrypoint
        .maxstack 8
        ldc.i4.0
        call void MyNamespace.Program::PrintWhetherGreaterThanZero(int32)
        ldc.i4.m1 // -1
        call void MyNamespace.Program::PrintWhetherGreaterThanZero(int32)
        ldc.i4.3
        call void MyNamespace.Program::PrintWhetherGreaterThanZero(int32)
        ret
    }

    .method private hidebysig static void PrintWhetherGreaterThanZero(int32 'value') cil managed noinlining 
    {
        .maxstack 8
        ldarga 0
        call instance bool MyCoolInt32::IsGreaterThanZero()
        brfalse.s printOtherMessage

        ldstr "Value is greater than zero"
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    printOtherMessage:
        ldstr "Value is not greater than zero"
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }
}

.class public MyCoolInt32 extends [mscorlib]System.ValueType
{
    .field assembly int32 myCoolIntsValue;

    .method public hidebysig bool IsGreaterThanZero()
    {
        .maxstack 8

        ldarg.0
        ldind.i4
        ldc.i4.0
        bgt.s isNonZero
        ldc.i4.0
        ret
    isNonZero:
        ldc.i4.1
        ret
    }
}

Это ничем не отличается от типа System.Int32, за исключением того, что компилятор С# не считает MyCoolInt32 соответствующим типом int32, но для CLR это не имеет значения. Это, однако, не удастся PEVerify.exe, но он будет работать нормально. Декомпиляторы будут показывать приведения и явные разыменования указателя при декомпиляции выше, потому что они также не считают MyCoolInt32 и int32 связанными.

Но функционально никакой разницы нет, и за кулисами в CLR не происходит никакой магии.