Иногда мне нравится проводить некоторое время, смотря на код.NET только, чтобы видеть, как вещи реализованы негласно. Я наткнулся на этот драгоценный камень при взгляде на String.Equals
метод через Отражатель.
C#
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public override bool Equals(object obj)
{
string strB = obj as string;
if ((strB == null) && (this != null))
{
return false;
}
return EqualsHelper(this, strB);
}
IL
.method public hidebysig virtual instance bool Equals(object obj) cil managed
{
.custom instance void System.Runtime.ConstrainedExecution.ReliabilityContractAttribute::.ctor(valuetype System.Runtime.ConstrainedExecution.Consistency, valuetype System.Runtime.ConstrainedExecution.Cer) = { int32(3) int32(1) }
.maxstack 2
.locals init (
[0] string str)
L_0000: ldarg.1
L_0001: isinst string
L_0006: stloc.0
L_0007: ldloc.0
L_0008: brtrue.s L_000f
L_000a: ldarg.0
L_000b: brfalse.s L_000f
L_000d: ldc.i4.0
L_000e: ret
L_000f: ldarg.0
L_0010: ldloc.0
L_0011: call bool System.String::EqualsHelper(string, string)
L_0016: ret
}
Каково обоснование для проверки this
против null
? Я должен предположить, что существует цель иначе, это, вероятно, было бы поймано и удалено к настоящему времени.
Полагаю, вы смотрели на реализацию .NET 3.5? Я считаю, что реализация .NET 4 немного отличается.
Однако у меня есть скрытое подозрение, что это связано с тем, что даже методы виртуального экземпляра можно вызывать не виртуально по нулевой ссылке . То есть возможно в IL. Я посмотрю, смогу ли я создать некоторый IL, который вызывал бы null.Equals (null)
.
РЕДАКТИРОВАТЬ: Хорошо, вот интересный код:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 17 (0x11)
.maxstack 2
.locals init (string V_0)
IL_0000: nop
IL_0001: ldnull
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: ldnull
IL_0005: call instance bool [mscorlib]System.String::Equals(string)
IL_000a: call void [mscorlib]System.Console::WriteLine(bool)
IL_000f: nop
IL_0010: ret
} // end of method Test::Main
Я получил это, скомпилировав следующий код C #:
using System;
class Test
{
static void Main()
{
string x = null;
Console.WriteLine(x.Equals(null));
}
}
... а затем разобрав его с помощью ildasm
и отредактировав. Обратите внимание на эту строку:
IL_0005: call instance bool [mscorlib]System.String::Equals(string)
Изначально это был callvirt
вместо call
.
Итак, что происходит, когда мы собираем его заново? Что ж, с .NET 4.0 мы получаем следующее:
Unhandled Exception: System.NullReferenceException: Object
reference not set to an instance of an object.
at Test.Main()
Хм. А как насчет .NET 2.0?
Unhandled Exception: System.NullReferenceException: Object reference
not set to an instance of an object.
at System.String.EqualsHelper(String strA, String strB)
at Test.Main()
Теперь это более интересно ... нам явно удалось попасть в EqualsHelper
, чего мы обычно не ожидали.
Хватит строки ... давайте попробуем самостоятельно реализовать ссылочное равенство и посмотрим, сможем ли мы получить null.Equals (null)
, чтобы вернуть истину:
using System;
class Test
{
static void Main()
{
Test x = null;
Console.WriteLine(x.Equals(null));
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public override bool Equals(object other)
{
return other == this;
}
}
Та же процедура, что и раньше - разобрать, изменить callvirt
на вызов
, повторная сборка и просмотр print true
...
Обратите внимание, что хотя другой отвечает на , этот вопрос C ++ , мы здесь еще более коварны ... потому что мы вызываем виртуальный метод не виртуально. Обычно даже компилятор C ++ / CLI будет использовать callvirt
для виртуального метода.Другими словами, я думаю, что в этом конкретном случае единственный способ, чтобы this
было нулевым, - это написать IL вручную.
РЕДАКТИРОВАТЬ: Я только что кое-что заметил ... На самом деле я не вызывал правильный метод в ни из наших маленьких программ-примеров. Вот вызов в первом случае:
IL_0005: call instance bool [mscorlib]System.String::Equals(string)
вот вызов во втором:
IL_0005: call instance bool [mscorlib]System.Object::Equals(object)
В первом случае I означал для вызова System.String :: Equals (object)
, а во втором I имел в виду вызвать Test :: Equals (object)
. Отсюда мы видим три вещи:
object.Equals (object)
с радостью сравнивает пустую ссылку «this» . Если вы добавите немного вывода консоли в переопределение C #, вы увидите разницу - он не будет вызываться, если вы не измените IL для явного вызова, например:
IL_0005: call instance bool Test::Equals(object)
Итак, вот и все. Развлечение и злоупотребление методами экземпляра для нулевых ссылок.
Если вы зашли так далеко, вам также может быть интересно посмотреть в моем блоге , как типы значений могут объявлять конструкторы без параметров ... в IL.
Короткий ответ заключается в том, что такие языки, как C #, вынуждают вас создавать экземпляр этого класса перед вызовом метода, но сама Framework этого не делает. В CIL есть два разных способа вызова функции: call
и callvirt
.... Вообще говоря, C # всегда будет генерировать callvirt
, для чего требуется ] this
не должен быть нулевым. Но другие языки (на ум приходит C ++ / CLI) могут выдавать вызов
, чего не ожидают.
(Ладно, это больше похоже на пять, если вы считаете calli, newobj и т. Д., Но давайте не будем усложнять)
Причина в том, что это
действительно может быть нулевым
. Есть 2 операционных кода IL, которые можно использовать для вызова функции: call и callvirt. Функция callvirt заставляет среду CLR выполнять нулевую проверку при вызове метода. Инструкция вызова этого не делает и, следовательно, позволяет вводить метод с , этот
имеет значение null
.
Звучит страшно? Действительно немного. Однако большинство компиляторов гарантируют, что этого никогда не произойдет. Инструкция .call выводится только тогда, когда null
не возможен (я почти уверен, что C # всегда использует callvirt).
Это верно не для всех языков, и по причинам, которые я точно не знаю, команда BCL решила дополнительно усилить класс System.String
в этом случае.
Другой случай, когда это может всплывать, - это обратный вызов pinvoke.
Если аргумент (obj) не приводится к строке, тогда strB будет иметь значение null, а результат должен быть ложным. Пример:
int[] list = {1,2,3};
Console.WriteLine("a string".Equals(list));
пишет false
.
Помните, что метод string.Equals () вызывается для любого типа аргумента, а не только для других строк.
Давайте посмотрим ... это
первая строка, которую вы сравниваете. obj
- второй объект. Похоже, это своего рода оптимизация. Это первое преобразование obj
к строковому типу. И если это не удается, тогда strB
имеет значение null. И если strB
имеет значение null, а this
- нет, тогда они определенно не равны, и функцию EqualsHelper
можно пропустить.
Это сохранит вызов функции. Помимо этого, возможно, лучшее понимание функции EqualsHelper
может пролить свет на то, зачем нужна эта оптимизация.
РЕДАКТИРОВАТЬ:
А, значит, функция EqualsHelper принимает (строка, строка)
в качестве параметров. Если strB
имеет значение NULL, то это, по сути, означает, что либо это был нулевой объект с самого начала, либо его нельзя было успешно преобразовать в строку. Если причина того, что strB
имеет значение NULL, заключается в том, что объект относится к другому типу, который не может быть преобразован в строку, тогда вы не захотите вызывать EqualsHelper с двумя нулевыми значениями (которые ' верну истину). Функция Equals должна возвращать false в этом случае. Таким образом, этот оператор if - это больше, чем оптимизация, он на самом деле также обеспечивает надлежащую функциональность.