CLR поддържа много езици за програмиране. За да се осигури съвместимост на данните между различните езици е разработена общата система от типове (Common Type System – CTS). CTS дефинира поддържаните от CLR типове данни и операциите над тях.
CTS и езиците за програмиране в .NET
Всички .NET езици използват типовете от CTS. За всеки тип в даден .NET език има някакво съответствие в CTS, макар че понякога това съответствие не е директно. Обратното не е вярно – съществуват CTS типове, които не се поддържат от някои .NET езици.
CTS e обектно-ориентирана
По идея всички езици в .NET Framework са обектно-ориентирани. Common Type System също се придържа към идеите на обектно-ориентираното програмиране (ООП) и по тази причина описва освен стандартните типове (числа, символи, низове, структури, масиви) и някои типове данни свързани с ООП (например класове и интерфейси).
CTS описва .NET типовете
Типовете данни в CTS биват най-разнообразни:
-
примитивни типове (primitive types – int, float, bool, char, …)
-
изброени типове (enums)
-
класове (classes)
-
структури (structs)
-
интерфейси (interfaces)
-
делегати (delegates)
-
масиви (arrays)
-
указатели (pointers)
Всички тези типове повече или по-малко вече са ни познати от езика C#, но всъщност те са част от CTS. Езикът C# и другите .NET езици използват CTS типовете и им съпоставят запазени думи съгласно своя синтаксис. Например типът System.Int32 от CTS съответства на типа int в C#, а типът System.String – на типа string.
Стойностни и референтни типове
В CTS се поддържат две основни категории типове: стойностни типове (value types) и референтни типове (reference types). Стойностните типове съдържат директно стойността си в стека за изпълнение на програмата, докато референтните типове съдържат строго типизиран указател (референция) към стойността, която се намира в динамичната памет. По-нататък ще разгледаме подробно разликите между стойностните и референтните типове и особеностите при тяхното използване.
Къде са ми указателите?
По принцип в .NET има класически указатели, но те не се използват масово, както при езиците C и C++. Указателите в .NET се поддържат най-вече заради съвместимост с Win32 платформата и се използват в много специални случаи. В силно типизираните езици като C# и VB.NET за достъп до обекти в динамичната памет се използват т. нар. референции (references), които са строго типизирани указатели, подобни на псевдонимите в C++.
С въвеждането на референтните типове в .NET отпада нуждата от класически указатели. На практика реферетните типове са типово-обезопасени указатели, защитени от неправилно преобразуване към друг тип, а сочената от тях динамична памет се управлява автоматично.
Йерархията на типовете
CTS дефинира строга йерархия на типовете данни, които се поддържат в .NET Framework:
В основата на йерархията стои системният тип System.Object. Той е общ предшественик (базов тип) за всички останали типове в CTS. Неговите преки наследници са стойностните и референтните типове (които ще дискутираме в детайли по-късно в тази тема).
Стойностните типове биват примитивни (int, float, bool и др.), структури (struct в C#) и изброени типове (enum в C#).
Референтните типове са всички останали – указателите, класовете, интерфейсите, делегатите, масивите и опакованите стойностни типове.
В предходните теми вече се запознахме с някои от CTS типовете. В тази и в следващите теми ще се запознаем и с останалите (опаковани стойностни типове, масиви, делегати).
Типът System.Object
В CTS всички типове наследяват системния тип System.Object. Не правят изключение дори примитивните типове (int, float, char, ...) и масивите. Всеки тип е наследник на System.Object и имплементира методите, включени в него. Като резултат значително се улеснява работата с типове, защото променлива от произволен тип може да се присвои на променлива от базовия тип System.Object (object в C#). Самият System.Object е референтен тип.
Стойностни типове (value types)
Стойностни типове (типове по стойност) са повечето примитивни типове (int, float, bool, char и др.), структурите (struct в C#) и изброените типове (enum в C#).
Стойностните типове директно съдържат стойността си и се съхраняват физически в работния стек за изпълнение на програмата. Tе не могат да приемат стойност null, защото реално не са указатели.
Стойностните типове и паметта
Стойностните типове заемат необходимата им памет в стека в момента на декларирането им и я освобождават в момента на излизане от обхват (при достигане на края на програмния блок, в който са декларирани). Заделянето и освобождаване на памет за стойностен тип реално се извършва чрез единично преместване на указателя на стека и следователно става много бързо.
Горното обяснение е малко опростено. Всъщност ако стойностен тип има за член-данни само стойностни типове, при инстанциране целият тип ще се задели в стека. Ако, обаче, стойностен тип (например структура) съдържа като член-данни референтни типове, стойностите им ще се запишат в динамичната памет.
Стойностните типове наследяват System.ValueType
CLR се грижи всички стойностни типове да наследяват системния тип System.ValueType. Всички типове, които не наследяват ValueType са референтни типове, т.е. реално са указатели към динамичната памет (адреси в паметта).
Предаване на стойностни типове
При извикване на метод стойностните типове се подават по стойност, т.е. предава се копие от тях. При подготовка на извикването на метод CLR копира подаваните като параметри стойностни типове от оригиналното им местоположение в стека на ново място в стека и подава на извиквания метод направените копия. Ако извикваният метод промени стойността на подадения му по стойност параметър, при връщане от извикването промяната се губи. Това поведение важи, разбира се, само ако параметрите се подават по подразбиране, без да се използват ключовите думи в C# ref и out, които ще разгледаме по-нататък в следващите теми.
Референтни типове (reference types)
Референтни типове (типове по референция) са указателите, класовете, интерфейсите, делегатите, масивите и опакованите стойностни типове. Физически референтните типове представляват указател към стойност в динамичната памет, но за CLR те не са обикновени указатели, а специални типово-обезопасени указатели. Това означава, че CLR не допуска на един референтен тип да се присвои стойност от друг референтен тип, който не е съвместим с него (т.е. не е същия тип или негов наследник). В резултат на това в .NET езиците грешките от неправилна работа с типове са силно намалени.
Референтните типове и паметта
Всички референтни типове се съхраняват в динамичната памет (т. нар. managed heap), която се контролира от системата за почистване на паметта (garbage collector). Динамичната памет е специално място от паметта, заделено от CLR за съхранение на данни, които се създават динамично по време на изпълнението на програмата. Такива данни са инстанциите на всички референтни типове.
Когато инстанция на референтен тип престане да бъде необходима на програмата, тя се унищожава от системата за почистване на паметта (т. нар. garbage collector).
Когато инстанцираме референтен тип с оператора new, CLR заделя място в динамичната памет, където ще стоят данните и един указател в стека, който съдържа адреса на заделеното място. Веднага след това заделената памет се занулява (освен ако програмистът не инициализира заделената променлива, например чрез извикване на подходящ конструктор).
Ако референтен тип (например клас) съдържа член-данни от стойностен тип, те се съхраняват в динамичната памет. Ако референтен тип съдържа член-данни от референтен тип, в динамичната памет се заделят указатели (референции) за тях, а техните стойности (ако не са null) също се заделят също в динамичната памет, но като отделни обекти.
Референтните типове и производителността
Понякога се приема, че заделянето на динамична памет е бърза операция, защото в текущата реализация (.NET Framework 1.1) физически се имплементира чрез преместване на един указател. Освобождаването на памет, обаче, е сложна и времеотнемаща операция, която се извършва от време на време от системата за почистване на паметта (garbage collector).
Ако изчислим средното време, необходимо за заделяне и освобождаване на динамична памет, се оказва, че заделянето и освобождаване на стойностните типове е значително по-бързо от референтните типове. Когато производителността е важна за нашата система, трябва да се съобразяваме с особеностите на стойностните и референтните типове и начина, по който те заделят и освобождават памет.
Глобално погледнато, нещата около управлението на динамичната памет в .NET Framework са доста комплексни, но в тази тема няма да се спираме на тях. По-нататък, в темата за управление на паметта и ресурсите, ще им обърнем специално внимание.
Стойностни срещу референтни типове
Стойностните и референтните типове в .NET Framework се различават съществено. Стойностните типове се разполагат в стека за изпълнение на програмата, докато референтните типове са строго типизирани указатели към динамичната памет, където се съдържа самата им стойност.
Следват някои по-съществени разлики между тях:
-
Стойностните типове наследяват системния тип System.ValueType, а референтните наследяват директно System.Object.
-
При създаване на променлива от стойностен тип тя се заделя в стека, а при референтните типове – в динамичната памет.
-
При присвояване на стойностни типове се копира самата им стойност, а при референтни типове – само референцията (указателя).
-
При предаване на променлива от стойностен тип като параметър на метод, се предава копие на стойността й, а при референтните типове се предава копие на референцията, т.е. самата стойност не се копира. В резултат, ако даден метод променя стойностен входен параметър, промените се губят при излизане от метода, а ако входният параметър е референтен, те се запазват.
-
Стойностните типове не могат да приемат стойност null, защото не са указатели, докато референтните могат.
-
Стойностните типове се унищожават при излизане от обхват, докато референтните се унищожават от системата за почистване на паметта (garbage collector) в някой момент, в който се установи, че вече не са необходими за работата на програмата.
-
Променливи от стойностен тип могат да се съхраняват в променливи от референтен тип чрез т.нар. "опаковане" (boxing), което ще разгледаме след малко.
Стойностни и референтни типове – пример
В настоящия пример се демонстрира използването на стойностни и референтни типове и се илюстрира разликата между тях:
using System;
// SomeClass is reference type
class SomeClass
{
public int mValue;
}
// SomeStruct is value type
struct SomeStruct
{
public int mValue;
}
class TestValueAndReferenceTypes
{
static void Main()
{
SomeClass class1 = new SomeClass();
class1.mValue = 100;
SomeClass class2 = class1; // Копира се референцията
class2.mValue = 200; // Променя се и class1.mValue
Console.WriteLine(class1.mValue); // Отпечатва се 200
SomeStruct struct1 = new SomeStruct();
struct1.mValue = 100;
SomeStruct struct2 = struct1; // Копира се стойността
struct2.mValue = 200; // Променя се копираната стойност
Console.WriteLine(struct1.mValue); // Отпечатва се 100
}
}
|
След като се изпълни примерът, се получава следния резултат:
Как работи примерът?
В началото на примера се създава инстанция на класа SomeClass, в нея се записва числото 100 и след това тя се присвоява на две променливи. Аналогично се създава инстанция на структурата SomeStruct, в нея също се записва 100 и след това тя се присвоява на две променливи.
При присвояването на инстанциите на класа, понеже той е референтен тип, се присвоява само референцията и стойността реално не се копира, а остава обща. При присвояването на инстанцията на структурата, понеже тя е стойностен тип, се присвоява самата стойност (нейно копие). Поради тази причина в резултат от изпълнението на програмата на конзолата се отпечатват различни стойности.
По-долу са показани схематично стекът за изпълнение на програмата и динамичната памет в момента преди приключване на програмата. Данните са взети от дебъгера на Visual Studio .NET поради което са много близки до истинското разположение на паметта по време на изпълнение на примерната програма:
Стекът расте отгоре надолу (от големите адреси към адрес 0), защото програмата е изпълнена върху Intel-съвместима архитектура, при която това поведение е нормално.
Проследяване на примера с VS.NET
За да проследим как се изпълнява горният пример стъпка по стъпка, можем да използваме проекта Demo-1-Value-And-Reference-Types от демонстрациите:
-
Отваряме с VS.NET проекта Demo-1-Value-And-Reference-Types.sln.
-
Слагаме точка на прекъсване на последния ред от Main() метода на основния клас.
-
Стартираме приложението с [F5].
-
След като дебъгерът спре в точката на прекъсване, показваме Disassembly и Registers прозорците. От менюто на VS.NET избираме Debug | Windows | Disassembly и Debug | Windows | Registers. Ето как изглежда VS.NET в този момент:
-
Можем да разгледаме асемблерния код, получен след компилиране на програмата и след превръщането на MSIL кода в чист Win32 код за процесор Intel x86.
Повечето компилатори за Intel-базирани процесори генерират код, който използва в тялото на методите регистър EBP като указател към върха на стека. Адресиране от типа на dword ptr [ebp-14h] най-често реферира стойност в стека – локална променлива или параметър.
Спомнете си за разликите между класове и структури (референтни и стойностни типове). Стойностните типове съхраняват стойността си директно в стека. Референтните типове съхраняват в стека само 4-байтов адрес, който указва мястото на променливата в динамичната памет.
Често пъти, с цел оптимизация на производителността, компилаторът вместо някаква област от стека използва регистри за съхранение на локални променливи. В случая в EBX се съхранява референцията class2, а в EDI – референцията class1.
-
Да разгледаме асемблерния код, генериран за операцията присвояване class2=class1. В него се присвоява на регистър EBX стойността на регистър EDI, т.е. на референцията class2 се присвоява референцията class1. Обърнете внимание, че се копира референцията, а не самата стойност.
-
Да разгледаме асемблерния код, генериран за операцията присвояване struct2=struct1. В него се присвоява на регистър EAX стойността от стека, съответстваща на struct1 и след това стойността от EAX се записва обратно в стека, в променливата struct2. На практика се копира самата стойност на структурата, като се използва за работна променлива регистърът EAX.
Сподели с приятели: |