Извините за стену текста, но я хотел дать хорошее представление о ситуации. Я знаю, что вы можете вызывать методы для нулевых ссылок в IL, но все еще не понимаете несколько очень странных вещей, которые происходят, когда вы делаете это, в отношении моего понимания того, как работает CLR. Несколько других вопросов, которые я нашел здесь относительно этого, не касались поведения, которое я вижу здесь.
Вот немного ИЛ:
.assembly MrSandbox {}
.class private MrSandbox.AClass {
.field private int32 myField
.method public int32 GetAnInt() cil managed {
.maxstack 1
.locals init ([0] int32 retval)
ldc.i4.3
stloc retval
ldloc retval
ret
}
.method public int32 GetAnotherInt() cil managed {
.maxstack 1
.locals init ([0] int32 retval)
ldarg.0
ldfld int32 MrSandbox.AClass::myField
stloc retval
ldloc retval
ret
}
}
.class private MrSandbox.Program {
.method private static void Main(string[] args) cil managed {
.entrypoint
.maxstack 1
.locals init ([0] class MrSandbox.AClass p,
[1] int32 myInt)
ldnull
stloc p
ldloc p
call instance int32 MrSandbox.AClass::GetAnotherInt()
stloc myInt
ldloc myInt
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
Теперь, когда этот код запускается, мы получаем то, что я ожидаю, вроде . callvirt
будет проверять наличие нуля, где call
, однако, здесь при вызове NullReferenceException
выбрасывается. Это не ясно для меня, поскольку я ожидал бы System.AccessViolationException
вместо этого. Я объясню свои рассуждения в конце этого вопроса.
Если мы заменим код внутри Main(string[] args)
на этот (после .locals
строк):
ldnull
stloc p
ldloc p
call instance int32 MrSandbox.AClass::GetAnInt()
stloc myInt
ldloc myInt
call void [mscorlib]System.Console::WriteLine(int32)
ret
К моему удивлению, он запускается и печатает 3
на консоли, успешно завершая работу. Я вызываю функцию по нулевой ссылке, и она выполняется правильно. Я предполагаю, что это как-то связано с тем, что никакие поля экземпляра не вызываются, поэтому CLR может успешно выполнить код.
Наконец, и вот тут-то и возникает настоящая путаница, замените код в Main(string[] args)
следующим (после строк .locals
):
ldnull
stloc p
ldloc p
call instance int32 MrSandbox.AClass::GetAnInt()
stloc myInt
ldloc myInt
call void [mscorlib]System.Console::WriteLine(int32)
call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
pop
call instance int32 MrSandbox.AClass::GetAnotherInt()
stloc myInt
ldloc myInt
call void [mscorlib]System.Console::WriteLine(int32)
ret
Теперь, что вы ожидаете от этого кода? Я ожидал, что код выведет 3
на консоль, прочитает ключ с консоли и затем завершится с ошибкой NullReferenceException
. Ну, ничего этого не происходит. Вместо этого никакие значения не выводятся на экран, кроме System.AccessViolationException
. Почему это противоречиво?
С задним фоном, вот мои вопросы:
1) MSDN указывает, что callvirt
будет выдавать NullReferenceException
, если obj равно null, но call
просто говорит, что оно не должно быть нулевым. Почему тогда он выбрасывает NRE по умолчанию вместо нарушения прав доступа? Мне кажется, что call
по контракту попытается получить доступ к памяти и потерпит неудачу, вместо того, чтобы делать то, что callvirt
делает, сначала проверяя на ноль.
2) Является ли причиной того, что второй пример работает, из-за того, что он не имеет доступа к полям уровня класса и что call
не выполняет проверку на ноль? Если так, как нестатический метод может быть вызван для нулевой ссылки и возвращен успешно? Насколько я понимаю, когда ссылочный тип помещается в стек, только объект типа помещается в кучу. Так вызывается ли метод из объекта типа?
3) Почему разницу в исключениях бросают между первым и последним примером? По моему мнению, третий пример выдает правильное исключение AccessViolationException
, поскольку это именно то, что он пытается сделать; доступ к нераспределенной памяти.
До того, как ответы "Поведение неопределено", я знаю, что это не ВСЕ правильный способ написания вещей, я просто надеюсь, что кто-то может помочь пролить некоторое представление о вышеуказанные вопросы.
Спасибо.