Автор: 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.ctor
Inside 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.

Дополнительный ссылки

О синглтонах и статичечских конструкторах

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).