Почему проверка нулевого значения выполняется после оценки списка аргументов?

В соответствии со спецификацией языка C# 7.4.3 Вызов члена функции Обработка во время выполнения вызова члена функции состоит из следующих шагов, где M - это элемент функции экземпляра, объявленный в ссылочном типе, E - выражение экземпляра:

  1. E оценивается. Если эта оценка вызывает исключение, дальнейшие шаги не выполняются.
  2. Список аргументов оценивается.
  3. Если тип E является типом значения, выполняется преобразование в бокс для преобразования E в объект типа, и E рассматривается как объект типа на следующих этапах. В этом случае M может быть только членом System.Object.
  4. Значение E проверяется, чтобы быть действительным. Если значение E равно нулю, выдается исключение System.NullReferenceException и дальнейшие шаги не выполняются.
  5. Определена реализация члена функции для вызова... и т. Д.

Мне интересно, почему проверка нуля не является вторым шагом? Зачем оценивать список аргументов, если E - ноль?

4 ответа

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

Как и сейчас, подавляющему большинству методов не нужно проверять, что экземпляр не нулевой. Вместо этого они пытаются вызвать метод, и если экземпляр имеет значение null, то попытка получить таблицу методов для этого приводит к недопустимому доступу к памяти, который затем перехватывается и превращается в NullReferenceException по рамкам. Здесь нет больше работы для исполняемого кода, чем было бы, если бы априори было известно, что он не равен нулю.

Единственный раз, когда экземпляр должен быть явно проверен на ненулевое значение, это когда оптимизация означает:

  1. Звонок был удален путем встраивания.
  2. Встроенный вызов не требует доступа к полю (в любом случае это приведет к исключению нулевой ссылки).
  3. Встроенный вызов не включает в себя еще один вызов для того же объекта (то же самое).
  4. Нельзя показать, что экземпляр определенно не равен нулю (или не будет проблем).

В этом случае доступ к полю добавляется для запуска NullReferenceException так же, как вызов.

Однако, если правила требовали нулевой проверки перед тем, как аргументы были оценены, необходимо было бы добавить явную проверку для каждого отдельного вызова. И все это будет означать на практике, что вы имели NullReferenceException брошенный до попытки чего-то, что привело бы к NullReferenceException быть брошенным (Они не могли удалить логику, которая превращает нарушение доступа к памяти с низким адресом в NullReferenceException потому что это все еще подходит другим путям).

Таким образом, правила, которые вы предлагаете, на практике потребуют намного больше работы.

Связанные с:

В C# добавлено правило, запрещающее вызов методов только в тех случаях, когда оно уже использовалось для внутреннего использования при разработке.NET, но еще не было опубликовано.

В конце концов, совершенно законно вызывать не виртуальный метод для нулевого экземпляра в.NET в целом, компилируя в инструкцию CIL call скорее, чем callvirt, (В этом отношении вы можете вызывать виртуальный метод не виртуально так же, как вызов base работает). Это будет работать до тех пор, пока в экземпляре нет доступа к полям или виртуальных вызовов методов (что на практике встречается редко, но может случиться).

До этого правилом было то, что нулевая проверка требовалась, только если метод был виртуальным.

Это было сделано так же, как и раньше; вызвать метод с callvirt и поймать нарушение доступа к памяти, если он вызывается по нулевой ссылке.

Когда правило было изменено, чтобы (к сожалению, IMO) запретить любой вызов нулевых объектов, это было сделано путем изменения компиляции для использования callvirt даже если метод не является виртуальным, поэтому нарушение доступа к памяти происходит, если экземпляр имеет значение null, и в результате NullReferenceException вытекает.

Я думаю, что в целом это вопрос определения, хотя я могу придумать несколько причин, почему это удобный порядок:

  • По сути, методы объектов - это функции в таблице функций, для которых ссылка на объект (или указатель) является еще одним аргументом. Я не уверен, что такое соглашение о вызовах, но правило для оценки аргумента слева направо. Если соглашение ставит указатель справа, имеет смысл, что он проверяется последним, потому что его значение уже известно.
  • Точно так же, если вы считаете, this- ссылка на (неявный) аргумент вызова метода, на самом деле это первый аргумент, который нужно проверить, прежде чем любые проверки аргументов вы написали сами в теле метода. Что имеет смысл: все аргументы оцениваются, и после оценки все они проверяются, this-ссылка первая.
  • Если аргумент сам по себе является выражением с побочным эффектом, имеет смысл очень четко определить порядок оценки. В этом случае побочный эффект всегда срабатывает (если более ранний аргумент не вызывает исключение).
  • Если у вас есть библиотека для предварительных и постусловий, они должны знать значения аргументов перед вызовом метода. Вы хотели бы знать, являются ли аргументы действительными независимо от того, является ли объект, к которому они вызваны, null или нет.
  • Перед этой проверкой может потребоваться сделать бокс *, что является потенциально относительно дорогим шагом. Вы хотели бы сделать это самое последнее возможное.
  • Методы расширения могут применяться к null объект. Этот порядок гарантирует, что методы расширения и методы экземпляра ведут себя одинаково (в методе расширения вы можете проверять наличие нуля только внутри тела метода, т.е. после оценки аргументов).
  • Объект не обязательно должен находиться в текущей системе или в памяти текущего процессора. В OO parlor (например, Бертран Мейер) вызов метода по сути отправляет сообщение объекту. Очевидно, что с этой точки зрения, сообщение должно быть сначала создано, прежде чем оно может быть отправлено. В классических COM и DCOM это аналогично: сообщение создается (т.е. оцениваются аргументы), а затем отправляется. Если цель окажется несуществующей, исчезнет, ​​уничтожится, возникнет ошибка. Но порядок в этом сценарии не может быть другим. Я не уверен, что это был аргумент здесь (имеющий дело с внешними объектами), но это могло быть связано с совместимостью COM.

Я понимаю, что у каждого из этих аргументов может быть контраргумент (кроме, возможно, последнего), но в целом, я думаю, что это способствует проверке объекта на null как можно позже.

* Бокс обычно совсем не дорогой, но если ваш список аргументов небольшой (ноль или единица) и не требует дополнительной оценки, бокс является относительно дорогим по сравнению с виртуальным запретом на оценку аргументов (обновляется из-за комментария hvd).

Оценка аргументов функций на самом деле отличается в некоторых других парадигмах программирования, таких как функциональное программирование с LISP или логическое программирование с Прологом и т. Д.

Но в процедурных и объектно-ориентированных языках программирования обычно выполняется оценка параметров функции перед выполнением фактического вызова. Я не знаю, является ли это обязательным условием, но оно используется таким же образом в C, C++, Java, C#, Pascal и т. Д. Они следуют тем же принципам.

Однако не смешивайте это с условиями оценки, к которым применяется правило короткого замыкания.

Возможно, потому что на шаге 3 тип E может быть преобразован в ноль. Имея фильтр на шаге 2, вы можете разрешить передачу значений, которые могут быть установлены на ноль на шаге 3, таким образом, требуется другой фильтр.

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