Зачем проверять это! = Ноль? - PullRequest
71 голосов
/ 29 июня 2010

Иногда мне нравится тратить некоторое время на просмотр кода .NET, просто чтобы посмотреть, как все реализовано за сценой. Я наткнулся на этот камень, глядя на метод String.Equals с помощью Reflector.

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? Я должен предположить, что есть цель, иначе это, вероятно, было бы уже поймано и удалено.

Ответы [ 6 ]

85 голосов
/ 29 июня 2010

Полагаю, вы рассматривали реализацию .NET 3.5? Я считаю, что реализация .NET 4 немного отличается.

Однако у меня есть подозрение, что это связано с тем, что даже виртуальные методы экземпляра можно вызывать не виртуально по нулевой ссылке . Возможно в ИЛ, то есть. Я посмотрю, смогу ли я создать какой-нибудь 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), чтобы вернуть true:

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)

В первом случае I означал для вызова System.String::Equals(object), а во втором I означал для вызова Test::Equals(object). Отсюда видно три вещи:

  • Вы должны быть осторожны с перегрузкой.
  • Компилятор C # отправляет вызовы объявителю виртуального метода - не самое специфическое переопределение виртуального метода. IIRC, VB работает наоборот
  • object.Equals(object) рад сравнить нулевую ссылку "this"

Если вы добавите немного консольного вывода в переопределение C #, вы увидите разницу - он не будет вызываться, если вы не измените IL для его явного вызова, например:

IL_0005:  call   instance bool Test::Equals(object)

Итак, мы здесь. Веселье и злоупотребление методами экземпляра на нулевых ссылках.

Если вы продвинулись так далеко, вы также можете посмотреть в моем блоге сообщение о , как типы значений могут объявить конструкторы без параметров ... в IL.

17 голосов
/ 29 июня 2010

Причина в том, что this действительно может быть null. Существует 2 кода операции IL, которые можно использовать для вызова функции: вызов и callvirt. Функция callvirt заставляет CLR выполнять нулевую проверку при вызове метода. Инструкция вызова не допускает и, следовательно, позволяет вводить метод с this, равным null.

Звучит страшно? На самом деле это немного. Однако большинство компиляторов гарантируют, что этого никогда не произойдет. Инструкция .call выводится только тогда, когда null не представляется возможным (я уверен, что C # всегда использует callvirt).

Это не относится ко всем языкам, и по причинам, которые я точно не знаю, команда BCL решила дополнительно укрепить класс System.String в этом случае.

Другой случай, когда это может всплывающее окно, - обратные вызовы пинвока.

9 голосов
/ 29 июня 2010

Короткий ответ заключается в том, что языки, подобные C #, вынуждают вас создавать экземпляр этого класса перед вызовом метода, а сама Framework этого не делает. В CIL есть два разных способа вызова функции: call и callvirt .... Вообще говоря, C # всегда будет выдавать callvirt, что требует, чтобы this не было нулевым. Но другие языки (C ++ / CLI приходят на ум) могут выдавать call, чего нельзя ожидать.

(Ладно, это больше похоже на пять, если считать колли, newobj и т. Д., Но давайте будем простыми)

4 голосов
/ 13 июня 2014

Исходный код имеет следующий комментарий:

это необходимо для защиты от обратных вызовов и других абонентов, которые не используют инструкцию callvirt

1 голос
/ 29 июня 2010

Давайте посмотрим ... this - первая строка, которую вы сравниваете. obj это второй объект. Похоже, это какая-то оптимизация. Сначала приводится obj к строковому типу. И если это не удается, то strB равно нулю. И если strB равно нулю, а this - нет, то они определенно не равны, и функцию EqualsHelper можно пропустить.

Это сохранит вызов функции. Кроме того, возможно, лучшее понимание функции EqualsHelper может пролить свет на то, зачем нужна эта оптимизация.

EDIT:

Ах, значит, функция EqualsHelper принимает (string, string) в качестве параметров. Если strB равно нулю, то это, по сути, означает, что это был либо нулевой объект с самого начала, либо он не может быть успешно преобразован в строку. Если причина того, что strB является нулевым, заключается в том, что объект был другого типа, который не может быть преобразован в строку, то вы не захотите вызывать EqualsHelper, по сути, с двумя нулевыми значениями (которые будут возвращать true) . Функция Equals должна возвращать false в этом случае. Так что, если оператор - это больше, чем оптимизация, он фактически также обеспечивает надлежащую функциональность.

0 голосов
/ 30 июня 2010

Если аргумент (obj) не приведен к строке, тогда strB будет нулевым, и результат должен быть ложным. Пример:

    int[] list = {1,2,3};
    Console.WriteLine("a string".Equals(list));

пишет false.

Помните, что метод string.Equals () вызывается для любого типа аргумента, а не только для других строк.

...