Глава 14. Дефиниране на класове

В тази тема...

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

 

Съдържание

Видео

Презентация

Мисловни карти


Собствени класове

"... Всеки модел представя някакъв аспект от реалността или някаква интересна идея. Моделът е опростяване. Той интерпретира реалността, като се фокусира върху аспектите от нея, свързани с решаването на проблема и игнорира излишните детайли." [Evans]

Целта на всяка една програма, която създаваме, е да реши даден проблем или да реализира някаква идея. За да измислим решението, ние първо създа­ваме опростен модел на реалността, който не отразява всички факти от нея, а се фокусира само върху тези, които имат значение за намира­нето на решение на нашата задача. След това, използвайки модела, намираме решение (т.е. създаваме алгоритъма) на нашия проблем и това решение го описваме чрез средствата на даден език за програми­ране.

В днешно време най-често използваният тип езици за програмиране са обектно-ориенти­раните. И тъй като обектно-ориентираното програмиране (ООП) е близко до начина на мислене на човека, то ни дава възмож­ността с лекота да описваме модели на заобикалящата ни среда. Една от причините за това е, че ООП ни предоставя средство, за описание на съвкупността от понятия, които описват обектите във всеки модел. Това средство се нарича клас (class). Понятието клас и дефинирането на собствени класове, различни от системните, е вградена възможност на езика C# и целта на настоящата глава е да се запознаем с него.

Да си припомним: какво са класовете и обектите?

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

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

Например, ако имаме клас Dog, описващ някакви характеристики на куче от реалния свят, казваме, че обектите, които са създадени по описанието на този клас (например кученцата "Шаро" и "Рекс") са от тип класa Dog. Това означение е същото, както когато казваме, че низът "some string" е от класа String. Разликата е, че обектът от тип Dog е екземпляр от клас, който не е част от библио­теката с класове на .NET Framework, а е дефиниран от самите нас.

Какво съдържа един клас?

Всеки клас съдържа дефиниция на това какви данни трябва да се съдържат в един обект, за да се опише състоянието му. Обектът (конкретния екземпляр от този клас) съдържа самите данни. Тези данни дефинират състоянието му.

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

Елементи на класа

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

Основните елементи на класовете в C# са следните:

-     Декларация на класа (class declaration) – това е редът, на който декларираме името на класа. Например:

public class Dog

-     Тяло на клас – по подобие на методите, класовете също имат част, която следва декларацията им, оградена с фигурни скоби – "{" и "}" между които се намира съдържанието на класа. Тя се нарича тяло на класа. Елементите на класа, които се описват в тялото му са изброени в следващите точки.

public class Dog

{

      // ... The body of the class comes here ...

}

-     Конструктор (constructor) – това е псевдометод, който се из­пол­зва за създа­ване на нови обекти. Така изглежда един конструктор:

public Dog()

{

      // ... Some code ...

}

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

// Field definition

private string name;

-     Свойства (properties) – така наричаме характеристиките на даден клас. Обикновено стойността на тези характеристики се пази в полета. Подобно на полетата, свойствата могат да бъдат притежа­вани само от конкретен обект или да са споделени между всички обекти от тип даден клас.

// Property definition

private string Name { get; set; }

-     Методи (methods) – от главата "Методи", знаем, че методите представляват именувани блокове програмен код. Те извършват някакви действия и чрез тях реализират поведението на обектите от този клас. В методите се изпълняват алгоритмите и се обработват данните на обекта.

Ето как изглежда един клас, който сме дефинирали сами и който прите­жа­ва елементите, които описахме току-що:

// Class declaration

public class Dog

{     // Opening brace of the class body

 

      // Field declaration

      private string name;

 

      // Constructor declaration

      public Dog()

      {

            this.name = "Balkan";

      }

 

      // Another constructor declaration

      public Dog(string name)

      {

            this.name = name;

      }

 

      // Property declaration

      public string Name

      {

            get { return name; }

            set { name = value; }

      }

 

      // Method declaration

      public void Bark()

      {

            Console.WriteLine("{0} said: Wow-wow!", name);

      }

}     // Closing brace of the class body

За момента няма да обясняваме в по-големи детайли изложения код, тъй като подробна информация ще бъде дадена при обяснението как се декларира всеки един от елементите на класа.

Използване на класове и обекти

В главата "Създаване и използване на обекти" видяхме подробно как се създават нови обекти от даден клас и как могат да се използват. Сега накратко ще си припомним как ставаше това.

Как да използваме дефиниран от нас клас?

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

За да можем да манипулираме новосъздадения обект, ще трябва да го присвоим на променлива от типа на неговия клас. По този начин в тази променлива ще бъде запазена връзка (референция) към него.

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

Пример – кучешка среща

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

static void Main()

{

      string firstDogName = null;

      Console.WriteLine("Write first dog name: ");

      firstDogName = Console.ReadLine();

 

      // Using a constructor to create a dog with specified name

      Dog firstDog = new Dog(firstDogName);

 

      // Using a constructor to create a dog wit a default name

      Dog secondDog = new Dog();

 

      Console.WriteLine("Write second dog name: ");

      string secondDogName = Console.ReadLine();

 

      // Using property to set the name of the dog

      secondDog.Name = secondDogName;

 

      // Creating a dog with a default name

      Dog thirdDog = new Dog();

 

      Dog[] dogs = new Dog[] { firstDog, secondDog, thirdDog };

 

      foreach (Dog dog in dogs)

      {

            dog.Bark();

      }

}

Съответно изходът от изпълнението ще бъде следният:

Write first dog name:

Bobcho

Write second dog name:

Walcho

Bobcho said: Wow-wow!

Walcho said: Wow-wow!

Balkan said: Wow-wow!

В примерната програма, с помощта на Console.ReadLine(), получаваме имената на обектите от тип куче, които потребителят трябва да въведе от конзолата.

Присвояваме първия въведен низ на променливата firstDogName. След това използваме тази променлива при създаването на първия обект от тип DogfirstDog, като я подаваме като параметър на конструктора.

Създаваме втория обект от тип Dog, без да подаваме низ за името на кучето на конструктора му. След това, чрез Console.ReadLine(), въвеж­даме името на второто куче и получената стойност директно подаваме на свойството Name. Извикването му става чрез точкова нотация, приложена към променливата, която пази референция към втория създаден обект от тип Dog – secondDog.Name.

Когато създаваме третия обект от тип Dog, не подаваме име на кучето на конструктора, нито след това модифицираме подразбиращата се стойност "Balkan".

След това създаваме масив от тип Dog, като го инициализираме с трите обекта, които току-що създадохме.

Накрая, използваме цикъл, за да обходим масива от обекти от тип Dog. На всеки елемент от масива, отново използвайки точкова нотация, извикваме метода Bark() за съответния обект чрез dog.Bark().

Природа на обектите

Нека припомним, че когато в .NET създадем един обект, той се състои от две части – същинска част от обекта, която съдържа неговите данни и се намира в частта от оперативната памет, наречена динамична памет (heap) и референция към този обект, която се намира в друга част от оперативната памет, където се държат локалните променливи и параметрите на мето­дите, наречена стек (stack).

Например, нека имаме клас Dog, на който характеристиките му са име (name), порода (kind) и възраст (age). Създаваме променлива dog от този клас. Тази променлива се явява референция (указател) към обекта в динамичната памет (heap).

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

clip_image002

Когато декларираме една променлива от тип някакъв клас, но не искаме тя да е инициализирана с връзка към конкретен обект, тогава трябва да й присвоим стойност null. Ключовата дума null в езика C# означава, че една променлива не сочи към нито един обект (липса на стойност):

clip_image004

Съхранение на собствени класове

В C# единственото ограничение относно съхранението на наши собствени класове е те да са във файлове с разширение .cs. В един такъв файл може да има няколко класа, структури и други типове. Въпреки че компилаторът не го изисква, е препоръчително всеки клас да се съхранява в отделен файл, който съответства на името му, т.е. класът Dog трябва да е записан във файл с име Dog.cs.

Вътрешна организация на класовете

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

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

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

Пространствата от имена съдържат декларации на класове, структу­ри, интерфейси и други типове данни, както и други пространства от имена. Пример за вложени пространства от имена е пространството от имена System, което съдържа пространството от имена Data. Името на вложеното пространство е System.Data.

Пълното име на класа в .NET Framework е името на класа, предшествано от името на пространството от имена, в което той е деклариран: <namespace_name>.<class_name>. Чрез using директивите можем да из­ползваме типовете от дадено пространство от имена, без да уточняваме пълното му име. Например:

using System;

DateTime date;

вместо

System.DateTime date;

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

// Using directives - optional

using <namespace1>;

using <namespace2>;

 

// Namespace definition - optional

namespace <namespace_name>

{

      // Class declaration

      class <first_class_name>

      {

            // ... Class body ...

      }

 

      // Class declaration

      class <second_class_name>

      {

            // ... Class body ...

      }

 

      // ...

 

      // Class declaration

      class <n-th_class_name>

      {

            // ... Class body ...

      }

}

Декларирането на пространство от имена и съответно включването на пространства от имена са вече обяснени в главата "Създаване и използ­ване на обекти" и затова няма да ги дискутираме отново.

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

Кодиране на файловете. Четене на кирилица и Unicode

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

Вътрешно в .NET Framework компилираният код се представя в Unicode кодиране и затова няма проблеми, ако във файла използваме символи, които са от азбуки, различни от латинската, например на кирилица:

using System;

 

public class EncodingTest

{

      // Тестов коментар

      static int години = 4;

 

      static void Main()

      {

            Console.WriteLine("години: " + години);

      }

}

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

За да направим това или ако искаме да използваме различно кодиране от Unicode, трябва да асоциираме съответното кодиране с файла. При отва­ряне на файлове това става по следния начин:

1.  От File менюто избираме Open и след това File.

2.  В прозореца Open File натискаме стрелката, съседна на бутона Open и избираме Open With.

3.  От списъка на прозореца Open With избираме Editor с encoding support, например CSharp Editor with Encoding.

4.  Натискаме [OK].

5.  В прозореца Encoding избираме съответното кодиране от пада­що­то меню Encoding.

6.  Натискаме [OK].

clip_image006

За запаметяване на файлове във файловата система в определено кодиране стъпките са следните:

1.  От менюто File избираме Save As.

2.  В прозореца Save File As натискаме стрелката, съседна на бутона Save и избираме Save with Encoding.

3.  В Advanced Save Options избираме желаното кодиране от списъ­ка Encoding (за предпочитане е универсалното кодиране UTF-8).

4.  От Line Endings избираме желания вид за край на реда.

Въпреки че имаме възможността да използваме символи от други азбуки, в .cs файловете, e препоръчително да пишем всички идентификатори и коментари на английски език, за да може кодът ни да е разбираем за по­вече хора по света.

Представете си само, ако ви се наложи да дописвате код, писан от виетна­мец, където имената на променливите и коментарите са на виет­намски език. Не искате да ви се случва, нали? Тогава се замислете как ще се почувства един виетнамец, ако види променливи и коментари на българ­ски език.

Модификатори и нива на достъп (видимост)

Нека си припомним, от главата "Методи", че модификатор наричаме ключова дума с помощта, на която даваме допълнителна информация на компилатора за кода, за който се отнася модификаторът.

В C# има четири модификатора за достъп. Те са public, private, protected и internal. Модификатори за достъп могат да се използват само пред следните елементи на класа: декларация, полета, свойства и методи на класа.

Модификатори и нива на достъп

Както обяснихме, в C# има четири модификатора за достъп – public, private, protected и internal. С тях ние ограничаваме или позволяваме достъпа (видимостта) до елементите на класа, пред които те са поставени. Нивата на достъп в .NET биват public, protected, internal, protected internal и private. В тази глава ще се занимаем подробно само с public, private и internal. Повече за protected и protected internal ще научим в главата "Принципи на обектно-ориенти­раното програмира­не".

Ниво на достъп public

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

Ниво на достъп private

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

Ниво на достъп internal

Модификаторът internal се използва, за да се ограничи достъпът до елемента само от файлове от същото асембли, т.е. същия проект във Visual Studio. Когато във Visual Studio направим няколко проекта, класовете от тях ще се компилират в различни асемблита.

Асембли (assembly)

Асембли (assembly) е колекция от типове и ресурси, която формира логическа единица функционалност. Всички типове в C# и изобщо в .NET Framework могат да съществуват само в асемблита. При всяка компилация на .NET приложение се създава асембли. То се съхранява като файл с разширение .exe или .dll.

Деклариране на класове

Декларирането на клас следва строго определени правила (синтаксис):

[<access_modifier>] class <class_name>

Когато декларираме клас, задължително трябва да използваме ключовата дума class. След нея трябва да стои името на класа <class_name>.

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

Видимост на класа

Нека имаме два класа – А и В. Казваме, че класът А, има достъп до класа В, ако може да прави едно от следните неща:

1.  Създава обект (инстанция) от тип класа В.

1.  Достъпва определени методи и член-променливи (полета) в класа В, в зависимост от нивото на достъп на съответните методи и полета.

Има и трета операция, която може да бъде извършвана с класове, когато видимостта им позволява, наречена наследяване на клас, но на нея ще се спрем по-късно, в главата "Принципи на обектно-ориентираното програмира­не".

Както разбрахме, ниво достъп означава "видимост". Ако класът А не може да "види" класа В, нивото на достъп на методите и полетата в класа В нямат значение.

Нивата на достъп, които един невложен клас може да има, са само public и internal.

Ниво на достъп public

Ако декларираме един клас с модификатор за достъп public, ще можем да го достъпваме от всеки един клас и от всичко едно пространство от имена, независимо къде се намират те. Това означава, че всеки друг клас ще може да създава обекти от тип този клас и да има достъп до методите и полетата на класа (стига тези полета да имат подходящо ниво на достъп).

Не трябва да забравяме, че ако искаме да използваме клас с ниво на достъп public от друго пространство от имена, различно от текущото, трябва да използваме конструкцията за включване на пространства от имена using или всеки път да изписваме пълното име на класа.

Ниво на достъп internal

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

Ако във Visual Studio имаме два проекта в общ solution и искаме от единия проект да използваме клас, дефиниран в другия проект, то реферираният клас трябва задължително да е public.

Ниво на достъп private

За да сме изчерпателни, трябва да споменем, че като модификатор за достъп до клас, може да се използва модификаторът за видимост private, но това е свързано с понятието "вътрешен клас" (nested class), което ще разгледаме в секцията "Вътрешни класове".

Тяло на класа

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

[<access_modifier>] class <class_name>

{

      // ... Class body – the code of the class goes here ...

}

Тялото на класа започва с отваряща фигурна скоба "{" и завършва със затваряща – "}". Класът винаги трябва да има тяло.

Правила за именуване на класовете

По подобие на декларирането на име на метод, за създаването на име на клас съществува следния общоприет стандарт:

1.  Имената на класовете започват с главна буква, а останалите букви са малки. Ако името е съставено от няколко думи, всяка дума започ­ва с главна буква, без да се използват разделители между думите.

2.  За имена на класове обикновено се използват съществителни име­на.

3.  Името на класовете е препоръчително да бъде на английски език.

Ето няколко примера за имена на класове, които са правилно именувани:

Dog

Account

Car

BufferedReader

Повече за имената на класовете ще научите в главата "Качествен програмен код".

Ключовата дума this

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

this.myField; // access a field in the class

this.DoMyMethod(); // access a method in the class

this(3, 4); // access a constructor with two int parameters

За момента няма да обясняваме изложения код. Разяснения ще дадем по-късно, в местата от секциите на тази глава, посветени на елементите на класа (полета, методи, конструктори) и засягащи ключовата дума this.

Полета

Както стана дума в началото на главата, когато декларираме клас, описваме обект от реалния свят. За описанието на този обект, се фокусираме само върху характеристиките му, които имат отношение към проблема, който ще решава нашата програма.

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

Деклариране на полета в даден клас

До момента сме се сблъсквали само с два типа променливи (вж. главата "Методи"), в зависимост от това къде са декларирани:

1.  Локални променливи – това са променливите, които са дефини­ра­ни в тялото на някой метод (или блок).

2.  Параметри – това са променливите в списъка с параметри, които един метод може да има.

В C# съществува и трети вид променливи, наречени полета (fields) или член-променливи на класа (instance variables).

Те се декларират в тялото на класа, но извън тялото на блок, метод или конструктор (какво е конструктор ще разгледаме подробно след малко).

clip_image007

Полетата се декларират в тялото на класа, но извън тялото на метод, конструктор или блок.

Ето един примерен код, в който се декларират няколко полета:

class SampleClass

{

      int age;

      long distance;

      string[] names;

      Dog myDog;

}

Формално, декларацията на полетата става по следния начин:

[<modifiers>] <field_type> <field_name>;

Частта <field_type> определя типа на даденото поле. Той може да бъде както примитивен тип (byte, short, char и т.н.) или масив, така и от тип някакъв клас (например Dog или string).

Частта <field_name> е името на даденото поле. Както при имената на обикно­вените променливи, когато именуваме една член-променлива, трябва да спазваме правилата за именуване на идентификатори в C# (вж. главата "Примитивни типове и променливи").

Частта <modifiers> е понятие, с което сме означили както модифика­то­ри­те за достъп, така и други модификатори. Те не са задължителна част от декларацията на едно поле.

Модификаторите и нивата на достъп, позволени в декларацията на едно поле, са обяснени в секцията "Видимост на полета и методи".

В тази глава, от другите модификато­ри, които не са за достъп, и могат да се използват при декларирането на полета на класа, ще обърнем внимание още на static, const и readonly.

Област на действие (scope)

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

Инициализация по време на деклариране

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

[<modifiers>] <field_type> <field_name> = <initial_value>;

Разбира се, трябва <initial_value> да бъде от типа на полето или някой съвместим с него тип. Например:

class SampleClass

{

      int age = 5;

      long distance = 234; // The literal 234 is of integer type

 

      string[] names = new string[] { "Pencho", "Marincho" };

      Dog myDog = new Dog();

 

      // ... Other code ...

}

Стойности по подразбиране на полетата

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

clip_image007[1]

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

В някои езици (като C и C++) новозаделените обекти не се инициализи­рат автоматично с нулеви стойности и това създава условия за допускане на трудни за откриване грешки. Появява се синдромът "ама това вчера работеше" – непредвидимо поведение, при което програмата понякога работи коректно (когато заделената памет съдържа по случай­ност благоприятни стойности), а понякога не работи (когато заделената памет съдържа неблагоприятни стойности. В C# и въобще в .NET платформата този проблем е решен чрез автоматичното зануляване на полетата.

Стойността по подразбиране за всички типове е 0 или неин еквивалент. За най-често използваните типове подразбиращите се стойности са както следва:

Тип на поле

Стойност по подразбиране

bool

false

byte

0

char

'\0'

decimal

0.0M

double

0.0D

float

0.0F

int

0

референция към обект

null

За по-изчерпателна информация може да погледнете темата "Примитивни типове и променливи", секция "Типове данни", подсекция "Видове", където има пълен списък с всички примитивни типове данни в C# и подразбиращите се стойности за всеки един от тях.

Например, ако създадем клас Dog и за него дефинираме полета име (name), възраст (age), дължина (length) и дали кучето е от мъжки пол (isMale), без да ги инициализираме по време на декларацията им, те ще бъдат автоматично занулени при създаването на обект от този клас:

public class Dog

{

      string name;

      int age;

      int length;

      bool isMale;

 

      static void Main()

      {

            Dog dog = new Dog();

 

            Console.WriteLine("Dog's name is: " + dog.name);

            Console.WriteLine("Dog's age is: " + dog.age);

            Console.WriteLine("Dog's length is: " + dog.length);

            Console.WriteLine("Dog is male: " + dog.isMale);

      }

}

Съответно при стартиране на примера като резултат ще получим:

Dog's name is:

Dog's age is: 0

Dog's length is: 0

Dog is male: False

Автоматична инициализация на локални променливи и полета

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

clip_image007[2]

За разлика от полетата, локалните променливи, не биват инициализирани с подразбираща се стойност при тяхното деклариране.

Нека разгледаме един пример:

static void Main()

{

      int notInitializedLocalVariable;

      Console.WriteLine(notInitializedLocalVariable);

}

Ако се опитаме да компилираме горния код, ще получим следното съобщение за грешка:

Use of unassigned local variable 'notInitializedLocalVariable'

Собствени стойности по подразбиране

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

Пример за такова инициализиране може да дадем като модифицираме класът SampleClass от предходната секция "Ини­циа­ли­зация по време на деклари­ране":

class SampleClass

{

      int age = 0;

      long distance = 0;

      string[] names = null;

      Dog myDog = null;

 

      // ... Other code ...

}

Модификатори const и readonly

Както споменахме в началото на тази секция, в декларацията на едно поле е позволено да се използват модификаторите const и readonly. Те не са модифи­катори за достъп, а се използват за еднократно инициали­зиране на полета. Полета, декларирани като const или readonly се наричат константи. Използват се когато дадена стойност се повтаря на няколко места в програмата. В такива стойността се изнася като константа и се дефинира само веднъж. Пример за константи от .NET Framework са математическите константи Math.PI и Math.E, както и константите String.Empty и Int32.MaxValue.

Константи, декларирани с const

Полетата, имащи модификатор const в декларацията си, трябва да бъдат инициали­зирани при декларацията си и след това стойността им не може да се променя. Те могат да бъдат достъпвани без да има инстанция на класа, тъй като са споделени между всичко обекти на класа. Нещо повече, при компилация на всички места в кода, където се реферират const полета, те се заместват със стойността им, сякаш тя е зададена директно, а не чрез константа. По тази причина const полетата се наричат още compile-time константи, защото се заместват със стойността им по време на компилация.

Константи, декларирани с readonly

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

Нека онагледим казаното с пример:

public class ConstReadonlyModifiersTest

{

      public const double PI = 3.1415926535897932385;

      public readonly double size;

 

      public ConstReadonlyModifiersTest(int size)

      {

            this.size = size; // Cannot be further modified!

      }

 

      static void Main()

      {

            Console.WriteLine(PI);

            Console.WriteLine(ConstReadonlyModifiersTest.PI);

            ConstReadonlyModifiersTest t =

                  new ConstReadonlyModifiersTest(5);

            Console.WriteLine(t.size);

 

            // This will cause compile-time error

            Console.WriteLine(ConstReadonlyModifiersTest.size);

      }

}

Методи

В главата "Методи" подробно се запознахме с това как да декларираме и използваме метод. В тази сек­ция накратко ще припомним казаното там и ще се фокусираме върху някои допълнителни особености при декларирането и създаването на методи.

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

Декларирането на методи, както знаем, става по следния начин:

// Method definition

[<modifiers>] [<return_type>] <method_name>([<parameters_list>])

{

      // ... Method’s body ...

      [<return_statement>];

}

Задължителните елементи при декларирането на метода са типът на връ­ща­ната стойност <return_type>, името на метода <method_name> и отваря­щата и затварящата кръгли скоби – "(" и ")".

Списъкът от параметри <params_list> не е задължителен. Използваме го да подаваме някакви данни на метода, който декларираме, ако той има нужда.

Знаем, че ако типът на връщаната стойност <return_type> е void, тогава <return_statement> може да участва само с оператора return без аргумент, с цел пре­кра­тя­ване действието на метода. Ако <return_type> е различен от void, методът задължително трябва да връща резултат чрез ключовата ду­ма return с аргумент, който е от тип <return_type> или съвместим с не­го.

Работата, която методът трябва да свърши, се намира в тялото му, заградена от фигурни скоби – "{" и "}".

Макар че разгледахме някои от модификаторите за достъп, позволени да се използват при декларирането на един метод, в секцията "Видимост на полета и методи" ще разгледаме по-подробно тази тема.

Ще разгледаме модификатора static в секцията "Статични класове (Static classes) и статични членове на класа (static members) на тази глава.

Пример – деклариране на метод

Нека погледнем декларирането на един метод за намиране сбор на две цели числа:

int Add(int number1, int number2)

{

      int result = number1 + number2;

      return result;

}

Името, с което сме го декларирали, е Add, а типът на връщаната му стойност е int. Списъкът му от параметри се състои от два елемента – променливите number1 и number2. Съответно, връщаме стойността на сбо­ра от двете числа като резултат.

Достъп до нестатичните данни на класа

В главата "Създаване и използване на обекти", разгледахме как чрез опера­то­ра точка, можем да достъпим полетата и да извикаме методите на един клас. Нека припомним как можем да достъпваме полета и да извикваме методи на даден клас, които не са статични, т.е. нямат модификатор static, в деклара­цията си.

Например, нека имаме клас Dog, с поле за възраст – age. За да отпечатаме стойността на това поле, е нужно да създадем обект от клас Dog и да достъпим полето на този обект чрез точкова нотация:

public class Dog

{

      int age = 2;

 

      public static void Main()

      {

            Dog dog = new Dog();

            Console.WriteLine("Dog's age is: " + dog.age);

      }

}

Съответно резултатът ще бъде:

Dog's age is: 2

Достъп до нестатичните полетата на класа от нестатичен метод

Достъпът до стойността на едно поле може да се осъществява не директно чрез оператора точка (както бе в последния пример dog.age), а чрез метод или свойство. Нека в класа Dog си създадем метод, който връща стойността на полето age:

public int GetAge()

{

      return this.age;

}

Както виждаме, за да достъпим стойността на полето за възрастта, вътре, от самия клас, използваме ключовата дума this. Знаем, че ключовата дума this е референция към текущия обект, към който се извиква метода. Следователно, в нашия пример, с "return this.age", ние казваме "от те­кущия обект (this) вземи (използването на оператора точка) стой­ността на полето age и го върни като резултат от метода (чрез ключовата дума return)". Тогава, вместо в метода Main() да достъпваме стойността на полето age на обекта dog, ние просто ще извикаме метода GetAge():

static void Main()

{

      Dog dog = new Dog();

      Console.WriteLine("Dog's age is: " + dog.GetAge());

}

Резултатът след тази промяна ще бъде отново същият.

Формално, декларацията за достъп до поле в рамките на класа, е след­ната:

this.<field_name>

Нека подчертаем, че този достъп е възможен само от нестатичен код, т.е. метод или блок, който няма модификатор static.

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

Например, нека декларираме метод MakeOlder(), който извикваме всяка го­ди­на на датата на рож­дения ден на нашия домашен люби­мец и който, увеличава възрастта му с една година:

public void MakeOlder()

{

      this.age++;

}

За да проверим дали това, което написахме работи коректно, в края на метода Main() добавяме следните два реда:

// One year later, on the birthday date...

dog.MakeOlder();

Console.WriteLine("After one year dog's age is: " + dog.age);

След изпълнението, резултатът е следният:

Dog's age is: 2

After one year dog's age is: 3

Извикване нестатичните методи на класа от нестатичен метод

По подобие на полетата, които нямат static в декларацията си, методите, които също не са статични, могат да бъдат извиквани в тялото на класа чрез ключовата дума this. Това става, след като към нея, чрез точкова нотация добавим метода, който ни е необходим заедно с аргументите му (ако има параметри):

this.<method_name>()

Например, нека създадем метод PrintAge(), който отпечатва възрастта на обекта от тип Dog, като за целта извиква метода GetAge():

public void PrintAge()

{

      int myAge = this.GetAge();

      Console.WriteLine("My age is: " + myAge);

}

На първия ред от примера указваме, че искаме да получим възрастта (стойността на поле­то age) на текущия обект, използвайки метода GetAge() на текущия обект. Това става чрез ключовата дума this.

clip_image007[3]

Достъпването на нестатичните елементи на класа (полета и методи) се осъществява чрез ключовата дума this и оператора за достъп – точка.

Достъп до нестатични данни на класа без използване на this

Когато достъпваме полетата на класа или извикваме нестатичните му ме­то­ди, е възможно, да го направим без ключовата дума this. Тогава двата метода, които декларирахме могат да бъдат записани по следния начин:

public int GetAge()

{

      return age; // The same like this.age

}

 

public void MakeOlder()

{

      age++; // The same like this.age++

}

Ключовата дума this се използва, за да укаже изрично, че трябва да се осъществи достъп до нестатично поле на даден клас или извикваме негов нестатичен метод. Когато това изрично уточнение не е необходимо, може да бъде пропускана и директно да се достъпва елемента на класа.

clip_image007[4]

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

Въпреки, че се подразбира, ключовата дума this често се използва при достъп до полетата на класа, защото прави кода по-лесен за четене и разбиране, като изрично уточнява, че трябва да се направи достъп до член на класа, а не до локална променлива.

Припокриване на полета с локални променливи

От секцията "Деклариране на полета в даден клас" по-горе, знаем, че областта на действие на едно поле е от реда, на който е декларирано полето, до затварящата скоба на тялото на класа. Например:

public class OverlappingScopeTest

{

      int myValue = 3;

 

      void PrintMyValue()

      {

            Console.WriteLine("My value is: " + myValue);

      }

 

      static void Main()

      {

            OverlappingScopeTest instance = new OverlappingScopeTest();

            instance.PrintMyValue();

      }

}

Този код ще изведе в конзолата като резултат:

My value is: 3

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

int CalculateNewValue(int newValue)

{

      int result = myValue + newValue;

      return result;

}

В този случай, локалната променлива, която използваме, за да изчислим новата стойност, е result.

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

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

void PrintMyValue()

{

      int myValue = 5;

      Console.WriteLine("My value is: " + myValue);

}

Ако декларираме така метода, дали той ще се компилира? А ако се компилира, дали ще се изпълни? Ако се изпълни коя стойност ще бъде отпечатана – тази на полето или тази на локалната променлива?

Така деклариран, след като бъде изпълнен методът Main(), резултатът, който ще бъде отпечатан, ще бъде:

My value is: 5

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

Точно затова областта на действие на локалната променлива myValue със стойност 5 препокри областта на действие на полето със същото име. Тогава, при отпечатването на стойността, бе използвана стойността на локалната променлива.

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

Нека разгледаме отново нашия пример с извеждането на стойността на полето myValue:

void PrintMyValue()

{

      int myValue = 5;

      Console.WriteLine("My value is: " + this.myValue);

}

Този път, резултатът от извикването на метода е:

My value is: 3

Видимост на полета и методи

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

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

За разлика от декларацията на клас, при декларирането на полета и методи на класа, могат да бъдат използвани и четирите нива на достъп – public, protected, internal и private. Нивото на видимост protected ня­ма да бъде разглеждано в тази глава, тъй като е обвързано с наследяването на класове и е обяснено подробно в главата "Принципи на обектно-ориентираното програмира­не".

Преди да продължим, нека припомним, че ако един клас A, не е видим (ня­ма достъп) от друг клас B, тогава нито един елемент (поле или метод) на класа A, не може да бъде достъ­пен от класа B.

clip_image007[5]

Ако два класа не са видими един за друг, то елементите им (полета и методи) не са видими също, независимо с какви нива на достъп са декларирани самите те.

В следващите подсекции, към обясненията, ще разглеждаме примери, в които имаме два класа (Dog и Kid), които са видими един за друг, т.е. все­ки един от класовете може да създава обекти от тип – другия клас и да до­стъп­ва еле­мен­тите му в зависимост от нивото на достъп, с което са декларирани. Ето как изглежда първия клас Dog:

public class Dog

{

      private string name = "Sharo";

 

      public string Name

      {

            get { return this.name; }

      }

 

      public void Bark()

      {

            Console.WriteLine("wow-wow");

      }

 

      public void DoSth()

      {

            this.Bark();

      }

}

В освен полета и методи се използва и свойство Name, което просто връща полето name. Ще разгледаме свойствата след малко, така че за момента се фокусирайте върху останалото.

Кодът на класа Kid има следния вид:

public class Kid

{

      public void CallTheDog(Dog dog)

      {

            Console.WriteLine("Come, " + dog.Name);

      }

 

      public void WagTheDog(Dog dog)

      {

            dog.Bark();

      }

}

В момента, всички елементи (полета и методи) на двата класа са деклари­рани с модификатор за достъп public, но при обяснението на различните нива на достъп, ще го променяме съответно. Това, което ще ни интере­сува, е как промяната в нивото на достъп на елемен­тите (полета и методи) на класа Dog и ще рефлектира върху достъпа до тези елементи, когато този достъп се извършва от:

-     Самото тяло на класа Dog.

-     Тялото на класа Kid, съответно вземайки предвид дали Kid е в пространството от имена (или асембли), в което се намира класа Dog или не.

Ниво на достъп public

Когато метод или променлива на класа са декларирани с модификатор за достъп public, те могат да бъдат достъпвани от други класове, независи­мо дали другите класове са декларирани в същото пространство от имена, в същото асембли или извън него.

Нека разгледаме двата типа достъп до член на класа, които се срещат в нашите класове Dog и Kid:

clip_image009

Достъп до член на класа осъществен в самата деклара­ция на класа.

clip_image011

Достъп до член на класа осъществен, чрез референция към обект, създаден в тялото на друг клас

Когато членовете на двата класа са public, се получава следното:

Dog.cs

 

 

 

 

 

clip_image009[1]

 

 

 

 

 

 

 

clip_image009[2]

class Dog

{

          public string name = "Sharo";

 

          public string Name

          {

                   get { return this.name; }

          }

 

          public void Bark()

          {

                   Console.WriteLine("wow-wow");

          }

 

          public void DoSth()

          {

                   this.Bark();

          }

}

 

Kid.cs

 

 

 

clip_image011[1]

 

 

clip_image011[2]

class Kid

{

          public void CallTheDog(Dog dog)

          {

                   Console.WriteLine("Come, " + dog.name);

          }

 

          public void WagTheDog(Dog dog)

          {

                   dog.Bark();

          }

}

Както виждаме, без проблем осъществяваме, достъп до полето name и до метода Bark() в класа Dog от тялото на самия клас. Независи­мо дали класът Kid е в пространството от имена на класа Dog, можем от тялото му, да до­стъ­пим полето name и съответно да извикаме метода Bark() чрез операто­ра точка, прило­жен към референцията dog към обект от тип Dog.

Ниво на достъп internal

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

Dog.cs

 

 

 

 

 

clip_image009[3]

 

 

 

 

 

 

 

clip_image009[4]

class Dog

{

          internal string name = "Sharo";

 

          public string Name

          {

                   get { return this.name; }

          }

 

          internal void Bark()

          {

                   Console.WriteLine("wow-wow");

          }

 

          public void DoSth()

          {

                   this.Bark();

          }

}

Съответно, за класа Kid, разглеждаме двата случая:

-     Когато е в същото асембли, достъпът до елементите на класа Dog, ще бъде позволен, независимо дали двата класа са в едно и също пространство от имена или в различни:

Kid.cs

 

 

 

clip_image011[3]

 

 

clip_image011[4]

class Kid

{

          public void CallTheDog(Dog dog)

          {

                   Console.WriteLine("Come, " + dog.name);

          }

 

          public void WagTheDog(Dog dog)

          {

                   dog.Bark();

          }

}

-     Когато класът Kid е външен за асемблито, в което е деклариран класът Dog, тогава достъпът до полето name и метода Bark() ще е невъзмо­жен:

Kid.cs

 

 

 

clip_image013

 

 

clip_image013[1]

class Kid

{

clip_image014          public void CallTheDog(Dog dog)

          {

                   Console.WriteLine("Come, " + dog.name);

          }

 

clip_image015          public void WagTheDog(Dog dog)

          {

                   dog.Bark();

          }

}

Всъщност достъпът до internal членовете на класа Dog е невъзможен по две причини: недостатъчна видимост на класа и недостатъчна видимост на членовете му. За да се позволи достъп от друго асембли до класа Dog, той, е необходимо той да е деклариран като public и едновременно с това въпросните му членове да са декларирани като public. Ако или класът или членовете му имат по-ниска видимост, достъпът до тях е невъзможен от други асемблита (други Visual Studio проекти).

Ако се опитаме да компилираме класа Kid, когато е външен за асемблито, в което се намира класа Dog, ще получим грешки при компилация.

Ниво на достъп private

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

Следователно, ако декларираме полето name и метода Bark() на класа Dog, с модификатори private, няма проблем да ги достъпваме вътрешно от самия клас Dog, но достъп от други класове не е позволен, дори ако са от същото асембли:

Dog.cs

 

 

 

 

 

clip_image009[5]

 

 

 

 

 

 

 

clip_image009[6]

class Dog

{

          private string name = "Sharo";

 

          public string Name

          {

                   get { return this.name; }

          }

 

          private void Bark()

          {

                   Console.WriteLine("wow-wow");

          }

 

          public void DoSth()

          {

                   this.Bark();

          }

}

 

Kid.cs

 

 

 

clip_image013[2]

 

 

clip_image013[3]

class Kid

{

clip_image014[1]          public void CallTheDog(Dog dog)

          {

                   Console.WriteLine("Come, " + dog.name);

          }

 

clip_image015[1]          public void WagTheDog(Dog dog)

          {

                   dog.Bark();

          }

}

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

Как се определя нивото на достъп на елементите на класа?

Преди да приключим със секцията за видимостта на елементите на един клас, нека направим един експеримент. Нека в класа Dog полето name и метода Bark() са декларирани с модификатор за достъп private. Нека съ­що така, декларираме метод Main(), със следното съдържание:

public class Dog

{

      private string name = "Sharo";

 

      // ...

 

      private void Bark()

      {

            Console.WriteLine("wow-wow");

      }

 

      // ...

 

      public static void Main()

      {

            Dog myDog = new Dog();

            Console.WriteLine("My dog's name is " + myDog.name);

            myDog.Bark();

      }

}

Въпросът, който стои пред нас е, ще се компилира ли класът Dog, при положение, че сме декларирали елементите на класа с модификатор за достъп private, а в същото време ги извикваме с точкова нотация, приложена към променливата myDog, в метода Main()?

Стартираме компилацията и тя минава успешно. Съответно, резултатът от изпълнението на метода Main(), който деклари­рах­ме в класа Dog ще бъде следният:

My dog’s name is Sharo

Wow-wow

Всичко се компилира и работи, тъй като мо­ди­фи­ка­торите за достъп до елементите на класа се прилагат на ниво клас, а не на ниво обекти. Тъй като променливата myDog е дефинирана в тялото на класа Dog (където е разположен и Main() метода на програ­мата), можем да достъпваме елементите му (полета и мето­ди) чрез точкова нотация, независимо че са декларирани с ниво на дос­тъп private. Ако обаче се опитаме да направим същото от тялото на класа Kid, това няма да е възможно, тъй като достъпът до private полетата  от външен клас не е разрешено.

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

В обектно-ориентираното програмиране, когато създаваме обект от даден клас, е необходимо да извикаме елемент от класа, наречен конструктор.

Какво е конструктор?

Конструктор на даден клас, наричаме псевдометод, който няма тип на връщана стойност, носи името на класа и който се извиква чрез ключо­вата дума new. Задачата на конструктора е да инициализира заделената за обекта памет, в която ще се съхраня­ват неговите полетата (тези, които не са static).

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

Единственият начин да извикаме един конструктор в C# е чрез ключовата дума new. Тя заделя памет за новия обект (в стека или в хийпа според това дали обектът е стойностен или референтен тип), занулява полетата му, извиква конструктора му (или веригата конструктори, образувана при наследяване) и накрая връща референция към новозаделения обект.

Нека разгледаме един пример, от който ще стане ясно как работи кон­структорът. От главата "Създаване и използване на обекти", знаем как се създава обект:

Dog myDog = new Dog();

В случая, чрез ключовата дума new, извикваме конструктора на класа Dog, при което се заделя паметта необходима за новосъздадения обект от тип Dog. Когато става дума за класове, те се заделят в динамичната памет (хийпа). Нека проследим как протича този процес стъпка по стъпка. Първо се заделя памет за обекта:

clip_image017

След това се инициализират полетата му (ако има такива) с подразбира­щи­те се стойнос­ти за съответните им типове:

clip_image019

Ако създаването на новия обект е завършило успешно, конструкторът връща референция към него, която се присвоява на променливата myDog, от тип класа Dog:

clip_image021

Деклариране на конструктор

Ако имаме класа Dog, ето как би изглеждал неговия най-опростен кон­струк­тор:

public Dog()

{

}

Формално, декларацията на конструктора изглежда по следния начин:

[<modifiers>] <class_name>([<parameters_list>])

Както вече знаем, конструкторите приличат на методи, но нямат тип на връщана стойност (затова ги нарекохме псевдометоди).

Име на конструктора

В C# задължително името на всеки конструктор съвпада с името на кла­са, в който го декларираме – <class_name>. В примера по-горе, името на конструктора е същото, каквото е името на класа – Dog. Трябва да знаем, че както при методите, името на конструктора винаги е следвано от кръгли скоби – "(" и ")".

В C# не е позволено, да се декларира метод, който притежава име, което съвпада с името на класа (следователно и с името на конструк­торите). Ако въпреки всичко бъде деклариран метод с името на класа, това ще доведе до грешка при компилация.

public class IllegalMethodExample

{

      // Legal constructor

      public IllegalMethodExample ()

      {

      }

 

      // Illegal method

      private string IllegalMethodExample()

      {

            return "I am illegal method!";

      }

}

При опит за компилация на този клас, компилаторът ще изведе следното съобщение за грешка:

SampleClass: member names cannot be the same as their enclosing type

Списък с параметри

По подобие на методите, ако за създаването на обекта са необходими допълнителни данни, конструкторът ги получава чрез списък от пара­метри – <parameters_list>. В примерния конструктор на класа Dog няма нужда от допълнителни данни за създаване на обект от такъв тип и затова няма деклариран списък от параметри. Повече за списъка от параметри ще разгледаме в една от следващите секции – "Деклариране на конструк­тор с параметри".

Разбира се след декларацията на конструктора, следва неговото тяло, което е като тялото на всеки един метод в C#, но по принцип съдържа предимно инициализационна логика, т.е. задава начални стойности на полетата на класа.

Модификатори

Забелязваме, че в декларацията на конструктора, може да се добавят модификатори – <modifiers>. За модификаторите, които познаваме и които не са модификатори за достъп, т.е. const и static, трябва да знаем, че само const не е позволен за употреба при декларирането на конструк­тори. По-късно в тази глава, в секцията "Статични конструктори" ще научим повече подробности за конструктори декларирани с модификатор static.

Видимост на конструкторите

По подобие на полетата и методите на класа, конструкторите, могат да бъдат декларирани с нива на видимост public, protected, internal, protected internal и private. Нивата на достъп protected и protected internal ще бъдат обяснени в главата "Прин­ципи на обектно-ориентираното програмира­не". Остана­лите нива на достъп имат същото значение и поведение като при полетата и методите.

Инициализация на полета в конструктора

Както обяснихме по-рано, при създаването на нов обект и извикването на конструктор, се заделя памет за нестатичните полетата на обекта от дадения клас и те се инициализират със стойностите по подразбиране за техния тип (вж. секция "Извикване на конструктор").

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

Например, в примерите, които разглеждахме до момента, винаги полето name на обекта от тип Dog, го инициализирахме по време на неговата декларация:

string name = "Sharo";

Вместо да правим това по време на декларацията на полето, по-добър стил на програмиране е да му дадем стойност в конструктора:

public class Dog

{

      private string name;

 

      public Dog()

      {

            this.name = "Sharo";

      }

 

      // ... The rest of the class body ...

}

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

public class Dog

{

      private string name = null;

 

      public Dog()

      {

            this.name = "Sharo";

      }

 

      // ... The rest of the class body ...

}

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

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

Ако полетата са от примитивен тип, тогава след подразбиращите се стойности, ще бъдат присвоени новите, които ние подаваме.

В случая, когато полетата са от референтен тип, например нашето поле name, конструкторът ще ги инициализира с null. След това ще създаде обекта от съответния тип, в случая низа "Sharo" и накрая ще се присвои референция към новия обект в съответното поле, при нас – полето name.

Същото ще се получи, ако имаме и други полета, които не са примитивни типове и ги инициализираме в конструктора. Например, нека имаме клас, който описва каишка – Collar:

public class Collar

{

      private int size;

 

      public Collar()

      {

      }

}

Нека съответно нашият клас Dog, има поле collar, което е от тип Collar и което инициализираме в конструктора на класа:

public class Dog

{

      private string name;

      private int age;

      private double length;

      private Collar collar;

 

      public Dog()

      {

            this.name = "Sharo";

            this.age = 3;

            this.length = 0.5;

            this.collar = new Collar();

      }

 

      public static void Main()

      {

            Dog myDog = new Dog();

      }

}

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

clip_image023

След това, конструкторът ще трябва да се погрижи за създаването на обекта за полето name (т.е. ще извика конструктора на класа string, който ще свърши работата по създаването на низа):

clip_image025

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

clip_image027

След това идва ред на създаването на обекта от тип Collar. Нашият конструктор (на класа Dog), извиква конструктора на класа Collar, който заделя памет за новия обект:

clip_image029

След това я инициализира с подразбиращата се стойност за съответния тип:

clip_image031

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

clip_image033

Накрая, референцията към новия обект от тип Dog се присвоява на локалната променлива myDog в метода Main():

 

clip_image035

Помним, че локалните променливи винаги се съхраняват в областта от оперативната памет, наречена стек, а обектите – в частта, наречена хийп.

Последователност на инициализиране на полетата на класа

За да няма обърквания, нека разясним последователността, в която се инициализират полетата на един клас, независимо от това дали сме им дали стойност по време на декларация и/или сме ги инициализирали в конструктора.

Първо се заделя памет за съответното поле в хийпа и тази памет се ини­циализира със стойността по подразбиране на типа на полето. Напри­мер, нека разгледаме отново нашия клас Dog:

public class Dog

{

      private string name;

 

      public Dog()

      {

            Console.WriteLine(

                  "this.name has value of: \"" + this.name + "\"");

            // ... No other code here ...

      }

      // ... Rest of the class body ...

}

При опит да създадем нов обект от тип нашия клас, в конзолата ще бъде отпечатано съответно:

this.name has value of: ""

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

Така, ако променим реда от класа Dog, на който декларираме полето name, то първоначално ще бъде инициализирано със стойност null и след това ще му бъде присвоена стойността "Rex".

private string name = "Rex";

Съответно, при всяко създаване на обект от нашия клас:

public static void Main()

{

      Dog dog = new Dog();

}

Ще бъде извеждано:

this.name has value of: "Rex"

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

Деклариране на конструктор с параметри

В предната секция, видяхме как можем да дадем стойности на полетата, различни от стойностите по подразбиране. Много често, обаче, по време на декларирането на конструктора не знаем какви стойности ще приемат различните полета. За да се справим с този проблем, по подобие на методите с параметри, нужната информация, която трябва за работата на конструктора, му се подава чрез списъка с параметри. Например:

public Dog(string dogName, int dogAge, double dogLength)

{

      name = dogName;

      age = dogAge;

      length = dogLength;

      collar = new Collar();

}

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

public static void Main()

{

      Dog myDog = new Dog("Bobi", 2, 0.4); // Passing parameters

 

      Console.WriteLine("My dog " + myDog.name +

            " is " + myDog.age + " year(s) old. " +

                  " and it has length: " + myDog.length + " m.");

}

Резултатът от изпълнението на този Main() метод е следния:

My dog Bobi is 2 year(s) old. It has length: 0.4 m.

В C# нямаме ограничение за броя на конструкторите, които можем да създадем. Единственото условие е те да се различават по сигнатурата си (какво е сигнатура обяснихме в главата "Методи").

Област на действие на параметрите на конструктора

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

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

public Dog(string name, int age, double length)

{

      name = name;

      age = age;

      length = length;

      collar = new Collar();

}

Нека компилираме и изпълним съответно Main() метода, който също из­пол­звахме в предходната секция. Ето какъв е резултатът от изпълне­ние­то му:

My dog  is 0 year(s) old. It has length: 0 m

Странен резултат, нали? Всъщност се оказва, че не е толкова странен. Обяснението е следното – областта, в която действат про­мен­ливите от списъка с параметри на конструктора, припокрива областта на действие на полетата, които имат същите имена в конструктора. По този на­чин не даваме никаква стойност на полетата, тъй като на практика ние не ги достъпваме. Например, вместо на полето age, ние присвояваме стойността на променливата age на самата нея:

age = age;

Както видяхме в секцията "Припокриване на полета с локални промен­ливи", за да избегнем това разминаване, трябва да достъ­пим полето, на което искаме да присвоим стойност, но чието име съвпада с името на променлива от списъка с параметри, използвайки ключовата дума this:

public Dog(string name, int age, double length)

{

      this.name = name;

      this.age = age;

      this.length = length;

      this.collar = new Collar();

}

Сега, ако изпълним отново Main() метода:

public static void Main()

{

      Dog myDog = new Dog("Bobi", 2, 0.4);

 

      Console.WriteLine("My dog " + myDog.name +

            " is " + myDog.age + " year(s) old. " +

                  " and it has length: " + myDog.length + " m");

}

Резултатът ще бъде точно какъвто очакваме да бъде:

My dog Bobi is 2 year(s) old. It has length: 0.4 m

Конструктор с променлив брой аргументи

Подобно на методите с променлив брой аргументи, които разгледахме в главата "Методи", конструкторите също могат да бъдат декла­ри­ра­ни с параметър за променлив брой аргументи. Правилата за деклара­ция и извикване на конструктори с променлив брой аргументи са същите като тези, които описахме за декларацията и извикването при методи:

-     Когато декларираме конструктор с променлив брой параметри, тряб­ва да използваме запазената дума params, след което поставяме типа на параметрите, следван от квадратни скоби. Накрая, следва името на масива, в който ще се съхраняват подадените при извикване на метода аргументи. Например за целочислени аргу­менти ползваме params int[] numbers.

-     Позволено е, конструкторът с променлив брой параметри да има и други параметри в списъка си от параметри.

-     Параметърът за променлив брой аргументи трябва да е последен в списъка от параметри на конструктора.

Нека разгледаме примерна декларация на конструктор на клас, който описва лекция:

public Lecture(string subject, params string[] studentsNames)

{

      // ... Initialization of the instance variables ...

}

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

Lecture lecture =

          new Lecture("Biology", "Pencho", "Mincho", "Stancho");

Съответно, като първи параметър сме подали името на предмета – "Biology", а за всички оставащи аргументи – имената на присъстващите студенти.

Варианти на конструкторите (overloading)

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

Нека вземем за пример класа Dog. Можем да декларираме различни кон­струк­тори:

// No parameters

public Dog()

{

      this.name = "Sharo";

      this.age = 1;

      this.length = 0.3;

      this.collar = new Collar();

}

 

// One parameter

public Dog(string name)

{

      this.name = name;

      this.age = 1;

      this.length = 0.3;

      this.collar = new Collar();

}

 

// Two parameters

public Dog(string name, int age)

{

      this.name = name;

      this.age = age;

      this.length = 0.3;

      this.collar = new Collar();

}

 

// Three parameters

public Dog(string name, int age, double length)

{

      this.name = name;

      this.age = age;

      this.length = length;

      this.collar = new Collar();

}

 

// Four parameters

public Dog(string name, int age, double length, Collar collar)

{

      this.name = name;

      this.age = age;

      this.length = length;

      this.collar = collar;

}

Преизползване на конструкторите

В последния пример, който дадохме, видяхме, че в зависимост от нуждите за създаване на обекти от нашия клас може да декларираме различни варианти на конструктори. Лесно се забелязва, че голяма част от кода на тези конструктори се повтаря. Това ни кара да се замислим, дали няма начин един конструктор, който вече извършва дадена инициализа­ция, да бъде преизползван от другите, които трябва да изпълнят същата инициа­лизация. От друга страна, в началото на главата споменахме, че един конструктор не може да бъде извикан по начина, по който се извикват методите, а само чрез ключовата дума new. Би трябвало да има някакъв начин, иначе много код ще се повтаря излишно.

В C#, съществува механизъм, чрез който един конструктор може да извиква друг конструктор деклариран в същия клас. Това става отново с ключовата дума this, но използвана в друга синтактична конструкция при декларацията на конструкторите:

[<modifiers>] <class_name>([<parameters_list_1>])

      : this([<parameters_list_2>])

Към познатата ни форма за деклариране на конструктор (първия ред от декларацията показана по-горе), можем да добавим двоеточие, следвано от ключовата дума this, следвана от скоби. Ако конструкторът, който искаме да извикаме е с параметри, в скобите трябва да добавим списък от параметри parameters_list_2, които да му подадем.

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

// Nо parameters

public Dog()

      : this("Sharo") // Constructor call

{

      // More code could be added here

}

 

// One parameter

public Dog(string name)

      : this(name, 1) // Constructor call

{

}

 

// Two parameters

public Dog(string name, int age)

      : this(name, age, 0.3) // Constructor call

{

}

 

// Three parameters

public Dog(string name, int age, double length)

      : this(name, age, length, new Collar()) // Constructor call

{

}

 

// Four parameters

public Dog(string name, int age, double length, Collar collar)

{

      this.name = name;

      this.age = age;

      this.length = length;

      this.collar = collar;

}

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

Конструктор по подразбиране

Нека разгледаме следния въпрос – какво става, ако не декларираме кон­струк­тор в нашия клас? Как ще създа­дем обекти от този тип?

Тъй като често се случва даден клас да няма нито един конструктор, този въпрос е решен в езика C#. Когато не декларираме нито един конструк­тор, компила­то­рът ще създаде един за нас и той ще се използва при създаването на обекти от типа на нашия клас. Този конструктор се нарича конструктор по подразби­ране (default implicit constructor), който няма да има параметри и ще бъде празен (т.е. няма да прави нищо в допълнение към подразбиращото се зануляване на полетата на обекта).

clip_image007[6]

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

Например, нека декларираме класа Collar, без да декларираме никакъв кон­струк­тор в него:

public class Collar

{

      private int size;

 

      public int Size

      {

            get { return size; }

      }

}

Въпреки, че нямаме изрично деклариран конструктор без параметри, ще можем да създадем обекти от този клас по следния начин:

Collar collar = new Collar();

Конструкторът по подразбиране изглежда по следния начин:

<access_level> <class_name>() { }

Трябва да знаем, че конструкторът по подразбиране винаги носи името на класа <class_name> и винаги списъкът му с параметри е празен и неговото тяло е празно. Той просто се "подпъхва" от компилатора, ако в класа няма нито един конструктор. Подразбиращият се конструктор обикновено е public (с изключение на някои много специфични ситуации, при които е protected).

clip_image007[7]

Конструкторът по подразбиране е винаги без парамет­ри.

За да се уверим, че конструкторът по подразбиране винаги е без пара­мет­ри, нека направим опит да извикаме подразбиращия се конструк­тор, като му подадем параметри:

Collar collar = new Collar(5);

Компилаторът ще изведе следното съобщение за грешка:

'Collar' does not contain a constructor that takes 1 arguments

Работа на конструктора по подразбиране

Както се досещаме, единственото, което конструкторът по подразбиране ще направи при създаването на обекти от нашия клас, е да занули полетата на класа. Например, ако в класа Collar не сме де­кларирали нито един конструктор и създадем обект от него и се опитаме да отпечатаме стойността в полето size:

public static void Main()

{

      Collar collar = new Collar();

      Console.WriteLine("Collar's size is: " + collar.Size);

}                                                

Резултатът ще бъде:

Collar's size is: 0

Виждаме, че стойността, която е запазена в полето size на обекта collar, е точно стойността по подразбиране за целочисления тип int.

Кога няма да се създаде конструктор по подразбиране?

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

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

public Collar(int size)

      : this()

{

      this.size = size;

}

Нека това е единственият конструктор на класа Collar. В него се опитва­ме да извикаме конструктор без параметри, надявайки се, че компилато­рът ще е създал конструктор по подразбиране за нас (който знаем, че е без параметри). След като се опитаме да компилираме, ще разберем, че това, което се опитваме да направим, е невъзможно:

'Collar' does not contain a constructor that takes 0 arguments

Ако сме декла­ри­рали дори един единствен конструктор в даден клас, компилаторът няма да създаде конструктор по подразбиране за нас.

clip_image007[8]

Ако декларираме поне един конструктор в даден клас, компилаторът няма да създаде конструктор по подразби­ране за нас.

Разлика между конструктор по подразбиране и конструктор без параметри

Преди да приключим със секцията за конструкторите, нека поясним нещо много важно:

clip_image007[9]

Въпреки че конструкторът по подразбиране и този, без параметри, си приличат по сигнатура, те са напълно раз­лични.

Разликата се състои в това, че конструкторът по подразбиране (default implicit constructor) се създава от компилатора, ако не декла­ри­раме нито един конструктор в нашия клас, а конструкторът без пара­метри (default constructor) го декларираме ние.

Освен това, както обяснихме по-рано, конструкторът по подразбиране винаги ще има ниво на достъп protected или public, в зависимост от модификатора на достъп на класа, докато нивото на достъп на конструк­тора без параметри изцяло зависи от нас – ние го определяме.

Свойства (Properties)

В света на обектно-ориентираното програмиране съществува елемент на класовете, наречен свойство (property), който е нещо средно между поле и метод и служи за по-добра защита на състоянието в класа. В някои езици за обектно-ориентирано програмиране, като С#, Delphi / Free Pascal, Visual Basic, JavaScript, D, Python и др., свойствата са част от езика, т.е. за тях съществува специален механизъм, чрез който се декларират и използват. Други езици, като например Java, не подържат концепцията за свойства и за целта програмистите, трябва да декларират двойка методи (за четене и модификация на свойството), за да се предостави тази функционалност.

Свойствата в С# – представяне чрез пример

Използването на свойства е доказано добра практика и важна част от концепциите на обектно-ориентираното програмиране. Създаването на свойство в програмирането става чрез деклариране на два метода – един за достъп (четене) и един за модификация (записване) на стойността на съответното свойство.

Нека разгледаме един пример. Да си представим, че имаме отново клас Dog, който описва куче. Характерно свойство за едно куче е, например, цвета му (color). Достъпът до свойството "цвят" на едно куче и съответната му модифика­ция може да осъществим по следния начин:

// Getting (reading) a property

string colorName = dogInstance.Color;

 

// Setting (modifying) a property

dogInstance.Color = "black";

Свойства – капсулация на достъпа до полетата

Основната цел на свойствата е да осигуряват капсулация на състоянието на класа, в който са декларирани, т.е. да го защитят от попадане в невалидни състояния.

Капсулация (encapsulation) наричаме скриването на физическото представяне на данните в един клас, така че, ако в последствие променим това представяне, това да не рефлектира върху останалите класове, които използват този клас.

Чрез синтаксиса на C#, това се реализира като декларираме полета (физи­чес­кото представяне на данните) с възможно най-ограничено ниво на видимост (най-често с модификатор private) и декларираме достъпът до тези полета (четене и модифициране) да може да се осъществява единствено чрез специални методи за достъп (accessor methods).

Капсулация – пример

За да онагледим какво представлява капсулацията, която предоставят свойствата на един клас, както и какво представляват самите свойства, ще разгледаме един пример.

Нека имаме клас, който представя точка от двумерното пространство със свойства координатите (x, y). Ето как би изглеждал той, ако деклари­раме всяка една от координатите, като поле:

Point.cs

using System;

 

class Point

{

      private double x;

      private double y;

 

      public Point(int x, int y)

      {

            this.x = x;

            this.y = y;

      }

 

      public double X

      {

            get { return x; }

            set { x = value; }

      }

 

      public double Y

      {

            get { return y; }

            set { y = value; }

      }

}

Полетата на обектите от нашия клас (т.е. координатите на точките) са декларирани като private и не могат да бъдат достъпвани чрез точкова нотация. Ако създадем обект от клас Point, можем да модифицираме и четем свойствата (координатите) на точката, единствено чрез свойствата за достъп до тях:

PointTest.cs

using System;

 

class PointTest

{

      static void Main()

      {

            Point myPoint = new Point(2, 3);

 

            double myPointXCoordinate = myPoint.X; // Access a property

            double myPointYCoordinate = myPoint.Y; // Access a property

 

            Console.WriteLine("The X coordinate is: " +

                  myPointXCoordinate);

            Console.WriteLine("The Y coordinate is: " +

                  myPointYCoordinate);

      }

}

Резултатът от изпълнението на този Main() метод ще бъде следният:

The X coordinate is: 2

The Y coordinate is: 3

Ако обаче решим, да променим вътрешното представяне на свойствата на точката, например вместо две полета, ги декларираме като едномерен масив с два елемента, можем да го направим, без това да повлияе по някакъв начин на останалите класове от нашия проект:

Point.cs

using System;

 

class Point

{

      private double[] coordinates;

 

      public Point(int xCoord, int yCoord)

      {

            this.coordinates = new double[2];

 

            // Initializing the x coordinate

            coordinates[0] = xCoord;

 

            // Initializing the y coordinate

            coordinates[1] = yCoord;

      }

 

      public double XCoord

      {

            get { return coordinates[0]; }

            set { coordinates[0] = value; }

      }

 

      public double YCoord

      {

            get { return coordinates[1]; }

            set { coordinates[1] = value; }

      }

}

Резултатът от изпълнението на Main() метода няма да се промени и ре­зул­та­тът ще бъде същият, без да променяме дори символ в кода на класа PointTest.

Демонстрираното е добър пример за добра капсулация на данните на един обект предоставена от механизма на свойствата. Чрез тях скриваме вътрешното представяне на информацията, като декларираме свойства / методи за достъп до него и ако в последствие настъпи промяна в представянето, това няма да се отрази на другите класове, които използват нашия клас, тъй като те ползват само свойствата му и не знаят как е представена информацията "зад кулисите".

Разбира се, разгледаният пример демонстрира само една от ползите да се опаковат (обвиват) полетата на класа в свойства. Свойствата позволяват още контрол над данните в класа и могат да проверяват дали присвоява­ните стойности свойства са коректни по някакви критерии. Например ако имаме свойство максимална скорост на клас Car, може чрез свойства да наложим изискването стойността й да е в диапазона между 1 и 300 км/ч.

Още за физическото представяне на свойствата в класа

Както видяхме по-горе, свойствата могат да имат различно представяне в един клас на физическо ниво. В нашия пример, информацията за свойствата на класа Point първоначално беше съхранена в две полета, а след това в едно поле-масив.

Ако обаче решим, вместо да пазим информацията за свойствата на точката в полета, можем да я запазим във файл или в база данни и всеки път, когато се наложи да достъпваме съответното свойство, можем да четем / пишем от файла или базата вместо както в предходните примери да използваме полета на класа. Тъй като свойствата се достъпват чрез специални методи (наречени методи за достъп и модификация или accessor methods), които ще разгле­даме след малко, за класовете, които ще използват нашия клас, това как се съхранява информацията няма да има значение (заради добрата капсула­ция!).

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

clip_image007[10]

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

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

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

Нека имаме клас Rectangle, който представя геометричната фигура пра­воъ­гъл­ник. Съответно този клас има две полета – за ширина width и дължина height. Нека нашия клас има и още едно свойство – лице (area). Тъй като винаги чрез дължината и ширината на правоъгълника можем да намерим стойността на свойството "лице", не е нужно да имаме отделно поле в класа, за да пазим тази стойност. По тази причина, можем да си декларираме просто един метод за получаване на лицето, в който пресмятаме формулата за лице на правоъгълник:

Rectangle.cs

using System;

 

class Rectangle

{

      private float height;

      private float width;

 

      public Rectangle(float height, float width)

      {

            this.height = height;

            this.width = width;

      }

 

      // Obtaining the value of the property area    

      public float Area

      {

            get { return this.height * this.width; }

      }

}

Както ще видим след малко, не е задължително едно свойство да има едновременно методи за модификация и за четене на стойността. Затова е позволено да декларираме само метод за четене на свойството Area на правоъгълника. Няма смисъл от метод, който модифицира стойността на лицето на един правоъгълник, тъй като то е винаги едно и също при определена дължина на страните.

Деклариране на свойства в C#

За да декларираме едно свойство в C#, трябва да декларираме методи за достъп (за четене и промяна) на съответното свойство и да решим по какъв начин ще съхраняваме ин­фор­ма­цията за това свойство в класа.

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

[<modifiers>] <property_type> <property_name>

С <modifiers> сме означили, както модифика­то­ри­те за достъп, така и други модификатори (например static, който ще разгледаме в следва­щата секция на главата). Те не са задължи­тел­­на част от декларацията на едно поле.

Типа на свойството <property_type> задава типа на стойностите на свойството. Може да бъде както примитивен тип (например int), така и референтен (например масив).

Съответно, <property_name> е името на свойството. То трябва да започва с главна буква и да удовлетворява правилото PascalCase, т.е. всяка нова дума, която се долепя в задната част на името на свойството, започва с главна буква. Ето няколко примера за правилно именувани свойства:

// MyValue property

public int MyValue { get; set; }

 

// Color property

public string Color { get; set; }

 

// X-coordinate property

public double X { get; set; }

Тяло на свойство

Подобно на класа и методите, свойствата в С# имат тяло, където се декларират методите за достъп до свойството (accessors).

[<modifiers>] <property_type> <property_name>

{

      // ... Property's accessors methods go here

}

Тялото на свойството започва с отваряща фигурна скоба "{" и завършва със затваряща – "}". Свойствата винаги трябва да имат тяло.

Метод за четене на стойността на свойство (getter)

Както обяснихме, декларацията на метод за четене на стойността на едно свойство (в литературата наричан още getter) се прави в тялото на свойството, като за целта трябва да се спазва следния синтаксис:

get { <accessor_body> }

Съдържанието на блока ограден от фигурните скоби (<accessor_body>) е подобно на съдържанието на произволен метод. В него се декларират действията, които трябва да се извършат за връщане на резултата от метода.

Методът за четене на стойността на едно свойство трябва да завършва с return или throw операция. Типът на стойността, която се връща като резултат от този метод, трябва да е същият както типa <property_type> описан в декларацията на свойството.

Въпреки, че по-рано в тази секция срещнахме доста примери на декла­рирани свойства с метод за четене на стойността им, нека разгледаме още един пример за свойството "възраст" (Age), което е от тип int и е декларирано чрез поле в същия клас:

private int age;                          // Field declaration

 

public string Age                         // Property declaration

{

      get { return this.age; }      // Getter declaration

}

Извикване на метод за четене на стойността на свойство

Ако допуснем, че свойството Age от последния пример е декларирано в клас от тип Dog, извикването на метода за четене на стойността на свойството, става чрез точкова нотация, приложена към променлива от типа, в чийто клас е декларирано свойството:

Dog dogInstance = new Dog();

// ...

int dogAge = dogInstance.Age;                         // Getter invocation

Console.WriteLine(dogInstance.Age);       // Getter invocation

Последните два реда от примера показват, че достъпвайки чрез точкова нотация името на свойството, автоматично се извиква неговият getter метод (методът за четене на стойността му).

Метод за промяна на стойността на свойство (setter)

По подобие на метода за четене на стойността на едно свойство, може да се декларира и метод за промяна (модификация) на стойността на едно свойство (в литературата наричан още setter). Той се декларира в тялото на свойството с тип на връщана стойност void и в него подадената при присвояването стойност е достъпна през неявен параметър value.

Декларацията се прави в тялото на свойството, като за целта трябва да се спазва следнияt синтаксис:

set { <accessor_body> }

Съдържанието на блока ограден от фигурните скоби (<accessor_body>) е подобно на съдържанието, на произволен метод. В него се декларират действията, които трябва да се извършат за промяна на стойността на свойството. Този метод използва неявен параметър, наречен value, който е предоставен от С# по подразбиране и който съдържа новата стойност на свойството. Той е от същия тип, от който е свойството.

Нека допълним примера за свойството "възраст" (Age) в класа Dog, за да онагледим казаното дотук:

private int age;                          // Field declaration

 

public string Age                         // Property declaration

{

      get{ return this.age; }      

      set{ this.age = value; }      // Setter declaration

}

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

Извикването на метода за модификация на стойността на свойството става чрез точкова нотация, приложена към променлива от типа, в чийто клас е декларирано свойството:

Dog dogInstance = new Dog();

// ...

dogInstance.Age = 3;                      // Setter invocation

На последния ред при присвояването на стойността 3 се извиква setter методът на свойството Age, с което тази стойност се записва в параметъра value и се подава на setter метода на свойството Age. Съответно в нашия пример, стойността на променливата value се присвоява на полето age oт класа Dog, но в общия случай може да се обработи по по-сложен начин.

Проверка на входните данни на метода за промяна на стойността на свойство

В процеса на програмиране е добра практика данните, които се подават на setter метода за модификация на свойство да бъдат проверявани дали са валидни и в случай че не са да се вземат необходимите "мерки". Най-често при некоректни данни се предизвиква изключение.

Да вземем отново примера с възрастта на кучето. Както знаем, тя трябва да бъде положително число. За да предотвратим възможността някой да присвои на свойството Age стойност, която е отрицателно число или нула, добавяме следната проверка в началото на setter метода:

public int Age

{

      get { return this.age;  }

      set

      {

            // Take precaution: пerform check for correctness

            if (value <= 0)

            {

                  throw new ArgumentException(

                        "Invalid argument: Age should be a negative number.");

            }

            // Assign the new correct value

            this.age = value;

      }

}

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

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

Видове свойства

В зависимост от особеностите им, можем да класифицираме свойствата по следния начин:

1.  Само за четене (read-only), т.е. тези свойства имат само get метод, както в примера с лицето на правоъгълник.

2.  Само за модифициране (write-only), т.е. тези свойства имат само set метод, но не и метод за четене на стойността на свойството.

3.  И най-честият случай е read-write, когато свойството има методи както за четене, така и за промяна на стойността.

Алтернативен похват за работа със свойства

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

В езици за програмиране като Java, в които няма концепция (и съответ­но синтактични средства) за работа със свойства, свойствата се деклари­рат чрез двойка методи, отново наречени getter и setter, по подобие на тези, които разгледахме по-горе.

Тези методи трябва да отговарят на следните изисквания:

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

[<modifiers>] <property_type> Get<property_name>

2.  Методът за модификация на стойността на свойство трябва да има тип на връщаната стойност void, името му да е образувано от името на свойството с представка Set и типа на единствения аргумент на метода да бъде идентичен с този на свойството:

[<modifiers>] void Set<property_name>(<property_type> par_name)

Ако представим свойството Age на класа Dog в примера, който използ­вахме в предходните секции чрез двойка методи, то декларацията на свойството би изглеждала по следния начин:

private int age;                                // Field declaration

 

public int GetAge()                             // Getter declaration

{

      return this.age;

}

 

public void SetAge(int age)         // Setter declaration

{

      this.age = age;

}

Съответно, четенето и модификацията на свойството Age, ще се извършва чрез извикване на декларираните методи:

Dog dogInstance = new Dog();

 

// ...

 

// Getter invocations

int dogAge = dogInstance.GetAge();

Console.WriteLine(dogInstance.GetAge());

 

// Setter invocation

dogInstance.SetAge(3);

Въпреки че представихме тази алтернатива за декларация на свойства, единствената ни цел бе да бъдем изчерпателни и да направим съпоставка с други езици като Java. Лесно се забелязва, че този начин за декларация на свойствата е по-трудно четим и по-неестествен в сравнение с първия, който изложихме. Затова е препоръчи­телно да се използват вградените средства на езика С# за декларация и използва­не на свойства.

clip_image007[11]

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

Статични класове (static classes) и статични членове на класа (static members)

Когато един елемент на класа е деклариран с модификатор static, го наричаме статичен. В С# като статични могат да бъдат декларирани полетата, методите, свойствата, конструкторите и класовете.

По-долу първо ще разгледаме статичните елементи на класа, или с други думи полетата, методите, свойствата и конструкторите му и едва тогава ще се запознаем и с концепцията за статичен клас.

За какво се използват статичните елементи?

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

Метод за сбор на две числа

Нека си представим, че имаме клас, в който един метод винаги работи по един и същ начин. Например, нека неговата задача е да получи две числа чрез списъка му от параметри и да върне като резултат сбора им. При такъв сценарий няма да има никакво значение кой обект от този клас ще изпълни този метод, тъй като той винаги ще се държи по един и същ начин – ще събира две числа, независими от извикващия обект. Реално поведението на метода не зависи от състоянието на обекта (стойностите в полетата на обекта). Тогава защо е нужно да създаваме обект, за да изпълним този метод, при положение че методът не зависи от никой от обектите от този клас? Защо просто не накараме класа да изпълни този метод?

Брояч на инстанциите от даден клас

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

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

Какво е статичен член?

Формално погледнато, статичен член (static member) на класа нари­чаме всяко поле, свойство, метод или друг член, който има модификатор static в декларацията си. Това означава, че полета, методи и свойства маркирани като статични, принад­ле­жат на самия клас, а не на някой конкретен обект от да­де­ния клас.

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

clip_image007[12]

Статичните елементи на класа могат да се използват без да се създава обект от дадения клас.

От друга страна, ако имаме създадени обекти от дадения клас, тогава статичните полета и свойства ще бъдат общи (споделени) за тях и ще има само едно копие на статичното поле или свойство, което се споделя от всички обекти от дадения клас. По тази причина в езика VB.NET вместо ключовата дума static със същото значение се ползва ключовата дума Shared.

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

Когато създаваме обекти от даден клас, всеки един от тях има различни стойности в полетата си. Например, нека разгледаме отново класа Dog:

public class Dog

{

      // Instance variables

      private string name;

      private int age;

}

Той има две полета съответно за име – name и възраст – age. Във всеки обект, всяко едно от тези полета има собствена стойност, която се съхранява на различно място в паметта за всеки обект.

Понякога обаче, искаме да имаме полета, които са общи за всички обекти от даден клас. За да постигнем това, трябва в декларацията на тези полета да използваме модификатора static. Както вече обяснихме, такива полета се наричат статични полета (static fields). В литерату­рата се срещат, също и като променливи на класа.

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

clip_image007[13]

Всички обекти, съз­дадени по описанието на един клас споделят статичните полета на класа.

Декларация на статични полета

Статичните полета декларираме по същия начин, както се декларира поле на клас, като след модификатора за достъп (ако има такъв), добавяме ключовата дума static:

[<access_modifier>] static <field_type> <field_name>

Ето как би изглеждало едно поле dogCount, което пази информация за броя на създадените обекти от клас Dog:

Dog.cs

public class Dog

{

      // Static (class) variable

      static int dogCount;

 

      // Instance variables

      private string name;

      private int age;

}

Статичните полета се създават, когато за първи път се опитаме да ги достъпим (прочетем / модифицираме). След създаването си, по подобие на обикновените полета в класа, те се инициализират с подразбиращата се стойност за типа си.

Инициализация по време на декларация

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

В горния пример можем да добавим инициализация на статичното поле:

// Static variable - declaration and initialization

static int dogCount = 0;

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

Достъп до статични полета

За разлика от обикновените (нестатични) полета на класа, статичните полета, бидейки асоциирани с класа, а не с конкретен обект, могат да бъдат достъпвани от външен клас като към името на класа, чрез точкова нотация, достъпим името на съответното статично поле:

<class_name>.<static_field_name>

Например, ако искаме да отпечатаме стойността на статичното поле, което пази броя на създадените обекти от нашия клас Dog, това ще стане по следния начин:

public static void Main()

{

      // Аccess to the static variable through class name

      Console.WriteLine("Dog count is now " + Dog.dogCount);

}

Съответно, резултатът от изпълнението на този Main() метод е:

Dog count is now 0

В C# статичните полета не могат да се достъпват през обект на класа (за разлика от други обектноориентирани езици за програмиране).

Когато даден метод се намира в класа, в който е дефинирано дадено статично поле, то може да бъде достъпено директно без да се задава името на класа, защото то се подразбира:

<static_field_name>

Модификация на стойностите на статичните полета

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

Ето защо, например, за да отчетем броя на създадените обекти от клас Dog, е удобно да използваме статично поле, което увеличаваме с единица, при всяко извикване на конструктора на класа, т.е. всеки път, когато създа­ва­ме обект от нашия клас:

public Dog(string name, int age)

{

      this.name = name;

      this.age = age;

 

      // Modifying the static counter in the constructor

      Dog.dogCount += 1;

}

Тъй като осъществяваме достъп до статично поле на класа Dog от него самия, можем да си спестим уточняването на името на класа и да ползваме следния код за достъп до полето dogCount:

public Dog(string name, int age)

{

      this.name = name;

      this.age = age;

 

      // Modifying the static counter in the constructor

      dogCount += 1;

}

За препоръчване е, разбира се, първият начин, при който е очевидно, че полето е статично в класа Dog. При него кодът е по-лесно четим.

Съответно, за да проверим дали това, което написахме е вярно, ще създадем няколко обекта от нашия клас Dog и ще отпечатаме броя им. Това ще стане по следния начин:

public static void Main()

{

      Dog dog1 = new Dog("Karaman", 1);

      Dog dog2 = new Dog("Bobi", 2);

      Dog dog3 = new Dog("Sharo", 3);

 

      // Access to the static variable