Зачем проверять это!= Ноль?
Иногда мне нравится тратить некоторое время на просмотр кода.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);
}
Иллинойс
.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
? Я должен предположить, что есть цель, иначе это, вероятно, было бы поймано и удалено к настоящему времени.
6 ответов
Я полагаю, вы смотрели на реализацию.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
в call
, собрать и посмотреть, как это распечатать 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)
В первом случае я хотел позвонитьSystem.String::Equals(object)
, а во втором я хотел позвонитьTest::Equals(object)
, Из этого мы видим три вещи:
- Вы должны быть осторожны с перегрузкой.
- Компилятор C# генерирует вызовы для объявления виртуального метода - не самое специфическое переопределение виртуального метода. IIRC, VB работает наоборот
object.Equals(object)
счастлив сравнить нулевую ссылку "это"
Если вы добавите немного консольного вывода в переопределение C#, вы увидите разницу - он не будет вызываться, если вы не измените IL для его явного вызова, например:
IL_0005: call instance bool Test::Equals(object)
Итак, мы здесь. Веселье и злоупотребление методами экземпляра на нулевых ссылках.
Если вы продвинулись так далеко, вы также можете посмотреть в моем блоге о том, как типы значений могут объявлять конструкторы без параметров... в IL.
Причина в том, что это действительно возможно для this
быть null
, Существует 2 кода операции IL, которые можно использовать для вызова функции: вызов и callvirt. Функция callvirt заставляет CLR выполнять нулевую проверку при вызове метода. Инструкция вызова не позволяет и, следовательно, позволяет вводить метод с this
являющийся null
,
Звучит страшно? На самом деле это немного. Однако большинство компиляторов гарантируют, что этого никогда не произойдет. Инструкция.call выводится только когда null
это не возможно (я уверен, что C# всегда использует callvirt).
Это не относится ко всем языкам, и по причинам, которые я точно не знаю, команда BCL решила усилить System.String
класс в этом случае.
Другой случай, когда это может всплывающее окно, - обратные вызовы pinvoke.
Короткий ответ заключается в том, что такие языки, как C#, заставляют вас создавать экземпляр этого класса перед вызовом метода, а сама Framework этого не делает. В CIL есть два разных способа вызова функции: call
а также callvirt
.... Вообще говоря, C# всегда будет излучать callvirt
, что требует this
не быть нулевым. Но другие языки (C++/CLI приходит на ум) могут испускать call
, который не имеет такого ожидания.
(Ладно, это больше похоже на пять, если считать колли, newobj и т. Д., Но давайте будем проще)
Исходный код имеет этот комментарий:
это необходимо для защиты от обратных вызовов и других абонентов, которые не используют инструкцию callvirt
Посмотрим... this
первая строка, которую вы сравниваете. obj
это второй объект. Похоже, это какая-то оптимизация. Это первый кастинг obj
к строковому типу. И если это не удается, то strB
нулевой. И если strB
в то время как ноль this
нет, то они определенно не равны, и EqualsHelper
Функция может быть пропущена.
Это сохранит вызов функции. Помимо этого, возможно, лучшее понимание EqualsHelper
Функция может пролить некоторый свет на то, зачем нужна эта оптимизация.
РЕДАКТИРОВАТЬ:
Ах, так что функция EqualsHelper принимает (string, string)
в качестве параметров. Если strB
имеет значение null, то по сути это означает, что это был либо нулевой объект с самого начала, либо он не может быть успешно преобразован в строку. Если причина strB
Нулевым является то, что объект был другого типа, который не может быть преобразован в строку, тогда вы не захотите вызывать EqualsHelper, по сути, с двумя нулевыми значениями (которые будут возвращать true). Функция Equals должна вернуть false в этом случае. Так что, если оператор - это больше, чем оптимизация, он на самом деле также обеспечивает надлежащую функциональность.
Если аргумент (obj) не приведен к строке, тогда strB будет нулевым, а результат должен быть ложным. Пример:
int[] list = {1,2,3};
Console.WriteLine("a string".Equals(list));
пишет false
,
Помните, что метод string.Equals() вызывается для любого типа аргумента, а не только для других строк.