Кратко съдържание



страница40/73
Дата21.07.2018
Размер9.03 Mb.
#76887
1   ...   36   37   38   39   40   41   42   43   ...   73

Какво е CTS?


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 от де­монстра­циите:

  1. Отваряме с VS.NET проекта Demo-1-Value-And-Reference-Types.sln.

  2. Слагаме точка на прекъсване на последния ред от Main() метода на основния клас.

  3. Стартираме приложението с [F5].

  4. След като дебъгерът спре в точката на прекъсване, показваме Disassembly и Registers прозорците. От менюто на VS.NET избираме Debug | Windows | Disassembly и Debug | Windows | Registers. Ето как изглежда VS.NET в този момент:



  1. Можем да разгледаме асемблерния код, получен след компилиране на програмата и след превръщането на MSIL кода в чист Win32 код за процесор Intel x86.

Повечето компилатори за Intel-базирани процесори генерират код, който използва в тялото на методите регистър EBP като указател към върха на стека. Адресиране от типа на dword ptr [ebp-14h] най-често реферира стойност в стека – локална променлива или параметър.

Спомнете си за разликите между класове и структури (референтни и стойностни типове). Стойностните типове съхраняват стойността си директно в стека. Референтните типове съхраняват в стека само 4-байтов адрес, който указва мястото на променливата в динамичната памет.

Често пъти, с цел оптимизация на производителността, компилато­рът вместо някаква област от стека използва регистри за съхране­ние на локални променливи. В случая в EBX се съхранява референ­цията class2, а в EDI – референцията class1.


  1. Да разгледаме асемблерния код, генериран за операцията присвоя­ване class2=class1. В него се присвоява на регистър EBX стойност­та на регистър EDI, т.е. на референцията class2 се присвоява референ­цията class1. Обърнете внимание, че се копира референ­цията, а не самата стойност.

  2. Да разгледаме асемблерния код, генериран за операцията присвоя­ване struct2=struct1. В него се присвоява на регистър EAX стой­ността от стека, съответстваща на struct1 и след това стой­ността от EAX се записва обратно в стека, в променливата struct2. На практика се копира самата стойност на структурата, като се изпол­зва за работна променлива регистърът EAX.




Сподели с приятели:
1   ...   36   37   38   39   40   41   42   43   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница