Engine & platform

Пользовательский оператор ==, стоит ли его оставить?

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
May 16, 2014|5 Мин
Пользовательский оператор ==, стоит ли его оставить?
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Когда Вы делаете это в Unity:

if (myGameObject == null) {}

Unity делает нечто особенное с оператором ==. Вместо того, что ожидает большинство людей, у нас есть специальная реализация оператора ==.

Это служит двум целям:

1) Когда у MonoBehaviour есть поля, только в редакторе[1], мы устанавливаем эти поля не в "настоящий null", а в объект "fake null". Наш пользовательский оператор == способен проверить, является ли что-то одним из этих поддельных объектов null, и ведет себя соответствующим образом. Хотя это и экзотическая установка, она позволяет нам хранить информацию в поддельном объекте null, что дает Вам больше контекстной информации, когда Вы вызываете метод или запрашиваете у объекта какое-либо свойство. Без этого трюка Вы получили бы только NullReferenceException, трассировку стека, но не имели бы ни малейшего представления о том, в каком GameObject'е находится MonoBehaviour с полем, в котором был null. С помощью этого трюка мы можем выделить GameObject в инспекторе, а также дать Вам больше указаний: "Похоже, Вы обращаетесь к неинициализированному полю в этом MonoBehaviour, используйте инспектор, чтобы поле указывало на что-то".

Цель номер два немного сложнее.

2) Когда Вы получаете объект c# типа "GameObject"[2], он почти ничего не содержит. Это потому, что Unity - движок на C/C++. Вся фактическая информация об этом GameObject'е (его имя, список компонентов, которые он имеет, его флаги HideFlags и т.д.) находится на стороне c++. Единственное, что есть у объекта c# - это указатель на родной объект. Мы называем эти объекты c# "объектами-обертками". Время жизни этих объектов на языке c++, таких как GameObject и все остальное, что происходит от UnityEngine.Object, явно управляется. Эти объекты уничтожаются при загрузке новой сцены. Или когда Вы вызываете Object.Destroy(myObject); на них. Время жизни объектов c# управляется способом c#, с помощью сборщика мусора. Это означает, что можно иметь объект-обертку на c#, который все еще существует, и который обертывает объект на c++, который уже уничтожен. Если Вы сравните этот объект с null, наш пользовательский оператор == в этом случае вернет "true", даже если на самом деле переменная c# не является null.

Хотя эти два варианта использования вполне разумны, пользовательская проверка null также имеет ряд недостатков.

- Это контринтуитивно.

- Сравнение двух объектов UnityEngine.Objects друг с другом или с null происходит медленнее, чем можно было бы ожидать.

- Пользовательский оператор == не является потокобезопасным, поэтому Вы не можете сравнивать объекты вне основного потока. (Это мы можем исправить).

- Он ведет себя несовместимо с оператором ??, который также выполняет проверку на null, но этот оператор выполняет проверку на null в чистом c#, и его нельзя обойти, чтобы вызвать нашу пользовательскую проверку на null.

Если бы мы создавали наш API с нуля, мы бы предпочли не делать пользовательскую проверку на null, а использовать свойство myObject.destroyed, чтобы проверить, мертв объект или нет, и просто смириться с тем, что мы больше не можем выдавать лучшие сообщения об ошибках, если Вы вызываете функцию для поля, которое является null.

Мы думаем о том, стоит ли это менять. Это шаг в нашем бесконечном стремлении найти правильный баланс между "чинить и приводить в порядок старые вещи" и "не ломать старые проекты". В этом случае нам интересно Ваше мнение. Для Unity5 мы работали над возможностью автоматического обновления скриптов в Unity (подробнее об этом в одном из следующих постов блога). К сожалению, мы не сможем автоматически обновить Ваши скрипты для этого случая. (потому что мы не можем отличить "это старый скрипт, который на самом деле хочет старого поведения", от "это новый скрипт, который на самом деле хочет нового поведения").

Мы склоняемся к варианту "удалить пользовательский оператор ==", но сомневаемся, потому что это изменит смысл всех проверок на null, которые сейчас выполняют Ваши проекты. А для случаев, когда объект не "действительно null", а разрушенный объект, проверка на null раньше возвращала true, и если мы изменим это, то она будет возвращать false. Если бы Вы хотели проверить, указывает ли Ваша переменная на уничтоженный объект, Вам бы пришлось изменить код, чтобы вместо этого проверять "if (myObject.destroyed) {}". Мы немного переживаем по этому поводу, поскольку если Вы не читали этот пост в блоге, а скорее всего, если читали, то очень легко не понять, что поведение изменилось, тем более что большинство людей вообще не подозревают о существовании этой пользовательской проверки на null.[3]

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

Что бы Вы предпочли, чтобы мы сделали? Обеспечили бы Вам более чистый опыт за счет того, что Вам пришлось бы менять проверки null в своем проекте, или оставили все как есть?

Пока, Лукас(@lucasmeijer)

[1] Мы делаем это только в редакторе. Вот почему, когда Вы вызываете GetComponent(), чтобы запросить несуществующий компонент, Вы видите, как происходит выделение памяти в C#, потому что мы генерируем эту пользовательскую строку предупреждения внутри только что выделенного поддельного объекта null. Такое распределение памяти не происходит во встроенных играх. Это очень хороший пример того, почему если Вы профилируете свою игру, Вы всегда должны профилировать реального автономного игрока или мобильного игрока, а не редактор, поскольку мы делаем много дополнительных проверок безопасности / защиты / использования в редакторе, чтобы сделать Вашу жизнь проще, за счет некоторой производительности. При профилировании производительности и распределения памяти никогда не профилируйте редактор, всегда профилируйте собранную игру.

[2] Это справедливо не только для GameObject, но и для всего, что происходит от UnityEngine.Object.

[3] Забавная история: Я столкнулся с этим, оптимизируя производительность GetComponent<T>(), и, реализовав некоторое кэширование для компонента трансформации, я не увидел никакого выигрыша в производительности. Затем @jonasechterhoff рассмотрел эту проблему и пришел к тому же выводу. Код кэширования выглядит следующим образом:

private Transform m_CachedTransform
public Transform transform
{
  get
  {
    if (m_CachedTransform == null)
      m_CachedTransform = InternalGetTransform();
    return m_CachedTransform;
  }
}

Оказалось, что два наших инженера не заметили, что проверка null была более дорогостоящей, чем ожидалось, и была причиной того, что кэширование не давало никакого преимущества в скорости. Это привело к размышлениям на тему "если даже мы пропустили это, то сколько же наших пользователей пропустят это?", результатом которых стал этот блогпост :)