Зачем это нужно? = Null?

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

С#

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

Ответ 1

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

Однако у меня есть подозрительное подозрение, что это связано с тем, что можно вызвать даже методы виртуального экземпляра практически без нулевой ссылки. Возможно в IL, то есть. Я посмотрю, могу ли я создать IL, который будет называть null.Equals(null).

EDIT: Хорошо, вот какой-то интересный код:

.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

Я получил это, скомпилировав следующий код С#:

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...

Обратите внимание, что, хотя другой отвечает на этот вопрос на С++, мы здесь еще более коварны... потому что мы вызываем виртуальный метод практически. Обычно даже компилятор С++/CLI будет использовать callvirt для виртуального метода. Другими словами, я думаю, что в этом конкретном случае единственным способом для this быть null является запись IL вручную.


EDIT: Я только что заметил кое-что... На самом деле я не называл правильный метод ни в одной из наших маленьких программ-примеров. Здесь вызов в первом случае:

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). Из этого мы можем видеть три вещи:

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

(¹okay, это больше похоже на пять, если вы считаете calli, newobj и т.д., но пусть это упрощает)

Ответ 4

В исходный код есть этот комментарий:

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

Ответ 5

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

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

EDIT:

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

Ответ 6

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

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

пишет false.

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