UPDATE: наткнулся на объяснение данного поведения Эриком Липпертом (подробности здесь).
Инициализаторы объектов (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 = "Jonh", Age = 42};
С первого взгляда может показаться, что инициализатор объекта – это не что иное, как вызов конструктора с последующим изменением его свойств. И, по сути, так оно и есть, только с небольшим уточнением:
var tmp = new Person();
tmp.Name = "Jonh";
tmp.Age = 42;
var person = tmp;
Временная переменная является необходимым условием «атомарности» инициализации, без которой сторонний код (например, из другого потока) смог бы получить ссылку на объект в промежуточном состоянии. Кроме того, отсутствие временной переменной сделало бы код еще менее безопасным с точки зрения исключений, ведь тогда генерация исключения из setter-а свойства привело бы частичной инициализации переменной:
// _person - это поле класса и, предположим, компилятор
// разворачивал бы инициализатор «по простому»
var _person = new Person();
_person.Name = "Jonh";
_person.Age = 42;
В этом случае, если сеттер одного из свойств упадет с исключением, то поле _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, интуитивностью не отличается и желательно понимать, как именно устроена эта комбинация, чтобы не отсрелить себе ногу по неосторожности.
ИМХО компилер нужно "обучить" генерить такой код:
ОтветитьУдалитьlong position = -1;
var tmpFile = new FileStream("d:\\1.txt", FileMode.OpenOrCreate);
FileStream file = null;
try
{
//Иницализатор работает здесь:
tmpFile.Position = position;
//...
//Конец инициализатора
file = tmpFile;
}
finally
{
if (tmpFile != null)
((IDisposable)tmpFile).Dispose();
else if (file != null)
((IDisposable)file).Dispose();
}
@Mastro Ombroj: у меня почему-то есть смутное подозрение, что ждать такого поведения от компилятора не стоит. Простой комбинацией двух этих фич подобный код не получить, а шансов, что под одну фичу будут жестко затачивать другую - мало, поскольку в конечном итоге это приведет к комбинаторному взрыву хаков, и дико усложнить понимание и реализацию этих возможностей.
ОтветитьУдалитьЗдесь, скорее, нужно понимать, чем это грозит, да и все. Подобных мест в C# довольно много. Вот, к примеру, виртуальные события, для какого количества разработчиков текущее поведение является интуитивно понятным?
Хочу добавить, что если запустить Code Analysis, то будет выдано предупреждение:
ОтветитьУдалитьWarning 1 CA2000 : Microsoft.Reliability : In method 'Program.Main(string[])', object '<>g__initLocal0' is not disposed along all exception paths. Call System.IDisposable.Dispose on object '<>g__initLocal0' before all references to it are out of scope.
(CA2000: удалите объекты до того, как будет потеряна область действия)
А вот ссылка на русском:
http://msdn.microsoft.com/ru-ru/library/ms182289.aspx
@biogenez: Да, здорово!
ОтветитьУдалитьЗдесь проявляется фича анализатора, что он анализирует не исходный код, а скомпилированный. Хотя для многих вид предупреждения может быть неожиданным:)
Сергей, спасибо за интересный лик без. Может быть Вам стоит написать статью по подобным узким местам? Так, сказать, из личного опыта.
ОтветитьУдалитьКстати, ссылка на "виртуальные события" неверна. Должна быть:
http://sergeyteplyakov.blogspot.com/2011/02/c.html
@Anton: я периодически как раз и пишу о подобных узких местах. То были виртуальные события (спасибо за правильную ссылку), то - замыкания, потом были изменяемые значимые типы и т.п.
ОтветитьУдалитьВсе эти заметки может быть стоит в одну рубрику объединить, чтобы их искать было легче.
Да было бы неплохо, или хотя бы общий тэг им присваивать.
ОтветитьУдалитьОффтоп: когда хочу оставить коммент, то рядом с мемкой коммента показывается аватар пользователя Mastro Ombroj (применительно к данной статье, вероятно это аватарка первого комментатора к статье). Так и должно быть?
ОтветитьУдалитьАвы в гуглоплюсе у меня нет.
@Anton: странно все это. Может глюки именно в форме редактирования, потому что в самих комментах аватарки нет.
ОтветитьУдалитьНикогда так не делал. Может подсознательно :). Но теперь сознательно так делать не буду :)... Показал на работе - обсудили. Знаешь, как-то аргументы о том, что инициализацию нельзя внести под try - не очень убедительны. foreach и замыкания в пятом дотнете же изменили поведение с переносом временной переменной в цикл. Мне кажется такое поведение возможно, но подозреваю это сложно, поэтому и не делается...
ОтветитьУдалить@eugene: ну, я не думаю, что эту фичу уж очень сложно реализовать, но ее цена/качество слишком низкое, чтобы Dev Team с нею заморачивался.
ОтветитьУдалитьВон, даже JetBrains пока не сделали ворнинга на такое поведение, так что шансов на исправления в компиляторе, думаю, мало.
Ну тогда не понятно. Поведение с внесением инициализации под try делает поведение ОДНОЗНАЧНО безопаснее. И если это "ничего не стоит" - тогда не понятно почему этого нет в запросах на улучшения.
ОтветитьУдалитьПо-моему, проблема надуманна. ЛЮБОЕ место, потенциально грозящее исключением, должно быть внутри ловушки. Тем более, с такой классической вещью как файлы.
ОтветитьУдалить@vt2012: насколько блок catch на высоком уровне поможет избежать недетерминированной очистки ресурсов, которая будет на уровне ниже?
ОтветитьУдалитьОпять таки, такая проблема можем быть не только с файлами, но и с любым другим disposable объектом, примем даже если сеттер его свойства не генерирует исключение.
Спасибо за блог. Очень интерестные статьи. Каждый раз узнаю чтото новое
ОтветитьУдалитьЯ под dotPeek и Reflector и даже под IL смотрю и в упор не вижу вот этой вот временной переменной...Может это зависит от версии Framework?
ОтветитьУдалитьНо вот инициализация последнего примера действительно проходит перед конструкцией using, а переменная уже записывается в using:
ОтветитьУдалитьPerson person1 = new Person {
Name = "John",
Age = 0x21
};
using (new FileStream(@"d:\1.txt", FileMode.OpenOrCreate))
{
}
long num = -1L;
FileStream stream1 = new FileStream(@"d:\1.txt", FileMode.OpenOrCreate) {
Position = num
};
using (stream1)
{
}