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



страница25/73
Дата21.07.2018
Размер9.03 Mb.
#76887
1   ...   21   22   23   24   25   26   27   28   ...   73

Класове


Класовете в C# са основните единици, от които се състоят програмите. Те моделират обектите от реалния свят и могат да дефинират различни чле­нове (член-променливи, методи, свойства и др.). Нека видим как изглеж­да един примерен клас на езика C#:

class Student

{

// Private member declarations



private string mFirstName;

private string mLastName;

private string mStudentId;
// Constant

private const double PI = 3.1415926535897932384626433;


// Constructor

public Student(string aStudentId)

{

mStudentId = aStudentId;



}
// Property

public string FirstName

{

get


{

return mFirstName;

}

set


{

mFirstName = value;

}

}
// Read-only property



public string StudentId

{

get



{

return mStudentId;

}

}
// Method



public string StoreExamResult(

string aSubject, double aGrade)

{

// ...


}

}


В горния пример е дефиниран класът Student, илюстриращ някои от видовете членове, които класовете могат да реализират – капсулираните полета mFirstName, mLastName и mStudentId, константата PI, кон­струк­то­рът Student(), свойствата FirstName и StudentId и методът StoreExamResult(). С течение на темата ще се запознаем по-отблизо с всеки от тези видове членове.

Членове на тип


В .NET типовете "клас" и "структура", като реализация на понятието клас от ООП, могат да съдържат в себе си членове (members), подобно на други обектно-ориентирани езици като Java и C++. Членовете могат да бъдат от един от следните видове:

  • полета, или член-променливи (fields)

  • константи (constants)

  • методи, или член-функции (methods)

  • свойства (properties)

  • индексатори (indexers)

  • събития (events)

  • оператори (operators)

  • конструктори (constructors)

  • деструктори (destructors)

  • вложени типове (класове, структури, изброени типове и др.)

Видимост на членовете


Множеството от типове, които могат да "виждат" определен член на даден клас се определя от видимостта. Правилното задаване на видимостта на членовете е ключов момент в разработването на йерархии от класове, тъй като основен принцип в ООП е клиентът на класа да вижда само това, което му е необходимо, и нищо повече. Следва описание на нивата на видимост в .NET Framework.

public


Глобална видимост – членовете с такова ниво на достъп могат да се достъпват от всеки тип.

protected internal


Това са членовете, видими от всички типове, дефинирани в асемблито, в което е дефиниран дадения, a също и от наследниците на типа.

internal


Членове, които се достъпват от всички типове, дефинирани в асемблито, в което е дефиниран дадения.

protected


Членове, видими само от наследниците на дадения тип.

private


Капсулирани членове, видими единствено в рамките на типа.

Член-променливи


Данните, с които инстанцията на класа работи, се съхраняват в член-про­мен­ливи (или още полета). Те се дефинират в тялото на класа и могат да се достъпват от други ви­дове членове – методи, конструктори, ин­дек­са­тори, свойства. В след­ва­щи­я пример ще покажем няколко декла­рации на член-про­мен­ли­ви, за които в последствие ще дадем обяснения.

class Student

{

private string mFirstName;



private string mLastName;

private string mStudentId;

private int mCourse = 1;

private string mSpeciality;

private Course[] mCoursesTaken;
// Avoid missing the visibility modifier

string mRemarks = "(няма забележки)";

}

Дефиниране на ниво на видимост


Дефиницията на всяко поле започва с ниво на видимост. Допустими са вси­чки по-горе изброени нива на видимост, но в примера са из­пол­зва­ни само private, защото скриването на полетата от из­пол­зва­щи­те класа, т.е. указването на видимост private или protected, е утвърдена практика в ООП. Когато искаме да предоставим данните на класа на окол­ния свят в .NET е прието вместо полета с ниво на достъп "public" да се из­ползват свойства, на които ще се спрем малко по-късно. Степента на видимост може и да не бъде определена явно, както е в последния ред за по­ле­то mRemarks от примера и в този случай се подразбира private. Тази прак­тика не се препоръчва, защото води до по-неясен код.

Дефиниране на тип


Следващият елемент от дефиницията на член-променлива е типът, който се указва задължително. Може да бъде произволен .NET тип от CTS или де­финиран от потребителя.

Задаване на име


След типа следва името на дефинираното поле, чрез което се обръщаме към него. То представлява идентификатор, т. е. последователност от unicode символи – главни и малки букви, цифри, -(тире) и _(подчерта­ващо тире), незапочваща с цифра или тире.

Имената на полетата и въобще на членовете в .NET Framework могат да бъдат идентични със съществуващи имена на типове или пространства от имена (на тях ще се спрем в края на темата). Например класът Student може да има свойство със същото име Student. Могат да бъдат и запазени думи, но само ако бъдат предшествани от @. Допуска се и използването на нелатински букви в имената, но не се препоръчва.


Задаване на стойност


При дефиницията на поле можем да му зададем стойност, както в примера това е направено за mCourse и mRemarks. Ако началната стойност бъде про­пусната, на член-променливата се задава стойност по под­раз­би­ране. За ре­фе­рен­тните типове това е null, а за стойностните типовете е 0 или неин еквивалент (например false за boolean). В .NET Framework всички членове и променливи се инициализират автоматично. Това намалява грешките, възникващи заради използването на неинициализирани про­мен­ливи.

Константни полета


Константните полета (или само константи) много приличат на обик­но­ве­ни­те полета, но имат някои особености. Нека обърнем внимание на след­ния при­мер, който показва няколко дефиниции на константи:

public class MathConstants

{

public const string PI_SYMBOL = "π";



public const double PI = 3.1415926535897932385;

public const double SQRT2 = 1.4142135623731;

}


От примера виждаме, че дефиницията на константа е дефиницията на поле с добавена ключовата дума const. Има и някои други разлики.

При декларирането на константно поле е задължително да се предостави стойност. Освен това стойността на константата не може да бъде про­ме­ня­на по време на работата с типа, в който е дефинирана – може само да бъ­де прочетена. Константите реално не съществуват като полета в типа, а съ­ществуват само в сорс кода и се заместват със стойността им по време на компилация. Поради тази причина const декларациите в C# се наричат още compile-time константи, т. е. константи, които съществуват само по време на компилацията.


Полета само за четене


Друг специален вид полета, подобни на константите, са полетата само за четене (read-only fields). Те се различават от константните по това, че стойността им освен при дефиницията може да бъде зададена и в кон­струк­тор, но от там нататък не може да бъде променяна. Член-променлива са­мо за четене се декларира, като се използва запазената дума readonly, като в примера:

class ReadOnlyDemo

{

private readonly int mSize;


public ReadOnlyDemo(int aSize)

{

mSize = aSize; // cannot be further modified!



}

}


За разлика от константите, полетата само за четене са реални полета в типа, които обаче, задължително трябва да се инициализират в конструк­тора на класа или при деклариране, защото след това не може да им бъде присвоя­вана стой­ност и биха останали с подразбиращата се. Поради тази причина те се наричат още run-time константи, т. е. константи, които се инициализират по време на изпълнение на програмата.

Методи


Методите (или още член-функции) дефинират операции за типа, в който са дефинирани. Те могат да боравят с членовете му, независимо от сте­пен­та им на видимост, да ги достъпват и променят (освен полетата обя­ве­ни като константни или само за четене).

В C# функции могат да бъдат дефинирани единствено като членове на клас или структура, за разлика от други обектно-ориентирани езици, къ­де­то се използват глобални функции – такива, които не са обвързани с конкре­тен тип и са общодостъпни. В C# функции, които се достъпват без да е нужна инстанция на даден клас, се дефинират като статични. На тях ще се спрем след малко.


Задаване на видимост


Подобно на полетата, и методите могат да имат ниво на видимост. И син­тактично, и от гледна точка на стила на програмиране, на методите е допустимо да се зададе коя да е от възможните нива на видимост, тъй като те представляват действията с типа и за някои от тях е необходимо да бъдат видими за околния свят, а за други – не. Отново подразбиращото се ниво на видимост е private, но е препоръчително да се декларира изрично.

Параметри и върната стойност


Методите могат да приемат параметри и да връщат стойност. Параметрите имат тип, който може да бъде всеки валиден .NET тип. Върнатата стойност може да бъде също от всеки възможен тип, а може и да отсъства. Нека обърнем внимание на следния пример:

class MethodsDemo

{

public void SayHiGeorgi()



{

SayHi("Гошо");

}
public void SayHiPeter()

{

SayHi("Пешо");



}
private void SayHi(string aName)

{

if (aName == null || aName == "" )



{

return;


}

Console.WriteLine("Здравей, {1}", aName);

}
public int Multiply(int x, int y)

{

return x * y;



}

}


Първите два метода, SayHiGeorgi() и SayHiPeter(), не приемат никакви па­раметри и не връщат стойност. Третият, SayHi(string aName), приема един параметър от тип string и не връща стойност. Последният, Multiply(int x, int y), приема два параметъра от тип int и връща стой­ност също от тип int.

В дефинициите на първите три метода от примера забелязваме ключовата дума void – тя се използва при методи, които не връщат стойност. За методи, които връщат стойност, вместо ключовата дума void се указва типа на връщаната стойност.

В последния метод забелязваме как се употребява ключовата дума return за връщане на стойност. Същата ключова дума използваме и за прекратя­ване на изпълнението на метод, който не връща стойност, както в метода SayHi(aName).

Методи с еднакви имена


В C# е допустимо един тип да има два и повече метода с едно и също име, но с някои ограничения. Ще въведем понятие, свързано с из­пол­зва­не­то на едно и също име за няколко метода. Комбинацията от името, броя и типа на параметрите на метод наричаме сигнатура. Ако два метода имат едно и също име, те задължително трябва да се различават по сиг­на­ту­ра. След­ва­щият пример илюстрира дефинирането на три метода с еднакви име­на:

int Sum(int a, int b)

{

return a + b;



}
int Sum(int a, int b, int c)

{

return a + b + c;



}
long Sum(long a, long b, long c) // avoid this

{

return a + b + c;



}

Горните дефиниции са напълно валидни – първите два метода се раз­ли­ча­ват по броя на параметрите си, а вторият и третият – по типа.



Трябва да сме особено внимателни с дефиниции като последните две и е препоръчително да се избягват, тъй като не е очевидно кой метод ще бъде извикан при обръщение като int sumTest = sum(1,2,3). Компилаторът по никакъв начин не ни предупреждава за двусмислието. В горния пример ще бъде извикан първият метод – sum(int a, int b, int c).

Статични членове


Както вече споменахме, в C# функции, които могат да се извикват без да е нужна инстанция на клас, се реализират като статични (или общи) методи. Това става, като в дефиницията им включим ключовата дума static. Статичните членове се споделят от всички инстанции и се изпол­зват за пресъздаване на свойства и действия, които са постоянни за всич­ки обекти от дадения клас. Достъпът до статичните членове на типа се извърша директно, а не през инстанция, както в следващия пример:

class Bulgaria

{

private static int mNumberOfCities = 267;


public static int NumberOfCities

{

get



{

return mNumberOfCities;

}

}
public static void AddCity(string aCityName)



{

mNumberOfCities++;

// ...

}
// ...


static void Main()

{

Console.WriteLine(



"В България има {0} града.", Bulgaria.NumberOfCities);

}

}



В примера видяхме дефинирането и използването на статични полета, методи и свойства. Използвахме статичните свойства без да инстанцираме класа Bulgaria никъде.



Важна особеност, която трябва да имаме предвид при използването на статични методи и свойства, е че те могат да използват само статични полета. Полетата, които са обвър­зани с инстанция могат да се достъпват само в ней­ния контекст, а статичните методи и свойства са незави­сими от инстанцията.

Статичните полета на типа много приличат на глобалните променливи в по-старите езици за програмиране като C, C++ и Pascal. Както глобалните променливи, статичните полета са достъпни от цялото приложение и имат само една инстанция.

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


Конструктори


Конструкторите се използват при създаване на обекти и служат за инициализация, или начално установяване на състоянието на полетата на обекта. Механизмът на работа и синтаксисът за дефиниране на кон­струк­то­рите в C# са подобни на други обектно-ориентирани езици, като Java и C++ с някои особености, на които ще обърнем внимание. Допуска се използването на повече от един конструктор, като кон­структорите трябва да се раз­личават по броя и/или типа на параметрите. Възможно е и да не се де­фи­нира конструктор и в такъв случай компила­торът създава подразбиращ се – публичен, с празно тяло и без параметри.

Инициализиране на полетата


Съществуват три възможности за инициализацията на полетата на обекта – да бъдат инициализират в конструктор, при декларацията им или да нямат изрично зададена стойност.

Инициализациите, описани в тялото на конструктора се изпълняват по време на изпълнението този конструктор – при създаване на обект от съответния клас с ключовата дума new в C#.

Инициализациите, дефинирани при декларацията на полетата се из­пъл­ня-ват директно преди конструктора. Можем да приемем, че при ком­пи­ла­ци­я­та инициализациите на полетата се добавят в началото на всеки кон­струк­тор. Всъщност C# компилаторът прави точно това скрито от програмиста – поставя код, който инициализира всички член-променливи на типа във всички негови конструктори.

Полетата, които нямат зададена начална стойност, получават стойност по под­разбиране (нулева стойност). Това поведение се изисква от специ­фикацията на езика C# и не зависи от конкретната имплементация на компилатора.


Конструктори – пример


Със следващия пример ще разгледаме примерни дефиниции на кон­струк­то­ри на базов клас с един наследник:

class Student

{

private string mName;



private int mStudentId;

private string mPosition = "Student";

public Student(string aName, int aStudentId)

{

mName = aName;



mStudentId = aStudentId;

}

public Student(string aName) : this(aName, -1)



{

}
public static void Main()

{

Student s = new Student("Бай Киро", 12345);



}

}
public class Kiro : Student

{

public Kiro() : base("Бай Киро", 12345)



{

}
// ...

}


Забелязваме употребата на ключовите думи this и base след де­фи­ни­ци­я­та на конструкторите на класа. Те представляват съответно обръщения към друг конструктор на същия клас и към конструктор на базовия клас, като в скобите се изреждат параметрите, които се подават на извиквания конструктор. В примера е използване наследяване, на което ще с спрем в детайли след малко (класът Kiro наследява класа Student).

Изследване на MSIL кода за конструкторите в C#


В следващата демонстрация ще си послужим с инструмента IL DASM (ildasm.exe), който е част от .NET Framework SDK, за да разгледаме MSIL кода, който C# компилаторът ге­нерира за класа Student, който дефини­рахме в примера по-горе. С това упражнение не само ще се запознаем с работата с инструмента, но и ще забележим особеностите в генерирания код, свързани с полетата със зададена стойност при декларацията. Ето стъпки­те, които трябва да на­пра­вим:

  1. Отваряме Demo-1-Constructors.sln, елементарен Visual Studio .NET проект с единствен C# файл, който съдържа кода от горния пример. Компилираме проекта.

  2. Стартираме командния интерпретатор към Visual Studio .NET. Не използваме стан­дартния cmd.exe, а този, който се намира в Start -> Programs -> Microsoft Visual Studio 2003 -> Visual Studio Tools, защото той се стартира с регистрирани пътища към .NET ин­стру­ментите, които се използват от командния ред.

  3. Избираме директорията, където се намира изпълнимият файл, получен при компилиране на проекта – Demo-1-Constructors.exe. Ако не сме променили настройките на Visual Studio .NET, това ще е директорията <директория на проекта>\bin\Debug.

  4. Извикваме от командния ред инструмента ildasm и му подаваме като параметър компилираното приложение:

ildasm Demo-1-Constructors.exe

Ето как изглежда прозорецът на инструмента, в който е заредено асемблито от приложението, когато разпънем всички елементи от дър­вото:

IL DASM показва дърво за асемблито, в което различаваме класа Student и членовете му. Ако се придвижим по дървото до конструк­торите на класа, можем да изследваме техния IL код, както е показано на след­ващата картинка:



В кода, генериран за конструктора с един параметър, се вижда обръ­ще­нието към този с два параметъра. Ако повторим същото действие и с втория конструктор, можем да наблюдаваме и неговия IL код (на картинката по-долу).

Забелязваме, че задаването на стойност на полетата с инициа­лизация при декларацията реално се извършва в началото на втория конструк­тор. Реално тези полета се инициализират и от първия конструктор, защото той извиква втория.


Singleton клас


В този пример ще представим един популярен шаблон в обектно-ориенти­рания дизайн – клас, който може да има най-много една инста­нция в рам­ки­те на цялото приложение. Такъв клас наричаме singleton. За ре­а­ли­зи­ра­нето на такива класове се използва следният подход:

public sealed class Singleton

{

private static Singleton mInstance = null;


private Singleton()

{

}


public static Singleton Instance

{

get



{

if (mInstance == null)

{

mInstance = new Singleton();



}

return mInstance;

}

}

}



Целта на задаването на private видимост за конструктора на класа е за да не могат да се създават инстанции освен от членове на класа, както в случая статичното свойство Instance. В дефиницията на класа е из­пол­зва­на ключовата дума sealed, която указва, че класът не може да бъде наследяван.

Горният пример само демонстрира използването на sealed класове и час­тен конструктор. В реална ситуация при реализацията на singleton шаб­лона трябва да се вземе предвид, че е възможно няколко нишки (threads) едновременно да се опитат да извлекат инстанцията на singleton класа и да се получи нежелано поведение. Затова обикновено реализацията на този шаблон изисква допълнителни усилия за нишково обезопасяване на работата на класа. На работата с нишки ще обърнем специално внимание в темата "Многонишково програмиране и синхронизация".


Статичен конструктор


Конструкторите, подобно на други видове членове на класа, могат да бъдат обявени за статични, с тази особеност че статичният конструктор може да бъде най-много един и не може да приема параметри и модифи­катори за достъп.

Извикване на статичен конструктор


Статичният конструктор се използва за инициализация на статичните членове и се извиква автоматично. Извикването на статичният кон­струк­тор се извършва "зад кулисите" от CLR. Това става по време на изпълне­нието на про­гра­мата и мо­мен­тът на стартирането му не е точно опре­делен. Това, което е сигурно, е че статичният конструктор е вече извикан когато се създаде първата инстанция на кла­са или когато се достъпи някой негов статичен член. В рамките на програмата, ста­тич­ният кон­структор може да бъде извикан най-много веднъж.

Статичен конструктор – пример


В следващия пример ще разгледаме класа SqrtPrecalculated, който из­ползва ста­тичен конструктор:

class SqrtPrecalculated

{

public const int MAX_VALUE = 10000;



private static int[] mSqrtValues; // static field
// Static constructor

static SqrtPrecalculated()

{

mSqrtValues = new int[MAX_VALUE + 1];



for (int i = 0; i <= MAX_VALUE; i++)

mSqrtValues[i] = (int) Math.Sqrt(i);

}
// Static method

public static int GetSqrt(int aValue)

{

return mSqrtValues[aValue];



}
static void Main()

{

Console.WriteLine(GetSqrt(1000));



}

}


Класът SqrtPrecalculated служи за бързо изчисляване на корен квад­ратен. Той предоставя статичния метод SqrtPrecalculated(), който връ­ща цялата част на квадратния корен на ар­гу­мен­та си.

За по-голямо бър­зо­дей­ствие всички квадратни корени на числата от 0 до 10000 се из­чис­ля­ват предварително в статичния конструктор и после се използват наготово. Мно­жеството от стойностите се съхранява в статич­ното по­ле mSqrtValues[], което се инициализира в статичния конструк­тор, който се изпълнява преди първия опит за достъп до класа.

Ще илю­стрираме поведението на статичните конструктори в .NET Framework, ка­то с помощта на дебъгера на VS.NET наблюдаваме как преди да започне да бъде използван даден клас се изпълнява първо статичният му кон­струк­тор.

Проследяване на изпълнението на примера


Ще използваме дебъгера на Visual Studio .NET за да проследим из­пъл­не­нието на кода от горния пример, който се съдържа в приложението Demo-2-TestStaticConstructor от демон­стра­циите­. Ще изпълним последова­телно следни­те стъп­ки:

  1. Отваряме с VS.NET TestStaticConstructor.sln и го ком­пи­лираме.

  2. Слагаме точки на прекъсване (breakpoints) на първия ред на ста­тич­ния конструктор (static SqrtPrecalculated()) и във фун­к­ци­я­та Main() като щракаме с мишката на равнището на тези редове в празното поле от ляво на областта за редактиране на код. След като поставим точките на прекъсване средата изглежда по следния начин:



  1. Стартираме програмата в дебъг режим (от меню Debug -> Start или с [F5]) и проследяваме как дебъгерът на Visual Studio .NET спира първо в статичния конструктор, а след като му зададем да продължи, спира в метода Main(). Това илюстрира как функцио­налността от статичния конструктор се изпълнява преди първото използване на класа. Ето как изглежда средата в момента, в който програмата е спряла в статичния конструктор:


Свойства


Свойствата са членове на класовете, структурите и интерфейсите, които обикновено се изпол­зват за да контролират достъпа до полетата на типа.

Свойствата приличат на член-променливите по това, че имат име, по-което се достъпват, и стойност от някакъв предварително определен тип. От гледна точка на синтаксиса за достъп до тях, свойствата изглеждат по същият начин както полетата. Разликата се състои в това, че свойства съдържат код, който се изпълнява при обръщение към тях, т. е. извър­шват действия. Свойствата могат да бъдат и ста­тич­ни.


Прочитане и присвояване на стойност


Свойствата могат да имат два компонента (accessors):

  • код за прочитане на стойността (get accessor)

  • код за присвояване на стойността (set accessor)

Когато създаваме свойства можем да предоставим дефиниции на двата компонента, както и на само един от тях, но задължително трябва да е дефиниран поне единият. Според предоставените компоненти делим свой­ствата на три вида:

  • Свойства само за чете (read only) - такива, които дефинират само код за прочитане на стойността им.

  • Свойства за четене и писане (read and write) - когато имат и двата компонента.

  • Свойства само за писане (write only) - когато е предоставен само код за присвояване на стойност.

Пример за свойства


Ще дефинираме класа Person за да илюстрираме дефинирането и из­пол­зва­нето на свойства:

public class Person

{

private string mName;



private DateTime mDateOfBirth;
// Property Name of type string

public string Name

{

get


{

return mName;

}

set


{

if ((value != null) && (value.Length > 0))

{

mName = value;



}

else


{

throw new ArgumentException("Invalid name!");

}

}

}


// Property DateOfBirth of type DateTime

public DateTime DateOfBirth

{

get


{

return mDateOfBirth;

}

set


{

if ((value.Year >= 1900) &&

(value.Year <= DateTime.Now.Year))

{

mDateOfBirth = value;



}

else


{

throw new ArgumentOutOfRangeException(

"Invalid date of birth!");

}

}



}
// Read-only property Age of type int

public int Age

{

get


{

DateTime now = DateTime.Now;

int yearsOld = now.Year - mDateOfBirth.Year;

DateTime birthdayThisYear =

new DateTime(now.Year, mDateOfBirth.Month,

mDateOfBirth.Day, mDateOfBirth.Hour,

mDateOfBirth.Minute, mDateOfBirth.Second);

if (DateTime.Compare(now, birthdayThisYear) < 0)

{

yearsOld--;



}

return yearsOld;

}

}

}


// Property usage example

class PropertiesDemo

{

static void Main()



{

Person person = new Person();

person.Name = "Svetlin Nakov";

person.DateOfBirth = new DateTime(1980, 6, 14);

Console.WriteLine("{0} is born on {1:dd.MM.yyyy}.",

person.Name, person.DateOfBirth);

Console.WriteLine("{0} is {1} years old.",

person.Name, person.Age);

}

}


В примерния клас виждаме дефинициите на две свойства за четене и писане - Name от тип string и DateOfBirth от тип DateTime, както и едно само за четене – Age от тип int.

Можем да доловим различните аспекти на употребата на свойства - едно свойство може да бъде просто обвивка около поле на типа, но може и да реализира по-сложна логика. Например свойствата Name и DateOfBirth в примера просто връщат стойността на полетата, които обвиват, или я задават след съответните проверки за валидност. Свойство може да бъде и абстракция на данни, извличането и съхранението на които би могло да бъде свързано със сложна обработка. Опростен пример за това е Age, което връща стойност, резултат от извършване на изчисления, в случая разликата между те­ку­щата дата и рождената дата на лицето.


Проследяване на изпълнението на свойствата


Ще си изясним работата със свойства като проследим хода на програмата по време на достъпа до тях. За целта ще си послужим с кода от примера, който се съдържа в приложението Demo-4-Properties от демонстрациите. Той съдържа горния пример. Нека изпълним следните стъпки:

  1. Отваряме с VS.NET Demo-4-Properties.sln и го ком­пи­лираме.

  2. Стартираме програмата в режим на проследяване с [F11]. Програ­мата спира изпълнението си на първия ред, но без той да е изпълнен, ето така:



  1. Натискаме отново [F11] при което се създава обекта person и мар­ке­рът спира на следващия ред.

  2. Когато още веднъж натиснем [F11], забелязваме, че кодът, който след­ва да бъде изпълнен, е тялото на компонента за достъп до свойството Name на класа Person:

Това ни показва, че зад операцията "присвояване на стойност" на свойството стои кодът му за присвояване.



  1. С [Shift-F11] продължаваме изпълнението на програмата до на­пус­ка­нето на текущият блок – то спира отново на следващия ред в тя­ло­то на метода Main().

  2. С [F10] продължаваме изпълнението на програмата с още една стъпка. Резултатът е същият, както при натискането на [F11] с тази разлика, че не се изпълняват стъпка по стъпка вложените блокове. Така преминаваме през изпълнението на кода за присвоя­ване на стойност на свойството DateOfBirth на "един дъх" и мар­керът се позиционира на операцията Console.WriteLine("{0} is born on {1:dd.MM.yyyy}.", person.Name, person.DateOfBirth).

  3. Ако в този момент натиснем [F11] ставаме свидетели на изпълне­ние­то и на кода за достъп на свойството Name:

Така се убеждаваме, че обръщението към свойство се равнява на изпълнение на кода му за прочитане на стойност.



  1. С [F5] продължаваме изпълнението на програмата до края и виждаме резултата от изпълнението и:





В режим на дебъгване прозорецът, в който се изпъл­нява приложението, се затваря веднага след прик­лючване на изпълнението на кода и резултатът трудно може да бъде видян. Ако искаме да видим отпеча­тания резултат, трябва или да сложим точка на пре­късване преди края на Main() метода, или да се прид­вижим до последната операция стъпка по стъпка или да изпълним програмата не с [F5], а с [Ctrl-F5].

  1. Нека изследваме с IL DASM генерирания междинен код за пример­ното приложение, за да си изясним вътрешното представяне на свойствата.

Като стартираме ildasm и разгледаме с него IL кода за класа Person, забелязваме нещо много интересно – в класа Person има методи с префикс set_, отговарящи на компонен­тите за прис­вояване на дефинираните от нас свойства, и методи с префикс get_, които съответстват на компонентите за връщане на стойност.

На практика след компилация get и set частите на свойствата са се превърнали в методи, а достъпът до тях се е превърнал в операции за извикване на метод. Това е начинът, по който C# компилаторът компилира свойствата – превръща ги в методи, а достъпът до тях превръща в извиквания на методи.

Ето как изглежда класът Person в инструмента IL DASM:


Индексатори


Индексаторите в C# (indexers) са членове на класовете, структурите и интерфейсите, които предоставят индексиран достъп до данни на типа, подобно на достъпа до елементите на масив.

Индексаторите по синтаксис и семантика много приличат на свойства, но получават като параметър индекс на елемент, с който да работят. На практика, те представляват свойства, приемащи параметър и дори в някои .NET езици, например VB.NET, синтаксисът на декларирането им е същият като при свойствата.


Индексатори – пример


За да си изясним най-лесно как се дефинират индексатори, да разгледаме следния пример:

private object[] mElements;
public object this[int index]

{

get



{

return mElements[index];

}

}


Виждаме, че дефиницията на индексатор прилича на тази на свойство, но има и някои разлики. На индексатора не се задава име, а вместо него се задава запазената дума this.

Достъпът до индексатор на обект се извършва посредством името на променливата от типа, дефиниращ индексатора, последвана от индекса в квад­ратни скоби, също както се извършва достъпа до елемент на масив, например myArrayList[5].

Позовавайки се на начина, по който се обръщаме към индексаторите, можем да ги разглеждаме като средство за предефиниране на оператора []. Използването на индексатори позволява интуитивен достъп до обекти, които се състоят от множество компоненти, каквито са масивите и ко­лек­ци­ите.

Имитация на масив чрез индексатори – пример


За да илюстрираме по-пълно дефинирането и използването на индекса­тори, ще използваме след­ващия пример. Ще дефинираме клас, който ими­тира поведе­нието на масив от 32 стойности, всяка от които е или 0 или 1:

struct BitArray32

{

private uint mValue;



// Indexer declaration

public int this [int index]

{

get


{

if (index >= 0 && index <= 31)

{

// Check the bit at position index



if ((mValue & (1 << index)) == 0)

return 0;

else

return 1;



}

else


{

throw new ApplicationException(String.

Format("Index {0} is invalid!", index));

}

}



set

{

if (index < 0 || index > 31)



throw new ApplicationException(

String.Format("Index {0} is invalid!", index));


if (value < 0 || value > 1)

throw new ApplicationException(

String.Format("Value {0} is invalid!", value));
// Clear the bit at position index

mValue &= ~((uint)(1 << index));


// Set the bit at position index to value

mValue |= (uint)(value << index);

}

}

}


class IndexerTest

{

static void Main()



{

BitArray32 arr = new BitArray32();


arr[0] = 1;

arr[5] = 1;

arr[5] = 0;

arr[25] = 1;

arr[31] = 1;
for (int i=0; i<=31; i++)

{

Console.WriteLine("arr[{0}] = {1}", i, arr[i]);



}

}

}



Класът BitArray32 представлява масив от битове с 32 елемента, който вътрешно съхранява стойностите им в едно 32-битово поле. Елементите му достъпваме посредством дефини­рания индексатор по същия начин, по който достъпваме елементите на вградените в CTS масиви. На масивите в .NET Framework ще се спрем в темата "Масиви и колекции".

Виждаме компонентите за прочитане и присвояване на стойността, които извършват проверка дали индексът е в съответния диапазон, след което чрез битови операции осъществяват достъп до посочения като параметър бит. При невалидни параметри се предизвиква изключение, чрез което се уведомява извикващия код за проблема. На изключенията ще се спрем подробно в темата "Управление на изключенията в .NET".


Проследяване на работата на индексатор


За да проследим работата на индексатора ще си послужим с приложе­ни­е­то от демонстрациите Demo-5-Indexers.sln, което съдържа кода от горния при­ме­р. Ще изпълним следните стъпки:

  1. Отваряме приложението и го компилираме.

  2. С [F11] стартираме изпълнение в режим на проследяване и маркерът се позиционира на първия ред от тялото на метода Main():



  1. Със следващото натискане на [F11] инициализираме обекта arr с подразбиращия се конструктор и текущ ред става присвояването arr[0] = 1.

  2. Когато продължим проследяването, виждаме как следващият код, който се изпълнява, е компонента за присвояване на стойност на индексатора:



  1. С [Shift-F11] прескачаме останалата част от блока и преминаваме с [F10] през другите присвоявания докато достигнем до цикъла, който про­чита стойностите от масива:



  1. В този момент при натискане на [F11] се изпълнява кодът за про­чи­та­не на стойността от масива. Забелязваме, че механизмът на из­пъл­нение на индексаторите е същият, както на свойствата.



  1. С помощта на инструмента IL DASM можем да си обясним при­ли­ки­те между свойства и индексатори. Когато разгледаме генерирания за приложението MSIL код виждаме, че индексаторите, както свой­ства­та, се реализират от двойка методи с имена get_Item и set_Item:


Индексатори с няколко параметъра


В .NET Framework се допуска дефинирането на индексатори, приемащи повече от един параметър. Примерно обръщение към такъв индексатор е конструкцията personInfo["Бай Иван", 68]. Възможно е в един тип да се дефинират и ня­колко индексатора с различен набор от параметри. Индек­са­торите не могат да бъдат статични, тъй като реализират индексиране в рамките на дадена инстанция.

Ето още един пример за индексатор, който приема два параметъра от тип символен низ и връща целочислена стойност:



class DistanceCalculator

{

public int this[string aTown1, string aTown2]



{

get


{

if (aTown1.Equals("София") && aTown2.Equals("Варна"))

return 470;

else


throw new ApplicationException("Unknown distance!");

}

}



}
class DistanceTest

{

static void Main()



{

DistanceCalculator dc = new DistanceCalculator();

Console.WriteLine("Разстоянието между {0} и {1} е {2} " +

"километра.", "София", "Варна", dc["София", "Варна"]);

}

}


В примера е реализиран клас, който по дадени имена на два град връща разстоянието между тях. Разбира се, тази функционалност не е реали­зи­рана напълно, но целта на примера е да се илюстрира работата с индек­сатори, а не да се даде завършен проект, който работи.




Сподели с приятели:
1   ...   21   22   23   24   25   26   27   28   ...   73




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

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