Поиск

Размерные и ссылочные типы

Концепция создания языка, где любая сущность является объектом, не нова. Такие попытки предпринимались, например, в SmallTalk. Самым большим недостатком представления всего в виде объектов всегда было снижение производительности. Так, если в SmallTalk попытаться сложить два значения типа double, при этом реально выделяется объект в куче. Нужно ли говорить, что выделение объекта лишь для суммирования двух чисел чрезвычайно малоэффективно.

Перед разработчиками CTS стояла задача создания системы типов, где любая сущность была бы объектом, но система типов при этом работала эффективно. Они решили эту задачу, разделив типы CTS на две категории: размерные (value types) и ссылочные (reference types). Как вы вскоре увидите, эти термины отражают способы выделения памяти и внутреннего функционирования переменных.

Размерные типы

Если некоторая переменная имеет размерный тип, она содержит реальные данные. Так что первое правило для размерных типов таково: они не могут быть null. Ниже, например, я на С# выделил память, создав переменную типа System.Int32, который определен в CTS. При этом объявлении происходит не что иное, как выделение в стеке 32-разрядной области.

int i = 32;

Кроме того, при присвоении / значения в выделенное пространство помещается 32-разрядное число.

В С# определено несколько размерных типов, включая перечислители (enumerators), структуры (structures) и примитивы (primitives). Объявляя переменную одного из этих типов, вы каждый раз выделяете в стеке некоторое число байтов, ассоциированных с этим типом, и работаете напрямую с выделенным массивом битов. Кроме того, когда вы передаете переменную размерного типа, передается значение переменной, а не ссылка на лежащий в ее основе объект.

Ссылочные типы

Ссылочные типы похожи на ссылки в C++, где они являются указателями, привязанными к типам (type-safe pointers). Это значит, что ссылка (если она не равна null) — это не просто адрес, который, как вы полагаете, может указывать (а может и не указывать) на определенный объект. Ссылка всегда гарантированно указывает объект заданного типа, уже выделенный в куче. Кроме того, ссылка может быть равна null.

Ниже выделяется значение ссылочного типа (string), но при этом «за кулисами» в куче выделяется значение и возвращается ссылка на него:

string s = "Hello,World";

Как и в случае размерных типов, в С# несколько типов определены как ссылочные: классы, массивы, делегаты (delegates) и интерфейсы. Объявляя переменную одного из этих типов, вы каждый раз выделяете в куче некоторое ассоциированное с этим типом число байт. Но вместо того, чтобы работать с ними напрямую (как в случае размерных типов), вы работаете со ссылкой на выделенный объект.

Упаковка и распаковка

Как же эти различные категории типов обеспечивают более эффективную работу системы? Это делается с помощью упаковки (boxing). В простейшем случае при упаковке размерный тип преобразуется в ссылочный. В обратном случае ссылочный тип распаковывается (unbox) в размерный.

Замечательно в данной методике то, что объект лишь тогда является объектом, когда это необходимо. Допустим, вы объявляете переменную типа System.Int32. Для нее выделяется память в стеке. Вы можете передавать эту переменную любому методу, определенному в качестве принимающего аргументы типа System.Object, а также обращаться к любому из ее членов, к которому у вас есть доступ. Поэтому вы воспринимаете и ощущаете ее как объект. Но в реальности это всего 4 байта в стеке.

Только когда вы пытаетесь использовать эту переменную согласно правилам, определенным интерфейсом базового класса System.Object, система автоматически упаковывает переменную, в результате чего она становится ссылочным типом и может быть использована так же, как любой объект. Упаковка — это механизм, посредством которого в С# любая сущность может быть представлена в виде объекта. Это позволяет избежать издержек, неизбежных в том случае, если б всякая сущность на самом деле была объектом. Обратимся к примерам:

int foo = 42; // Размерный тип.
object bar = foo; // Переменная foo упакована в bar.

В первой строке этого кода мы создавали переменную (foo) типа int. Как вам известно, int является размерным типом (поскольку это базисный тип). Во второй строке компилятор обнаружит, что переменная foo скопирована в ссылочный тип, представленный переменной bar. При этом компилятор добавит код MSIL, необходимый для упаковки этой переменной.

А теперь выполним явное приведение типов, чтобы преобразовать bar обратно в размерный тип:

int foo = 42; // Размерный тип.
object bar = foo; // Переменная foo упакована в bar.
int foo2 = (int)bar; // Распаковка и приведение к типу int.

При упаковке (т. е. преобразовании из размерного типа в ссылочный) явного приведения типов не требуется. Однако при распаковке — преобразовании из ссылочного типа в размерный — приведение типов необходимо. Это так, потому что в случае распаковки объект может быть приведен к любому типу. Преобразование позволяет компилятору проверить, возможно ли приведение для заданного типа переменной. Поскольку приведение типов подчинено строгим правилам, определяемым CTS.

Корень всех типов: System.Object

Как я уже говорил, в конечном счете все типы происходят от типа System.Object, что позволяет гарантировать наличие у каждого типа минимального набора функциональных возможностей. Все типы получают «бесплатно» четыре открытых метода (табл. 4-1).