Подтвердить что ты не робот

Вызов вместо callvirt в случае нового С# 6 "?" null check

Учитывая два метода:

    static void M1(Person p)
    {
        if (p != null)
        {
            var p1 = p.Name;
        }
    }

    static void M2(Person p)
    {
        var p1 = p?.Name;
    }

Почему код M1 IL использует callvirt:

IL_0007:  brfalse.s  IL_0012
IL_0009:  nop
IL_000a:  ldarg.0
IL_000b:  callvirt   instance string ConsoleApplication4.Person::get_Name()

и M2 IL используют call:

brtrue.s   IL_0007
IL_0004:  ldnull
IL_0005:  br.s       IL_000d
IL_0007:  ldarg.0
IL_0008:  call       instance string ConsoleApplication4.Person::get_Name()

Я просто могу догадаться, что это потому, что в M2 мы знаем, что p не является нулевым и его похожее

new MyClass().MyMethod();

Это правда?

Если это так, что, если p будет null в другом потоке?

4b9b3361

Ответ 1

Думаю, теперь ясно,

Это простой и потокобезопасный способ проверить значение null перед запуском события. Причина его потокобезопасности в том, что функция оценивает левую сторону только один раз и сохраняет ее во временной переменной. MSDN

Таким образом, здесь можно использовать инструкцию call.

Я написал сообщение о различиях между call и callvirt и почему С# generate callvirt

Спасибо Dan Lyons за ссылку MSDN.

Ответ 2

callvirt в M1 - стандартное генерирование кода С#. Он предоставляет языковые гарантии того, что метод экземпляра никогда не может быть вызван с нулевой ссылкой. Другими словами, он гарантирует, что p != null и генерирует исключение NullReferenceException, если оно равно null. Ваш явный тест не меняет этого.

Эта гарантия довольно приятная, отладка NRE становится довольно волосатой, если она this равна нулю. Гораздо проще диагностировать неудачу на сайте вызова, отладчик может быстро показать вам, что это p, что является нарушителем спокойствия.

Но, конечно, callvirt не является бесплатным, хотя стоимость очень низкая, одна дополнительная инструкция процессора во время выполнения. Поэтому, если его можно заменить на call, тогда код будет быстрее на половину наносекунды, дайте или возьмите. Фактически это может быть с помощью оператора elvis, поскольку он уже гарантирует, что ссылка не является нулевой, поэтому компилятор С# 6 воспользовался этим и генерирует вызов вместо callvirt.

Ответ 3

Начните с того, что вместо call используется callvirt из-за правила С#, что у нулевых объектов могут не быть вызванные им методы, даже если .NET это позволяет.

Теперь, в обоих ваших методах, мы можем статически ставить, что p не является нулевым, и как таковое использование call вместо callvirt не нарушит это правило С# и, как таковая, является разумной оптимизацией.

В то время как if (a != null) a.b и т.д. является общей идиомой, требуется анализ, чтобы понять, что a не может быть нулевым в точке, в которой используется b. Добавление этого анализа в компилятор потребует выполнения работ, внедрения, тестирования и постоянного тестирования против ошибок регрессии, внесенных другими изменениями.

a?.b выходит за пределы идиомы, поскольку он использует оператор ?., о котором должен знать С#. Поэтому С# должен иметь код, чтобы превратить это в нулевую проверку, за которой следует доступ к члену. Поэтому компилятор должен знать, что в момент, когда происходит доступ к члену, a не является нулевым. Таким образом, логика "знать" использование call уже безопасна. Нет никакой дополнительной работы по анализу, чтобы понять, что call можно использовать.

Итак, для первого случая потребуется куча дополнительной работы для использования call и потенциально ввести ошибки, в то время как второй случай должен выполнить эту работу в любом случае, поэтому он также может быть.