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

Теперь давайте рассмотрим упаковку и распаковку. Все типы внутри CLR относятся к двум категориям: ссылочные типы (объекты) и типы значений (значения). Объекты определяются с помощью классов, а значения — с помощью структур. Между этими двумя группами существует четкое разделение.

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

1
2
3
4
5
6
7
8
9
10
public class EntryPoint
{
static void Print ( object obj )
{
System.Console.WriteLine ( "{0}", obj.ToString () );
}
static void Main() {
int x = 42; Print ( x ) ;
}
}

Все выглядит достаточно просто. В Main имеется int, который в С# является псевдонимом для System. Int32 и представляет собой тип значения. Переменную х можно было бы также объявить как относящуюся к типу System. Int32. Место, выделенное для х, находится в локальном стеке. Затем переменная х передаете в виде параметра методу Print. Метод Print принимает ссылку на object и просто посылает результат вызова ToString на этом объекте на консоль. Давайте проанализируем детали. Print принимает ссылку на расположенный в куче объект. Но вы передаете методу тип значения.

Что же здесь происходит? Как такое возможно?

Ключ лежит в концепции, называемой упаковкой (boxing). В точке, где определяется тип значения, CLR создает во время выполнения класс-оболочку для помещения в него копии типа значения. Экземпляры этой оболочки находятся в куче и обычно называются упаковочными объектами. Это способ, которым CLR преодолевает зазор между типами значений и ссылочными типами.

Фактически, если с помощью ILDASM заглянуть в код IL, сгенерированный для метода Main, можно увидеть там следующее:

1
2
3
4
5
6
7
8
9
10
11
.method private hidebysig static void Main() cil managed
.entrypoint
// Code size 15 (Oxf) .maxstack 1
.locals init (int32 V_0)
IL_0000: ldc.i4.s 42
IL_0002: stloc.O
IL_0003: ldloc.O
IL_0004: box [mscorlib]System.Int32
IL_0009: call void EntryPoint::Print(object)
IL_000e: ret }
// end of method EntryPoint::Main

Обратите внимание на IL-инструкцию box, которая выполняет операцию упаковки перед вызовом метода Print.

Упаковочный объект ведет себя подобно любому другому ссылочному типу в CLR. К тому же упаковочный тип реализует интерфейсы содержащегося типа значений. Упаковочный тип — это тип класса, внутренне сгенерированный виртуальной исполняющей системой CLR в точке определения содержащегося типа значений. Затем CLR использует этот внутренний тип класса при выполнении необходимых операций упаковки.

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

Например, рассмотрим следующую небольшую модификацию предыдущего кода:

1
2
3
4
5
6
7
8
9
10
11
12
public class EntryPoint
static void PrintAndModifу ( object obj )
{
System.Console.WriteLine ( "{0}", obj.ToString() );
int x = (int) obj ;
x = 21;
}
static void Main()
{
int x = 42;
PrintAndModifу ( x );
PrintAndModifу ( x );

Вывод этого кода может удивить:

42
42

Фактически первоначальное значение переменной х, объявленной и инициализированной в Main, никогда не изменяется. Когда оно передается методу PrintAndModif у, то упаковывается, поскольку метод PrintAndModif у принимает тип object в качестве параметра. Несмотря на то что метод PrintAndModif у принимает ссылку на объект, который можно модифицировать, принятый им объект является упаковочным и содержит копию исходного значения.

В методе PrintAndModifу также вводится другая операция, называемая распаковкой (unboxing). Поскольку значение упаковывается внутри экземпляра объекта в куче, изменить его нельзя, так как методы, поддерживаемые объектом — это только методы, реализуемые System.Object. Формально он также поддерживает те же самые интерфейсы, что и System. Int32.

Поэтому необходим какой-то способ получения значения из упаковки. В С# это можно сделать синтаксически — с помощью приведения. Обратите внимание, что выполняется приведение экземпляра объекта обратно к int, а компилятор достаточно интеллектуален, чтобы понять, что на самом деле необходимо распаковать тип значения, и применяет IL-инструкцию unbox, что видно в следующем коде IL метода PrintAndModifу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.method private hidebysig static void PrintAndModifу(object obj) cil managed {
// Code size 28 (Oxlc) .maxstack 2
.locals init (int32 V_0)
IL_0000: ldstr "{0}"
IL_0005: ldarg.O
IL_0006: callvirt instance string [mscorlib]System.Object::ToString ()
IL_000b: call void [mscorlib]System.Console::WriteLine(string, object)
IL_0010: ldarg.O
IL_0011: unbox [mscorlib]System.Int32
IL_0016: ldind.i4
IL_0017: stloc.O
IL_0018: ldc.i4.s 21
IL_001a: stloc.O
IL_001b: ret }
// end of method EntryPoint::PrintAndModifу

Теперь необходимо внести ясность относительно того, что происходит во время распаковки в С#. Операция распаковки значения в точности противоположна упаковке. Значение из упаковки копируется в экземпляр значения в локальном стеке.

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

Что еще хуже, так это то, что две копии int создаются между моментом инициации вызова PrintAndModif у и моментом манипулирования этим int в методе. Первая копия помещается в упаковку. Вторая копия создается, когда упакованное значение копируется из упаковки.

Формально значение, содержащееся в упаковке, можно модифицировать.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface IModifyMyValue {
int X {
get;
set;
}
public struct MyValue : IModifyMyValue {
public int x;
public int X {
get {
return x;
}
set {
x = value;
}
public override string ToString() {
System.Text.StringBuilder output = new System.Text.StringBuilder();
output.AppendFormat( "{0}", x ) ;
return output.ToString();
}
public class EntryPoint {
static void Main() {
// создать значение
MyValue myval = new MyValue ();
myval.x = 123;
// упаковать его object obj = myval;
System.Console.WriteLine ( "{0}", obj.ToString() );
// модифицировать содержимое упаковки IModifyMyValue iface = (IModifyMyValue) obj;
iface.X = 456;
System.Console.WriteLine ( "{0}", obj.ToString() );
// распаковать и посмотреть, что получилось MyValue newval = (MyValue) obj;
System.Console.WriteLine ( "{0}", newval.ToString () );
}

Запуск этого кода на выполнение дает следующий вывод:
123
456
456

Как и ожидалось, значение внутри упаковки можно модифицировать, используя интерфейс по имени IModif yMyValue. Однако это не такой уж прямолинейный процесс. Вдобавок имейте в виду, что перед тем, как получить ссылку на интерфейс для типа значения, оно должно быть упаковано. Это имеет смысл, если вспомнить, что ссылки на интерфейсы — это также ссылочные типы.

Очень сложно представить вескую причину, по которой понадобилось бы определять специальный интерфейс только для того, чтобы иметь возможность модифицировать значение, содержащееся внутри упакованного объекта.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100