Вече обяснихме, че стойностните типове се съхраняват в стека на приложението и не могат да приемат стойност null, докато референтните типове съдържат указател (референция) към стойност в динамичната памет и могат да бъдат null.
Понякога се налага на референтен тип да се присвои обект от стойностен тип. Например може да се наложи в System.Object инстанция да се запише System.Int32 стойност. CLR позволява това благодарение на т. нар. "опаковане" на стойностните типове (boxing).
В .NET Framework стойностните типове могат да се използват без преобразуване навсякъде, където се изискват референтни типове. При нужда CLR опакова и разопакова стойностните типове автоматично. Това спестява дефинирането на обвиващи (wrapper) класове за примитивните типове, структурите и изброените типове, но разбира се, може да доведе и до някои проблеми, които ще дискутираме по-късно.
Опаковане (boxing) на стойностни типове
Опаковането (boxing) е действие, което преобразува стойностен тип в референция към опакована стойност. То се извършва, когато е необходимо да се преобразува стойностен тип към референтен тип, например при преобразуване на Int32 към Object:
Всяка инстанция на стойностен тип може да бъде опакована чрез просто преобразуване до System.Object. Ако един тип е вече опакован, той не може да бъде опакован втори път и при преобразуване към System.Object си остава опакован само веднъж.
CLR извършва опаковането по следния начин:
-
Заделя динамична памет за създаване на копие на обекта от стойностния тип.
-
Копира съдържанието на стойностната променливата от стека в заделената динамична памет.
-
Връща референция към създадения обект в динамичната памет.
При опаковането в динамичната памет се записва информация, че референцията съдържа опакован обект и се запазва името на оригиналния стойностен тип.
Разопаковане (unboxing) на опаковани типове
Разопаковането (unboxing) е процесът на извличане на опакована стойност от динамичната памет. Разопаковане се извършва при преобразуване на опакована стойност обратно към инстанция на стойностен тип, например при преобразуване на Object към Int32:
object obj = 5; // 5 се опакова
int value = (int) obj; // стойността на obj се разопакова
|
CLR извършва разопаковането по следния начин:
-
Ако референцията е null се предизвиква NullReferenceException.
-
Ако референцията не сочи към валидна опакована стойност от съответния тип, се предизвиква изключение InvalidCastException.
-
Ако референцията е валидна опакована стойност от правилния тип, стойността се извлича от динамичната памет и се записва в стека.
За разлика от опаковането, разопаковането невинаги е успешна операция (и това трябва да се съобразява, когато се работи с опаковани стойности).
При използване на автоматично опаковане и разопаковане на стойности трябва да се имат предвид някои особености:
-
Опаковането и разопаковането намаляват производителността. За оптимална производителност трябва да се намали броят на опакованите и разопакованите обекти.
-
Опакованите типове са копия на оригиналните стойности, поради което, ако променяме оригиналния неопакован тип, опакованото копие не се променя.
Как работят опаковането и разопаковането?
Нека имаме следния код:
int i = 5;
object obj = i; // boxing
int i2;
i2 = (int) obj; // unboxing
|
На картинката по-долу схематично е показано как работят опаковането и разопаковането на стойностни типове в .NET Framework:
При опаковане стойността от стека се копира в динамичната памет, а при разопаковане стойността от динамичната памет се копира в обратно в стека.
Опакованите стойности се държат като останалите референтни типове – разполагат се в динамичната памет, унищожават се от garbage collector, когато не са необходими на програмата, и при подаване като параметър при извикване на метод се пренасят по адрес.
Пример за опаковане и разопаковане
В следващия пример се илюстрира опаковането и разопаковането на стойностни типове, като се обръща внимание на някои особености при тези операции:
using System;
class TestBoxingUnboxing
{
static void Main()
{
int value1 = 1;
object obj = value1; // извършва се опаковане
value1 = 12345; // променя се само стойността в стека
int value2 = (int)obj; // извършва се разопаковане
Console.WriteLine(value2); // отпечатва се 1
long value3 = (long) (int) obj; // разопаковане
long value4 = (long) obj; // InvalidCastException
}
}
|
От примера се вижда, че разопаковане на Int32 стойност не може да се извърши чрез директно преобразуване към Int64. Необходимо е първо да се извлече Int32 стойността от опакования обект и след това да се извърши преобразуване до Int64.
Аномалии при опаковане и разопаковане
При работа с опаковани обекти трябва да се внимава, защото ако не бъдат съобразени някои особености, може да се наблюдава странно поведение на програмата. Ето един такъв пример:
using System;
interface IMovable
{
void Move(int aX, int aY);
}
///
/// Много лоша практика! Структурите не бива
/// да съдържат логика, а само данни!
///
struct Point : IMovable
{
public int mX, mY;
public void Move(int aX, int aY)
{
mX += aX;
mY += aY;
}
public override string ToString()
{
return String.Format("({0},{1})", mX, mY);
}
}
class TestPoint
{
static void Main()
{
Point p1 = new Point();
Console.WriteLine("p1={0}", p1); // p1=(0,0)
IMovable p1mov = (IMovable) p1; // p1 се опакова
IMovable p2mov = // p1mov не се опакова втори
(IMovable) p1mov; // път, защото е вече опакован
Point p2 = (Point) p2mov; // p2mov се разопакова
p1.Move(-100,-100);
p2mov.Move(5,5);
p2.Move(100,100);
Console.WriteLine("p1={0}", p1); // p1=(-100,-100)
Console.WriteLine("p1mov={0}", p1mov); // p1mov=(5,5)
Console.WriteLine("p2mov={0}", p2mov); // p2mov=(5,5)
Console.WriteLine("p2={0}", p2); // p2=(100,100)
}
}
|
Резултатът от изпълнение на примера е следният:
Основната причина за този резултат е фактът, че при преобразуване към интерфейс структурите се опаковат и съответно се създава копие на данните, намиращи се в тях. Опаковането е съвсем в реда на нещата, като се има предвид, че структурите са стойностни типове, а интерфейсите са референтни типове.
|
Препоръчва се, когато се използват структури в C#, те да съдържат само данни. Лоша практика е в структура да се дефинират методи с логика, както и структура да имплементира интерфейс.
| Как работи примерът?
Да разгледаме как работи примерът. Ако съобразим разположението на стойностните и референтните променливи в паметта, можем да си обясним какво се случва:
Променливите p1 и p2 са от стойностен тип и се разполагат директно в стека (и заемат по 8 байта от него).
Променливите p1mov и p2mov са от референтен тип и се разполагат в динамичната памет. В стека за тях се пазят по 4 байта, които съдържат адреса на стойността им.
С помощта на дебъгера на VS.NET можем да проследим точното разположение и стойностите на тези променливи. В горната таблица е показано състоянието им точно преди завършване на програмата.
Напомняме, че при Intel архитектурата стекът расте надолу и свършва на адрес 0x00000000.
Сподели с приятели: |