Share via


Упаковывать или не упаковывать, вот в чем вопрос

Предположим, что у нас есть неизменяемый значимый тип (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. Факт отсутствия упаковки невидим и недоступен и, таким образом, не существует способа понять, что этот этап пропущен.

Мораль этой истории следующая: изменяемые значимые типы являются достаточным злом, чтобы разорвать вас на мелкие кусочки , поэтому их нужно избегать.

Оригинал статьи