Pull to refresh

Инициализаторы объектов в блоке using

Reading time3 min
Views13K
Инициализаторы объектов (Object Initializers) – это полезная возможность языка C#, которая позволяет инициализировать необходимые свойства объекта прямо во время его создания. Поскольку синтаксически эта «фича» очень близка к инициализации объекта с передачей параметров через конструктор, многие разработчики начинают забивать на принципы ООП (в частности на понятие инварианта) и использовать ее, где только можно.

Но даже если не переходить к холиварам и малопонятным терминам, давайте рассмотрим небольшой пример, и подумаем над тем, может ли он привести к проблемам или нет:

// 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, интуитивностью не отличается и желательно понимать, как именно устроена эта комбинация, чтобы не отсрелить себе ногу по неосторожности.
Tags:
Hubs:
Total votes 44: ↑40 and ↓4+36
Comments32

Articles