Упаковывать или не упаковывать, вот в чем вопрос
Предположим, что у нас есть неизменяемый значимый тип (value type), который реализует интерфейс IDisposable. Предположим, он представляет собой некоторый дескриптор.
struct MyHandle : IDisposable
{
public MyHandle(int handle) : this() { this.Handle = handle; }
public int Handle { get; private set; }
public void Dispose()
{
Somehow.Close(this.Handle);
}
}
Вы можете размышлять следующим образом, «я уменьшу вероятность повторного закрытия дескриптора путем изменения структуры в методе Dispose!»
public void Dispose()
{
if (this.Handle != 0)
Somehow.Close(this.Handle);
this.Handle = 0;
}
Это должно зажечь тревожный огонек у вас в голове. Мы изменяем значимый тип, что, как мы знаем, является опасным, поскольку значимые типы копируются по значению; вы изменяете переменную, и разные переменные могут содержать копии одного и того же значения. Изменение одной переменной не приведет к изменению остальных, если только при изменении переменной, содержащей 12 не изменять в программе все переменные, содержащие 12. Но давайте пока не будем об этом.
Что будет в этом случае?
var m1 = new MyHandle(123);
try
{
// выполняем некоторые действия
}
finally
{
m1.Dispose();
}
// проверка корректности
Debug.Assert(m1.Handle == 0);
Все нормально в этом коде?
Да, все в порядке. При создании m1 Handle устанавливается равным 123, а после вызова Dispose он становится равным 0.
А как насчет этого кода?
var m2 = new MyHandle(123);
try
{
// выполняем некоторые действия
}
finally
{
((IDisposable)m2).Dispose();
}
// проверка корректности
Debug.Assert(m2.Handle == 0);
А здесь мы получим то же самое поведение? Приведение объекта к его интерфейсу ни к чему плохому не приводит, правда?
.
.
.
.
.
.
.
.
Нет, не правда. В этом коде происходит упаковка m2. Упаковка создает копию, и метод Dispose вызывается для копии, и, таким образом, изменяется именно копия. Значение m2.Handle остается равным 123.
Так что приводит к такому поведению и почему?
var m3 = new MyHandle(123);
using(m3)
{
// выполняем некоторые действия
}
// проверка корректности
Debug.Assert(m3.Handle == 0);
.
.
.
.
.
.
.
.
.
.
На основе предыдущего примера вы, вероятно думаете, что переменная m3 будет упакована, изменения произойдут именно в упакованном значении и, таким образом, утверждение сработает. Правильно?
Правильно?
Вы так думаете?
Ваши рассуждения о том, что упаковка произойдет в блоке finally, абсолютно оправданы, поскольку именно об этом говорится в спецификации. В спецификации сказано, что блок “using” для не-nullable типов разворачивается следующим образом:
finally
{
((IDisposable)resource).Dispose();
}
Однако сегодня я хочу сказать, что в нашей реализации языка C# освобождаемый ресурс на самом деле не упаковывается. В компиляторе есть оптимизация: он определяет, что метод Dispose находится непосредственно в значимом типе, а значит эффективнее сгенерировать следующий вызов:
finally
{
resource.Dispose();
}
происходит без преобразования типов и, таким образом, без упаковки.
Теперь, когда вы об этом узнали, поменяли ли вы свое мнение? Сработает ли утверждение? Если да, то почему? И если нет, то почему?
Подумайте хорошенько.
.
.
.
.
.
.
.
.
.
.
Утверждение сработает, хотя там и нет упаковки. Важной строкой спецификации является не та, в которой говорится о преобразовании типов; это был отвлекающий маневр. Важным фрагментом спецификации является следующий:
Оператор using вида "using (ResourceType resource = expression) statement" соответствует одному из трех возможных вариантов преобразования. [...] Оператор using вида "using (expression) statement" также имеет три варианта преобразования, но в этом случае, ResourceType неявно является типом времени компиляции (compile-time type) и переменная с ресурсом недоступна и невидима внутри вложенного выражения.
Таким образом, наш код эквивалентен следующему:
var m3 = new MyHandle(123);
using(MyHandle invisible = m3)
{
// выполнение некоторых действий
}
// проверка корректности
Debug.Assert(m3.Handle == 0);
что эквивалентно
var m3 = new MyHandle(123);
{
MyHandle invisible = m3;
try
{
// выполнение некоторых действий
}
finally
{
invisible.Dispose(); // благодаря оптимизации, упаковки нет
}
}
// проверка корректности
Debug.Assert(m3.Handle == 0);
Это невидимая копия освобождается и изменяется, а не переменная m3.
Вот почему компилятор может избавиться от упаковки в блоке finally. Факт отсутствия упаковки невидим и недоступен и, таким образом, не существует способа понять, что этот этап пропущен.
Мораль этой истории следующая: изменяемые значимые типы являются достаточным злом, чтобы разорвать вас на мелкие кусочки , поэтому их нужно избегать.