Зачем проверять это!= Ноль?

Иногда мне нравится тратить некоторое время на просмотр кода.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() вызывается для любого типа аргумента, а не только для других строк.

Другие вопросы по тегам