Для демонстрации примеров я буду использовать класс Test, у которого есть свойство BlobData, возвращающее объект типа Blob, который по легенде создаётся довольно медленно, и было решено создавать его лениво.
class Test{ public Blob BlobData { get { return new Blob(); } }}
Проверка на null
Самый простой вариант, доступный с первых версий языка, это создание неинициализированной переменной и проверка её на null перед возвращением. Если переменная равна null, создаём объект и присваиваем этой переменной, а потом его возвращаем. При повторном обращении объект уже будет создан и мы сразу его вернём.
class Test{ private Blob _blob = null; public Blob BlobData { get { if (_blob == null) { _blob = new Blob(); } return _blob; } }}
Объект типа Blob тут создаётся при первом обращении к свойству. Либо не создаётся, если он по какой-то причине в этой сессии программе не понадобился.
Тернарный оператор ?:
В C# есть тернарный оператор, позволяющий проверить условие и, если оно истинно вернуть одно значение, а если ложно, другое. Мы можем использовать его для того, чтобы немного сократить и упростить код.
class Test{ private Blob _blob = null; public Blob BlobData { get { return _blob == null ? _blob = new Blob() : _blob; } }}
Суть осталась той же. Если объект не инициализирован, инициализируем и возвращаем. Ежели уже инициализирован, то просто сразу вовзращаем.
is null
Ситуации бывают разные и мы, например, можем столкнуться с такой, в которой у класса Blob перегружен оператор ==. Для этого, вероятно, нам может потребоваться сделать проверку is null вместо == null.
return _blob is null ? _blob = new Blob() : _blob;
Но это так, небольшое отступление.
Null-coalescing оператор ??
Ещё больше упростить код нам поможет бинарный оператор ??
Суть его работы такова. Если первый операнд не равен null, то он и возвращается. Если же первый операнд равен null, возвращается второй.
class Test{ private Blob _blob = null; public Blob BlobData { get { return _blob ?? (_blob = new Blob()); } }}
Второй операнд пришлось взять в круглые скобки из-за приоритета операций.
Оператор ??=
В C# 8 появился null-coalescing assignment operator, выглядящий вот так ??=
Принцип его работы заключается в следующем. Если первый операнд не равен null, то он просто возвращается. Если первый операнд равен null, то ему присваивается значение второго и возвращается уже это значение.
class Test{ private Blob _blob = null; public Blob BlobData { get { return _blob ??= new Blob(); } }}
Это позволило ещё немного сократить код.
Потоки
Если есть вероятность, что к данному ресурсу могут обращаться сразу несколько потоков, нам стоит сделать его потокобезопасным. В противном случае может случиться такая ситуация, что, например, оба потока проверят объект на null, результат окажется false, а затем будет создано два объекта типа Blob, нагрузив систему в два раза больше, чем нам хотелось, и кроме того, один из этих объектов сохранится, а второй будет потерян.
class Test{ private readonly object _lock = new object(); private Blob _blob = null; public Blob BlobData { get { lock (_lock) { return _blob ?? (_blob = new Blob()); } } }}
Оператор lock получает взаимоисключающую блокировку заданного объекта перед выполнением определенных операторов, а затем снимает блокировку. Он является эквивалентом использования метода System.Threading.Monitor.Enter(..., ...);
Lazy<T>
В .NET 4.0 появился класс Lazy, позволяющий скрыть всю эту грязную работу от наших глаз. Теперь мы можем оставить только локальную переменную типа Lazy. При обращении к его свойству Value, мы получим объект класса Blob. Если объект был создан ранее, он сразу вернётся, если нет сначала будет создан.
class Test{ private readonly Lazy<Blob> _lazy = new Lazy<Blob>(); public Blob BlobData { get { return _lazy.Value; } }}
Так как у класса Blob есть конструктор без параметров, то Lazy сможет создать его в нужный момент без лишних вопросов. Если же нам нужно выполнить какие-то дополнительные действия во время создания объекта Blob, конструктор класса Lazy может принимать ссылку на Func<T>
private Lazy<Blob> _lazy = new Lazy<Blob>(() => new Blob());
Кроме того, во втором параметре конструктора мы можем указать, нужна ли нам потокобезопасность (тот самый lock).
Свойство
Теперь давайте сократим запись readonly свойства, благо современный C# позволяет это делать красиво. В конечном итоге выглядеть всё это станет так:
class Test{ private readonly Lazy<Blob> _lazy = new Lazy<Blob>(); public Blob BlobData => _lazy.Value;}
LazyInitializer
Ещё есть вариант не оборачивать класс в обёртку Lazy, а вместо этого использовать функционал LazyInitializer. Этот класс имеет один статический метод EnsureInitialized с кучей перегрузок, позволяющих творить всякое, в том числе делать потокобезопасность и писать кастомный код для создания объекта, но основная суть которого заключается в следующем. Проверить, не инициализирован ли объект. Если нет, то инициализировать. Вернуть объект. С использованием данного класса, мы можем переписать наш код так:
class Test{ private Blob _blob; public Blob BlobData => LazyInitializer.EnsureInitialized(ref _blob);}
На этом всё. Спасибо за внимание.