Россия |
Введение и обзор платформы .NET
Ссылочные типы
Ссылочные типы представляют собой указатель на ту или иную структуру и потому переменные ссылочных типов всегда выделяются в "куче". При любом обращении к значению ссылочных типов происходит разыменовывание указателя, поэтому они считаются несколько более тяжеловесными, чем типы-значения. При создании ссылочные типы инициализируются значением null. Попытка обратиться к значению указателя, равного null, приводит к появлению NullPointerException (эта исключительная ситуация не может возникнуть при использовании типов-значений). При присваивании ссылочных типов происходит копирование адреса, а не значения переменной, поэтому изменение значений одной переменной может повлиять на другие, указывающие на тот же объект.
Так как ссылочные типы выделяются в "куче", то после того, как они отрабатывают, они должны быть зачищены сборщиком мусора (напомним, что типы-значения уничтожаются сразу же по выходу из блока или метода, в которых они определены). Для грамотного уничтожения ссылочных типов рекомендуется описывать собственный метод Finalize, который вызывается сборщиком мусора.
В C# ссылочные типы создаются с помощью ключевого слова class:
class RectRef {public int x, y, cx, cy; }
Упаковка и распаковка
Зачастую возникает необходимость в интерпретации типа-значения как ссылочного типа. Например, в следующем примере мы добавляем тип-значение в коллекцию:
ArrayList a = new ArrayList(); for (int i=0; i < 10; i++) { Point p; // Allocate a Point (not in the heap) p.x = p.y = i; // Initialize members in the value type a.Add(p); // here we're boxing the value type... }
Для добавления в массив нам необходимо преобразовать значение в ссылочный тип, т.к. метод Add принимает на вход только параметры типа Object. Процесс преобразования типа-значения в ссылочный тип называется упаковкой (boxing). Естественно, существует и обратный процесс, который называется распаковкой (unboxing). Отметим, что ссылочный тип существует только в упакованной форме, а тип-значение может находиться как в упакованной, так и в распакованной форме.
Как работает распаковка
Теперь разберемся с обратной операцией, распаковкой:
- Проверяется, что исходная ссылочная переменная не равняется null и что она ссылается на значение, полученное упаковкой ожидаемого типа-значения. Если какое-либо из этих условий неверно, то выдается InvalidCallException.
- Если же типы совпадают, то возвращается указатель на содержимое ссылочного типа (без учета накладных расходов, связанных с организацией объекта).
Важно понимать, что упаковка всегда копирует значение при создании объекта, а распаковка ничего не копирует, а просто возвращает прямую ссылку на само значение (хотя чаще всего результат распаковки все равно куда-нибудь копируется).
На следующем слайде мы рассмотрим пример, иллюстрирующий процесс упаковки и распаковки.
Пример упаковки и распаковки
Пример упаковки и распаковки
public static void Main() { Int32 v = 5; // creating unboxed value type variable Object o = v; // o refers to boxed version of v v = 123; // changes the unboxed value to 123 Console.WriteLine (v + "," + (Int32) o); // displays "123, 5" }
Вопрос: сколько раз в данном примере производится операция упаковки?
Правильный ответ: операция упаковки производится ровно 3 раза. Дополнительная операция возникает внутри Console.WriteLine, так как оператор ' + ' означает неявный вызов метода Concat, который ожидает переменные типа Object в качестве параметров. Мы же перед выводом на печать приводим объектную переменную к типу Int32 (т.е. к типу-значению). Для того, чтобы тип-значение мог быть использован в методе Concat, он должен быть приведен обратно в ссылочный вид.
Итак, в данном примере после последнего плюса мы имеем и упаковки, и распаковку. Конечно, это неэффективно, поэтому грамотнее было бы записать последний оператор в следующем виде:
Console.WriteLine (v + "," + o);
При этом результаты вывода не изменятся, а эффективность возрастет за счет избавления от лишних операций упаковки и распаковки.
Интересно, что если бы мы не пытались напечатать строку, составленную из нескольких параметров, то лишних операций удалось бы избежать и в примере на слайде, т.к. метод WriteLine может принимать и значения типа Int32.
Упаковка и распаковка в С++
Упаковка и распаковка в С++
#using <mscorlib.dll> using namespace System; __value struct V { int i; }; void Positive(Object*) { }; // expects a managed class void main() { V v={10}; // allocate and initialize Object* o = __box(v); // copy to the CLR heap Positive( o ); // treat as a managed class dynamic_cast<V*>(o)- >i = 20; // update the boxed version }
Некоторые языки, такие как C# или Visual Basic.NET, поддерживают операции упаковки и распаковки прозрачно для программиста. Это, конечно, хорошо, т.к. упрощает программирование, но как было показано на предыдущем слайде, при недостаточном понимании происходящих "за кадром" процессов это может привести к потере эффективности. Поэтому необходимо вдумчиво подходить к каждому отдельному случаю. Например, иногда выгоднее явно произвести упаковку один раз и затем использовать объектную переменную.
В других языках программирования, например, в Java, данная проблема решена еще проще: все типы данных заведомо представлены только в ссылочной форме, поэтому нет никаких проблем с упаковкой (но при этом имеется потенциальная потеря в скорости выполнения).
Наконец, большинство остальных языков программирования в .NET требуют явной записи для операций упаковки и распаковки. Например, в примере на managed C++, приведенном на слайде, можно увидеть обе операции: __box(v) приводит тип-значение к ссылочному типу, а dynamic_cast<V*>(o) позволяет изменить именно значение переменной i.
Литература к лекции
- Д. Пратт "Знакомство с .NET", Русская редакция, 2001
- J. Richter "Microsoft .NET Framework Delivers the Platform for an Integrated, Service-Oriented Web", MSDN Magazine, Oct/Nov 2000
- J. Richter "Type Fundamentals", MSDN Magazine, December 2000
- Т. Арчер "Основы C#", Русская редакция, 2001