Почему проверка это! = пустой указатель?

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

71
задан Brian Gideon 25 July 2010 в 15:03
поделиться

5 ответов

Полагаю, вы смотрели на реализацию .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) . Отсюда мы видим три вещи:

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

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

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

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

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

85
ответ дан 24 November 2019 в 13:04
поделиться

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

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

9
ответ дан 24 November 2019 в 13:04
поделиться

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

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

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

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

17
ответ дан 24 November 2019 в 13:04
поделиться

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

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

пишет false .

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

0
ответ дан 24 November 2019 в 13:04
поделиться

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

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

РЕДАКТИРОВАТЬ:

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

1
ответ дан 24 November 2019 в 13:04
поделиться
Другие вопросы по тегам:

Похожие вопросы: