Инициализаторы объектов (Object Initializers) – это полезная возможность языка C#, которая позволяет инициализировать необходимые свойства объекта прямо во время его создания. Поскольку синтаксически эта «фича» очень близка к инициализации объекта с передачей параметров через конструктор, многие разработчики начинают забивать на принципы ООП (в частности на понятие инварианта) и использовать ее, где только можно.
Но даже если не переходить к холиварам и малопонятным терминам, давайте рассмотрим небольшой пример, и подумаем над тем, может ли он привести к проблемам или нет:
В данном фрагменте внутри директивы using создается ресурс (файл) и устанавливается одно из его свойств (Position) с помощью инициализатора объекта. При этом самое главное в этом коде то, что setter этого свойства может генерировать исключение.
В отличие от языка C++ в .NET-ах мы довольно редко сталкиваемся с проблемами безопасностью исключений, но это один из тех редких случаев, когда код является небезопасным с точки зрения исключений.
Чтобы понять это, давайте посмотрим, как реализуются компилятором инициализатор объекта:
С первого взгляда может показаться, что инициализатор объекта – это не что иное, как вызов конструктора с последующим изменением его свойств. И, по сути, так оно и есть, только с небольшим уточнением:
Временная переменная является необходимым условием «атомарности» инициализации, без которой сторонний код (например, из другого потока) смог бы получить ссылку на объект в промежуточном состоянии. Кроме того, отсутствие временной переменной сделало бы код еще менее безопасным с точки зрения исключений, ведь тогда генерация исключения из setter-а свойства привело бы к частичной инициализации переменной:
В этом случае, если сеттер одного из свойств упадет с исключением, то поле _person будет уже проинициализировано, но не до конца, что нарушило бы «атомарность» инициализатора объектов, такую привычную по использованию конструкторов.
Однако, хотя временная переменная решает ряд проблем, этого не происходит в случае использования инициализатора объектов внутри директивы using. Как вы знаете, директива using разворачивается в такой код:
Теперь, если сложить 2 и 2, то мы получим, что наш исходный пример разворачивается в следующее:
И это означает, что если свойство, инициализируемое с помощью инициализатора объекта, упадет с исключением, метод Dispose нашего объекта вызван не будет.
Заключение
Инициализаторы объектов – это полезная возможность, но ее нужно еще и уметьготовить использовать. Хотя синтаксически (и семантически, кстати, тоже) эта возможность очень близка к вызову конструктора, но, к сожалению, они не всегда эквиваленты. Что касается принципов ООП, то тут вам самим решать, когда использовать инициализатор объектов, а когда нет, но если речь заходит о безопасности исключений и управлении ресурсами, то тут однозначно нужно понимать, как устроена эта возможность и что из этого следует.
Язык C# отличается довольно предсказуемым поведением в большинстве случаев (хотя есть и исключения, типа тонкостей с изменяемыми значимыми типами), но смешивание инициализаторов объектов с блоком using, интуитивностью не отличается и желательно понимать, как именно устроена эта комбинация, чтобы не отсрелить себе ногу по неосторожности.
Но даже если не переходить к холиварам и малопонятным терминам, давайте рассмотрим небольшой пример, и подумаем над тем, может ли он привести к проблемам или нет:
// position передается извне или настраиватся каким-то образом
long position = -1;
using (var file = new FileStream("d:\\1.txt", FileMode.Append)
{
// Мы точно знаем, что нужные данные расположены
// с некоторым сдвигом!
Position = position
})
{
// Делаем чего-то с файлом
}
В данном фрагменте внутри директивы using создается ресурс (файл) и устанавливается одно из его свойств (Position) с помощью инициализатора объекта. При этом самое главное в этом коде то, что setter этого свойства может генерировать исключение.
В отличие от языка C++ в .NET-ах мы довольно редко сталкиваемся с проблемами безопасностью исключений, но это один из тех редких случаев, когда код является небезопасным с точки зрения исключений.
Чтобы понять это, давайте посмотрим, как реализуются компилятором инициализатор объекта:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// ...
var person = new Person {Name = "John", Age = 42};
С первого взгляда может показаться, что инициализатор объекта – это не что иное, как вызов конструктора с последующим изменением его свойств. И, по сути, так оно и есть, только с небольшим уточнением:
var tmp = new Person();
tmp.Name = "Jonh";
tmp.Age = 42;
var person = tmp;
Временная переменная является необходимым условием «атомарности» инициализации, без которой сторонний код (например, из другого потока) смог бы получить ссылку на объект в промежуточном состоянии. Кроме того, отсутствие временной переменной сделало бы код еще менее безопасным с точки зрения исключений, ведь тогда генерация исключения из setter-а свойства привело бы к частичной инициализации переменной:
var tmp = new Person();
tmp.Name = "John";
tmp.Age = 42;
var person = tmp;
В этом случае, если сеттер одного из свойств упадет с исключением, то поле _person будет уже проинициализировано, но не до конца, что нарушило бы «атомарность» инициализатора объектов, такую привычную по использованию конструкторов.
Однако, хотя временная переменная решает ряд проблем, этого не происходит в случае использования инициализатора объектов внутри директивы using. Как вы знаете, директива using разворачивается в такой код:
var file = new FileStream("d:\\1.txt", FileMode.OpenOrCreate);
try
{}
finally
{
if (file != null)
((IDisposable)file).Dispose();
}
Теперь, если сложить 2 и 2, то мы получим, что наш исходный пример разворачивается в следующее:
long position = -1;
var tmpFile = new FileStream("d:\\1.txt", FileMode.OpenOrCreate);
// Упс! Если мы здесь упадем, то Dispose вызван не будет!
tmpFile.Position = position;
var file = tmpFile;
try
{ }
finally
{
if (file != null)
((IDisposable)file).Dispose();
}
И это означает, что если свойство, инициализируемое с помощью инициализатора объекта, упадет с исключением, метод Dispose нашего объекта вызван не будет.
Заключение
Инициализаторы объектов – это полезная возможность, но ее нужно еще и уметь
Язык C# отличается довольно предсказуемым поведением в большинстве случаев (хотя есть и исключения, типа тонкостей с изменяемыми значимыми типами), но смешивание инициализаторов объектов с блоком using, интуитивностью не отличается и желательно понимать, как именно устроена эта комбинация, чтобы не отсрелить себе ногу по неосторожности.