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 не происходит никакой магии.