Автор: Sergey Teplyakov
Недавно один из читателей задал вопрос по поводу разницы между двумя реализациями синглтонов: через обычное статическое поле или же через статическое поле, содержащее Lazy<,T>,: public class FieldLikeSingleton{ // Вариант C# 6 public static FieldLikeSingleton Instance { get, } = new FieldLikeSingleton(),private FieldLikeSingleton() {}}public class FieldLikeLazySingleton{ private static readonly Lazy<,FieldLikeLazySingleton>, _instance =new Lazy<,FieldLikeLazySingleton>,(() =>, new FieldLikeLazySingleton()),public static FieldLikeLazySingleton Instance =>, _instance.Value,private FieldLikeLazySingleton() {}}
Для простоты, первую реализацию я буду называть field-like реализацией (*), а вторую ленивой.
Существует 2 ключевых отличия в поведения этих двух реализаций, о которых нужно знать заранее. Для некоторых классов или приложений разница может оказаться несущественной, но для других она может быть критической.
(*) Название field-like уходит корнями в существующую терминологию, как например, field-like events. Да, с C# 6, field-like реализация теперь содержит свойство, но классическая реализация была основана именно на закрытом поле.
Все различия происходят из-за того, что в первом случае тело конструктора синглтона вызывается в статическом конструкторе, а во-втором случае в статическом конструкторе вызывается лишь конструктор объекта Lazy<,T>,, а сам объект создается при первом обращении.
1. Время инициализации объекта-синглтона
- Field-like синглтон будет проинициализирован при вызове статического конструктора (а не только при обращении к свойству Instance).
- Правила, определяющие время инициализации типа довольно запутаны. Например, при отсутствии явного статического конструктора синглтон может быть проинициализирован перед вызовом метода, в котором он может использоваться (подробнее об этом в статье О синглтонах и статичечских конструкторах).
Вот небольшой пример:class FieldLikeSingleton{ public static FieldLikeSingleton Instance { get, } = new FieldLikeSingleton(),private FieldLikeSingleton() { Console.WriteLine('FieldLikeSingleton.ctor'), }public void Foo() { Console.WriteLine('Foo'),}}class Program{ static void Main(string[] args) { Console.WriteLine('Inside Main()'), if (args.Length == 42) { Singletons.FieldLikeSingleton.Instance.Foo(), } }}
При запуске в релизе, на экране нас ждут:
FieldLikeSingleton.ctorInside Main()
Поскольку у синглтона нет явного статического конструктора, то правила инициализации типа несколько ослабляются и она происходит до вызова метода. В случае с ленивым синглтоном, мы его проинициализируем непосредственно в момент обращения к свойству Instance.
2. Проблемы с исключениями
- Исключение, возникшее в конструкторе field-like синглтона будет обернуто в TypeLoadException.
- Конструктор field-like синглтона будет вызван только один раз.
- В случае исключение тип синглтона окажется в нерабочем состоянии
Это наиболее ключевое отличие между ленивой и field-like реализацией. В случае field-like синглтона исключения в конструкторе являются по сути фатальными. Когда статический конструктор генерирует исключение, то весь тип помечается невалидным: любое обращение завершается с TypeLoadException (внутри которого будет храниться исходное исключение):class FieldLikeSingleton{ public static FieldLikeSingleton Instance { get, } = new FieldLikeSingleton(),private FieldLikeSingleton() { throw new DataException('Oops!'), }public void Foo() { Console.WriteLine('Foo'),}}class Program{ static void Main(string[] args) { try { FieldLikeSingleton.Instance.Foo(), } catch (TypeLoadException e) { // e.InnerException is of type DataException Console.WriteLine(e.InnerException), }Console.WriteLine('Done'), }}
Исключение, завернутое в TypeLoadException, по сути, становится необрабатываемым. Мы можем перехватить его в высокоуровневом catch(Exception), но там мы точно ничего не сможем с ним сделать. Наш тип является невалидным и любое обращение даже к любому статическому члену приведет к исключению. По сути, исключение в конструкторе такого синглтона должно приводить к прекращению работы приложения, поскольку его нормальная работа невозможна.
Ленивый синглтон этой проблемой не обладает: если конструктор такого синглтона упадет в первый раз, то поле _instance останется непроинициализированным и при следующем обращении к синглтону будет еще одна попытка инициализации.
Так что выбрать?
Понятно, что исключения в конструкторах синглтонах это дело нехорошее, но ведь далеко не всегда очевидно, может оно произойти или нет. А если произойдет, то будет ли оно восстанавливаемым или временным (transient)? Если это критическое исключение, то тут все просто: в любом случае нужно крэшить приложение, надеясь на то, что в логах будет полное сообщение, а не лишь свойство Message.
Если же исключение является временным (конструктор обращается к файлу или другому ресурсу, который может быть временно недоступен), то ленивый синглтон позволит восстановить работу приложения, а field-like нет. Это может быть особенно важным в случае сервисов, который сможет восстановить свою работу в одном случае, и не сможет в другом.
Я практически всегда отдаю предпочтение ленивой версии. Она на одну строку длиннее, но это небольшая цена за более вменяемый стек исключений в случае ошибки в конструкторе. Если же Lazy<,T>, недоступен (используется более старая версия фреймворка), то для простых типов я использую field-like синглтон, а для сложных старый добрый double-checking lock.
Дополнительный ссылки
О синглтонах и статичечских конструкторах