February 3, 2019

Упаковка или не упаковка (перевод)


!!НАШ БЛОГ ПЕРЕЕХАЛ!!

Мы создали свой сайт! Все материалы, опубликованные в этом блоге, переехали туда.

Наш новый сайт maddevelop.ru


Как и было обещано, в посте с разбором вопросов, публикуем перевод статьи Эрика Липперта "To box or not to box".

Предположим имеется неизменяемый тип значения, реализующий интерфейс 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);
  }
}

Можно подумать, что вероятность двукратного уничтожения одного и того же объекта класса дескриптора уменьшится если заставить структуру измениться при удалении.

public void Dispose()
{
  if (this.Handle != 0)
    Somehow.Close(this.Handle);
  this.Handle = 0;
}

Такая ситуация должна сразу Вас насторожить. Изменение значимого типа, как известно, опасно, потому что они копируются по значению. При изменении переменной другие переменные могут оставить копию исходного значения. Изменение одной переменной не изменяет другие, точно так же, как и изменение переменной, содержащей 12, меняет каждую переменную, которая также содержит 12.

Что делает этот код?

var m1 = new MyHandle(123);
try
{
  // do something
}
finally
{
  m1.Dispose();
}
// Sanity check
Debug.Assert(m1.Handle == 0);

Здесь все работает хорошо. При создании объекта m1 свойству Handle присваивается значение 123, а после его уничтожения свойство устанавливается в 0.

А что на счет этого кода?

var m2 = new MyHandle(123);
try
{
  // do something
}
finally
{
  ((IDisposable)m2).Dispose();
}
// Sanity check
Debug.Assert(m2.Handle == 0);

Здесь происходит тоже самое? Ведь, приведение объекта к типу интерфейса, который он реализует, ничего не делает, так ли?

........

........

........

Неправильно! Здесь происходит упаковка объекта m2. При упаковке создается копия и эта копия будет уничтожена, и следовательно объект копирования m2.Handle так и останется со своим значением 123.

А что происходит здесь и почему?

var m3 = new MyHandle(123);
using(m3)
{
  // Do something
}
// Sanity check
Debug.Assert(m3.Handle == 0);

........

........

........

Основываясь на предыдущем примере можно подумать, что здесь происходит упаковка m3, изменения происходят над упакованным экземпляром и проверка не срабатывает. Эта мысль подкрепляется еще тем, что так написано в спецификации. Там говорится, что при передачи в оператор using() объект типа значения не поддерживающий null в блоке finally будет происходить приведение к типу интерфейса, т.е. упаковка.

finally
{
  ((IDisposable)resource).Dispose();
}

Тем не менее, мы покажем, что в нашей реализации C# удаляемые ресурсы не обязательно должны быть упакованы. Это происходит из-за того, что компилятор имеет оптимизацию: если он определит, что метод Dispose выполняется над значимым типом, то будет выполняться код:

finally
{
resource.Dispose();
}

Без приведения типов, и соответственно без упаковки.

Но утверждение все равно не будет верным, даже в случае отсутствия процесса упаковки. Существует еще одна часть в спецификации, которая объясняет преобразования, происходящие при использовании оператора using().

A using statement of the form “using (ResourceType resource = expression) statement” corresponds to one of three possible expansions. […] A using statement of the form “using (expression) statement” has the same three possible expansions, but in this case ResourceType is implicitly the compile-time type of the expression, and the resource variable is inaccessible in, and invisible to, the embedded statement.

Значит предыдущий блок кода можно переписать следующим образом:

var m3 = new MyHandle(123);
using(MyHandle invisible = m3)
{
  // Do something
}
// Sanity check
Debug.Assert(m3.Handle == 0);

что, в свою очередь, эквивалентно

var m3 = new MyHandle(123);
{
  MyHandle invisible = m3;
  try
  {
    // Do something
  }
  finally
  {
    invisible.Dispose(); // No boxing, due to optimization
  }
}
// Sanity check
Debug.Assert(m3.Handle == 0);

Это и есть та невидимая копия, которая изменяется и уничтожается, она не является m3.

И вот почему компилятор может обойтись без упаковки в блоке finaly. Конечно, нет никакого способа отследить наличие процесса упаковки.

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



Ещё больше интересной информации на нашем Telegram канале.