Глава 9. Методи

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

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

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

Съдържание

Видео

Презентация

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


Подпрограмите в програмирането

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

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

В някои езици за програмиране подпрограмите могат да се срещнат под наименованията функции (functions) или процедури (procedures). В C#, те се наричат методи (methods).

Какво е "метод"?

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

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

static double GetRectangleArea(double width, double height)

{

      double area = width * height;

      return area;

}

Защо да използваме методи?

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

По-добро структуриране и по-добра четимост

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

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

Избягване на повторението на код

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

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

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

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

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

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

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

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

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

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

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

Къде е позволено да декларираме метод

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

clip_image001

В езика C# можем да декларираме метод единствено в рамките на даден клас – между отварящата "{" и затварящата "}" му ско­би.

Най-очевидният пример за методи е вече познатият ни метод Main(…) – винаги го декларираме между отварящата и затварящата скоба на нашия клас, нали? Да си припомним това с един пример:

HelloCSharp.cs

public class HelloCSharp

{ // Opening brace of the class

 

      // Declaring our method between the class' braces

      public static void Main(string[] args)

      {

            Console.WriteLine("Hello C#!");

      }

} // Closing brace of the class

Декларация на метод

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

[public] [static] <return_type> <method_name>([<param_list>])

Задължителните елементи в декларацията на един метод са:

-     Тип на връщаната от метода стойност – <return_type>.

-     Име на метода – <method_name>.

-     Списък с параметри на метода – <param_list> – може да е празен списък или да съдържа поредица от декларации на параметри.

За онагледяване на елементите от декларацията на методите, можем да погледнем Main(…) метода в примера HelloCSharp от предходната секция:

public static void Main(string[] args)

При него, типът на връщаната стойност е void (т.е. методът не връща резултат), името му е Main, следвано от кръгли скоби, в които има списък с параметри, състоящ се от един параметър – масивът string[] args.

Последователността, в която трябва да се поставят отделните елементи от декларацията на метода е строго определена. Винаги на първо място е типът на връщаната стойност <return_type>, следвана от името на метода <method_name> и накрая, списък с параметри <param_list> ограден с кръгли скоби – "(" и ")". Опционално в началото на декларацията може да има модификатори за достъп (например public и static).

clip_image001[1]

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

Списъкът от параметри може да е празен и тогава просто пишем "()" след името на метода. Дори методът да няма параметри, кръглите скоби трябва да присъстват задължително в декларацията му.

clip_image001[2]

Кръглите скоби – "(" и ")", винаги следват името на метода, независимо дали той е с или без параметри.

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

Ключовите думи public и static в описанието на декларацията по-горе са неза­дъл­жителни и имат специално предназначение, което ще разгле­даме по-късно в тази глава. За момента ще разглеждаме методи, които винаги имат static в декларацията си. Повече за методите, които не са де­кларирани като static, ще научим от главата "Дефиниране на класове".

Сигнатура на метод

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

C#, като език за обектно-ориентирано програмиране, също разпознава еднозначно различните методи, използвайки тяхната спецификация (сигнатура) – името на метода <method_name> и списъкът с параметрите му – <param_list>.

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

clip_image001[3]

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

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

Име на метод

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

В примера показан по-долу, името на метода е PrintLogo:

static void PrintLogo()

{

      Console.WriteLine("Microsoft");

      Console.WriteLine("www.microsoft.com");

}

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

Добре е, когато декларираме името на метода, да спазваме правилата за именуване на методи, препоръчани ни от Microsoft:

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

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

-     Имената на методите е препоръчително да бъдат съставени от глагол или от глагол и съществително име.

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

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

Print

GetName

PlayMusic

SetUserName

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

Abc11

Yellow___Black

foo

_Bar

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

clip_image001[4]

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

-     Името на метода трябва да описва неговата цел.

-     Името на метода трябва да започва с главна буква.

-     Трябва да се прилага правилото PascalCase.

-     Името на метода трябва да е съставено от глагол или от двойка - глагол и съществително име.

Модификатори (modifiers)

Модификатор (modifier) наричаме ключова дума в езика C#, която дава допълнителна информация на компилатора за даден код.

Модификаторите, които срещнахме до момента са public и static. Сега ще опишем на кратко какво представляват те. Детайлно обяснение за тях, ще бъде дадено по-късно в главата "Дефиниране на класове". Да започнем с един пример:

public static void PrintLogo()

{

      Console.WriteLine("Microsoft");

      Console.WriteLine("www.microsoft.com");

}

В примера декларираме публичен метод чрез модификатора public. Той е специален вид модификатор, наречен модификатор за достъп (access modifier) и се използва, за да укаже, че извикването на метода може да става от кой да е C# клас, независимо къде се намира той. Публичните методи нямат ограничение кой може да ги извиква.

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

Когато един метод няма дефиниран модификатор за достъп (например public или private), той е достъпен от всички класове в текущото асембли, но не и от други асемблита (например от други проекти във Visual Studio). По тази причина за малки програмки, каквито са повечето примери в настоящата глава, няма да задаваме модификатори за достъп.

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

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

Имплементация (създаване) на собствен метод

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

Тяло на метод

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

static <return_type> <method_name>(<parameters_list>)

{

      // ... code goes here – in the method’s body ...

}

Реалната работа, която методът извършва, се намира именно в тялото на метода. В него трябва да бъде описан алгоритъмът, по който методът решава поставения проблем.

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

static void PrintLogo()

{ // Method’s body start here

      Console.WriteLine("Microsoft");

      Console.WriteLine("www.microsoft.com");

} // ... And finishes here

Обръщане отново внимание на едно от правилата, къде може да се декларира метод:

clip_image001[5]

Метод НЕ може да бъде деклариран в тялото на друг метод.

Локални променливи

Когато декларираме променлива в тялото на един метод, я наричаме локална променлива (local variable) за метода. Когато именуваме една променлива трябва да спазваме правилата за идентификатори в C# (вж. глава "Примитивни типове и променливи").

Областта, в която съществува и може да се използва една локална променлива, започва от реда, на който сме я декларирали и стига до затварящата фигурна скоба "}" на тялото на метода. Тази област се нарича област на видимост на променливата. Ако след като сме декларирали една променлива се опитаме да декларираме в същия метод друга промен­лива със същото име, ще получим грешка при компилация. Например да разгледаме следния код:

static void Main()

{

      int x = 3;

      int x = 4;

}

Компилаторът няма да ни позволи да ползваме името x за две различни променливи и ще изведе със съобщение по­добно на следното:

A local variable named 'x' is already defined in this scope.

Програмен блок (block) наричаме код, който се намира между отваряща и затваряща фигурни скоби "{" и "}".

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

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

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

Извикването на метода става просто като напишем името на метода <method_name>, следвано от кръглите скоби и накрая сложим знака за край на ред – ";":

<method_name>();

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

За да имаме ясна представа за извикването, ще покажем как бихме извикали метода, който използвахме в примерите по-горе – PrintLogo():

PrintLogo();

Изходът от изпълнението на метода ще бъде:

Microsoft

www.microsoft.com

Предаване на контрола на програмата при извик­ване на метод

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

Например, нека от метода Main() извикаме метода PrintLogo():

clip_image002

Първо ще се изпълни кодът от метода Main(), който е означен с (1), след това контролът на програмата ще се предаде на метода PrintLogo() – пунктираната стрелка (2). След това, ще се изпълни кодът в метода PrintLogo(), номериран с (3). След приключване на работата на метода PrintLogo() управлението на програмата ще бъде върнато обратно на метода Main() – пунктираната стрелка (4). Изпълнението на метода Main() ще продължи от реда, който следва извикването на метода PrintLogo() – стрелката маркирана с (5).

От къде може да извикаме метод?

Един метод може да бъде извикван от следните места:

-     От главния метод на програмата – Main():

static void Main()

{

      PrintLogo();

}

-     От някой друг метод, например:

static void PrintLogo()

{

      Console.WriteLine("Microsoft");

      Console.WriteLine("www.microsoft.com");

}

 

static void PrintCompanyInformation()

{

      // Invoking the PrintLogo() method

      PrintLogo();

 

      Console.WriteLine("Address: One, Microsoft Way");

}

-     Методът може да бъде извикан от собственото си тяло. Това се нарича рекурсия (recursion), но ще се запознаем нея по-подробно в следващата глава – "Рекурсия".

Независимост между декларацията и извикването на метод

Когато пишем на C# наредбата на методите в класовете не е от значение и е позволено извикването на метод да пред­хожда неговата декларация и имплементация. За да онагледим това, нека разгледаме следния пример:

static void Main()

{

      // ...

      PrintLogo();

      // ...

}

 

static void PrintLogo()

{

      Console.WriteLine("Microsoft");

      Console.WriteLine("www.microsoft.com");

}

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

clip_image001[6]

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

Използване на параметри в методите

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

Например, ако имаме метод, който намира лице на квадрат, в тялото му е описан алгоритъма, по който се намира лицето (формулата S = a2). Тъй като лицето на квадрата зависи от дължината на неговата страна, при пресмятането на лицето на всеки отделен квадрат, методът ни ще се нуждае от стойност, която задава дължината на страната му. Затова, ние трябва да му я подадем някак и за тази цел се използват параметрите.

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

За да можем да подадем информация на даден метод, която е нужна за неговата работа, използваме списък от параметри. Този списък, поста­вяме между кръглите скоби в деклара­цията на метода, след името му:

static <return_type> <method_name>(<parameters_list>)

{

      // Method’s body

}

Списъкът от параметри <parameters_list>, представлява списък от нула или повече декла­ра­ции на променливи, разделени със запетая, които ще бъдат изпол­звани в процеса на работа на метода:



<parameters_list> = [<type1> <name1>[, <typei> <namei>]],

където i = 2, 3,...

Когато създаваме метода и ни трябва дадена информация за реализиране­то на алгоритъма, избираме тази променлива от списъка от параметри, чийто тип е <typei> и я използваме съответно чрез името й <namei>.

Типът на параметрите в списъка може да бъде различен. Той може да бъ­де както примитивни типове – int, double, ... така и обекти (например string или масиви – int[], double[], string[], ...).

Метод за извеждане на фирмено лого – пример

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

static void PrintLogo(string logo)

{

      Console.WriteLine(logo);

}

По този начин, нашият метод вече няма да извежда само "Microsoft" като резултат от изпълнението си, но логото на всяка компания, чието име подадем като параметър от тип string. В примера виждаме също как използваме информацията подадена ни в списъка от параметри – променливата logo, дефинирана в списъка от параметри, се използва в тялото на метода чрез името, с което сме я де­финирали.

Метод за сумиране цените на книги в книжарница – пример

По-горе казахме, че когато е нужно, можем да подаваме като параметри на метода и масиви – int[], double[], string[], ... Нека в тази връзка разгледаме друг пример.

Ако сме в книжарница и искаме да пресметнем сумата, която дължим за всички книги, които желаем да закупим, можем да си създадем метод, който приема като входни данни цените на отделните книги във вид масив от тип decimal[] и връща общата им стойност, която трябва да заплатим на продавача:

static void PrintTotalAmountForBooks(decimal[] prices)

{

      decimal totalAmount = 0;

      foreach (decimal singleBookPrice in prices)

      {

            totalAmount += singleBookPrice;

      }

      Console.WriteLine("The total amount of all books is:" +

            totalAmount);

}

Поведение на метода в зависимост от входните данни

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

clip_image001[7]

Когато даден метод приема параметри, поведението му, зависи от тях.

Метод за извеждане знака на едно число – пример

За да стане ясно как поведението (изпълнението) на метода зависи от входните параметри, нека разгледаме следния метод, на който подаваме едно цяло число (от тип int), и в зависимост от това, дали числото е положително, отрицателно или нула, съответно той извежда на конзолата стойност "Positive", "Negative" или "Zero":

static void PrintSign(int number)

{

      if (number > 0)

      {

            Console.WriteLine("Positive");

      }

      else if (number < 0)

      {

            Console.WriteLine("Negative");

      }

      else

      {

            Console.WriteLine("Zero");

      }

}

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

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

Например, когато търсим по-голямото от две числа, ние подаваме два параметъра:

static void PrintMax(float number1, float number2)

{

  float max = number1;

  if (number2 > number1)

  {

    max = number2;

  }

  Console.WriteLine("Maximal number: " + max);

}

Особеност при декларацията на списък с много параметри

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

float var1, var2;

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

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

static void PrintMax(float var1, var2)

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

static void PrintMax(float var1, float var2)

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

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

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

PrintSign(-5);

PrintSign(balance);

 

PrintMax(100, 200);

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

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

За по-голяма яснота, при декларирането на метода, елементите на списъка от параметрите му, ще наричаме параметри (някъде в литера­турата могат да се срещнат също като "формални параметри").

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

С други думи, елементите на списъка от параметри var1 и var2, наричаме параметри:

static void PrintMax(float var1, float var2)

Съответно стойностите, при извикването на метода -23.5 и 100, наричаме аргументи:

PrintMax(100, -23.5);

Подаване на аргументи от примитивен тип

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

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

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

static void PrintNumber(int numberParam)

{

      // Modifying the primitive-type parameter

      numberParam = 5;

           

      Console.WriteLine("in PrintNumber() method, after the " +

            "modification, numberParam is {0}: ", numberParam);

}

Извиквайки го от метода Main():

static void Main()

{

      int numberArg = 3;

 

      // Copying the value 3 of the argument numberArg to the

      // parameter numberParam

      PrintNumber(numberArg);      

 

      Console.WriteLine("in the Main() method number is: " +

            numberArg);

}

Стойността 3 на променливата numberArg, се копира в параметъра numberParam. След като бъде извикан методът PrintNumber(), на параме­търа numberParam се присвоява стойността 5. Това не рефлектира върху стойността на променливата numberArg, тъй като при извикването на метода, в променливата numberParam се пази копие на стойността на подадения аргумент. Затова, методът PrintNumber() отпечатва числото 5. Съответно, след извикването на метода PrintNumber(), в метода Main() отпечатваме стойността на променливата numberArg и виждаме, че тя не е променена. Ето и изходът от изпълнението на горната програма:

in PrintNumber() method, after the modification numberParam is:5

in the Main() method number is: 3

Подаване на аргументи от референтен тип

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

Преди да обясним защо, нека припомним нещо от главата "Масиви". Масивът, като всеки референтен тип, се състои от промен­лива (рефе­ренция) и стойност – реалната информация в паметта на компютъра (нека я наречем обект). Съответно в нашия случай обектът представлява реал­ният масив от елементи. Променливата пази адреса на обекта в паметта (т.е. мястото в паметта, където се намират елементите на масива):

clip_image004

Когато оперираме с масиви, винаги го правим чрез променливата, с която сме ги декларирали. Така е и с всеки референтен тип. Следователно, когато подаваме аргумент от референтен тип, стойността, която е записана в променливата-аргумент, се копира в променливата, която е параметър в списъка от параметри на метода. Но какво става с обекта (реалният масив от елементи)? Копира ли се и той или не?

За да бъде по-нагледно обяснението, нека използваме следния пример: имаме метод ModifyArray(), който модифицира първия елемент на подаден му като параметър масив, като го реинициализира със стойност 5 и след това отпечатва елементите на масива, оградени в квадратни скоби и раз­делени със запетайки:

static void ModifyArray(int[] arrParam)

{

      arrParam[0] = 5;

 

      Console.Write("In ModifyArray() the param is: ");

      PrintArray(arrParam);

}

 

static void PrintArray(int[] arrParam)

{

      Console.Write("[");

      int length = arrParam.Length;

      if (length > 0)

      {

            Console.Write(arrParam[0].ToString());

            for (int i = 1; i < length; i++)

            {

                  Console.Write(", {0}", arrParam[i]);

            }

      }

      Console.WriteLine("]");

}

Съответно, декларираме и метод Main(), от който извикваме новосъздаде­ния метод ModifyArray():

static void Main()

{

      int[] arrArg = new int[] { 1, 2, 3 };

 

      Console.Write("Before ModifyArray() the argument is: ");

      PrintArray(arrArg);

 

      // Modifying the array's argument

      ModifyArray(arrArg);

 

      Console.Write("After ModifyArray() the argument is: ");

      PrintArray(arrArg);

}

Какъв ще е резултатът от изпълнението на този код? Нека погледнем:

Before ModifyArray() the argument is: [1, 2, 3]

In ModifyArray() the param is: [5, 2, 3]

After ModifyArray() the argument is: [5, 2, 3]

Забелязваме, че след изпълнението на метода ModifyArray(), масивът към който променливата arrArg пази референция, не съдържа [1,2,3], а съдържа [5,2,3]. Какво означава това?

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

clip_image001[8]

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

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

clip_image006

По време на извикването на ModifyArray(), стойността, която е запазена в аргумента arrArg, се копира в параметъра arrParam:

clip_image008

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

clip_image010

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

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

Подаване на изрази като аргументи на метод

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

PrintSign(2 + 3);

 

float oldQuantity = 3;

float quantity = 2;

PrintMax(oldQuantity * 5, quantity * 2);

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

Positive

Maximal number: 15.0

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

Подаване на аргументи съвместими с типа на съответния параметър

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

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

static void PrintNumber(float number)

{

      Console.WriteLine("The float number is: {0}", number);

}

 

static void Main()

{

      PrintNumber(5);

}

В примера, при извикването на метода PrintNumber() в метода Main(), първо целочисленият литерал 5 (който по подразбиране е от тип int) се преобразува до съответната стойност с десетична запетая 5.0f. След това така преобразуваната стойност се подава на ме­тода PrintNumber().

Както предполагаме, изходът от изпълнението на то­зи код е:

The float number is: 5.0

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

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

Например, ако се изисква параметър от тип float, е позволено стойността от пресмятането на израза да е например от тип int. Т.е. в горния пример, ако вместо PrintNumber(5), извикаме метода, като на мястото на 5, поставим например израза 2+3, резултатът от пресмятането на този израз, трябва да е от тип float (който метода очаква), или тип, който може да се преобразува до float безпроблемно (в нашия случай това е int). За да онагледим това, нека леко модифицираме метода Main() от предходната точка:

static void Main()

{

      PrintNumber(2 + 3);

}

В този пример съответно, първо ще бъде извършено сумирането, след това целочисленият резултат 5, ще бъде преобразуван до еквивалента му с плаваща запетая 5.0f и едва след това ще бъде извикан методът PrintNumber(…) с аргумент 5.0f. Резултатът отново ще бъде:

The float number is: 5.0

Спазване на последователността на типовете на аргументите

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

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

Person.cs

class Person

{

      static void PrintNameAndAge(string name, int age)

      {

            Console.WriteLine("I am {0}, {1} year(s) old.",

                  name, age);

      }

}

Нека към нашия клас добавим метод Main(), в който да извикаме нашия метод PrintNameAndAge(), като се опитаме да му подадем аргументи, кои­то вместо "Pesho" и 25, са в обратна последователност като типове – 25 и "Pesho:

static void Main()

{

      // Wrong sequence of arguments

      Person.PrintNameAndAge(25, "Pesho");

}

Компилаторът няма да намери метод, който се казва PrintNameAndAge и в същото време приема параметри, които са последователно от тип int и string. Затова, той ще ни уведоми за грешка:

The best overloaded method match for 'Person.PrintNameAndAge(string, int)' has some invalid arguments

Метод с променлив брой аргументи (var-args)

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

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

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

static void PrintTotalAmountForBooks(decimal[] prices)

{

      decimal totalAmount = 0;

 

      foreach (decimal singleBookPrice in prices)

      {

            totalAmount += singleBookPrice;

      }

      Console.WriteLine("The total amount of all books is:" +

            totalAmount);

}

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

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

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

decimal[] prices = new decimal[] { 3m, 2.5m };

PrintTotalAmountForBooks(prices);

Можем директно да подадем списъка с цените на книгите, като аргументи на метода:

PrintTotalAmountForBooks(3m, 2.5m);

PrintTotalAmountForBooks(3m, 5.1m, 10m, 4.5m);

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

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

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

static <return_type> <method_name>(<parameters_list>)

{

      // Method’s body

}

Разликата е, че <parameters_list> се декларира с ключовата дума params по следния начин:



<parameters_list> =

      [<type1> <name1>[, <typei> <namei>], params <var_type>[] <var_name>]

където i= 2, 3, ...

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

При декларацията на този елемент, преди типа му <var_type> трябва да добавим params: "params <var_type>[]". Типът <var_type> може да бъде както примитивен тип, така и референ­тен.

Правилата и особеностите за останалите елементи от списъка с параметри на метода, предхождащи var-args параметъра <var_name>, са същите, каквито ги разгледахме по-горе в тази глава.

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

static long CalcSum(params int[] elements)

{

      long sum = 0;

      foreach (int element in elements)

      {

            sum += element;

      }

      return sum;

}

 

static void Main()

{

      long sum = CalcSum(2, 5);

      Console.WriteLine(sum);

     

      long sum2 = CalcSum(4, 0, -2, 12);

      Console.WriteLine(sum2);

 

      long sum3 = CalcSum();

      Console.WriteLine(sum3);     

}

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

7

14

0

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

Параметърът от формалната дефиниция по-горе, който позволява подава­нето на променлив брой аргументи при извикването на метода – <var_name>, всъщност е име на масив от тип <var_type>. При извик­ването на метода, аргументите от тип <var_type> или тип съвместим с него, които подаваме на метода (независимо от броя им), ще бъдат съхранени в този масив. След това те ще бъдат използвани в тялото на метода. Достъпът и работата до тези елементи става по същия начин, по който работим с масиви.

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

static void PrintTotalAmountForBooks(params decimal[] prices)

{

      decimal totalAmount = 0;

 

      foreach (decimal singleBookPrice in prices)

      {

            totalAmount += singleBookPrice;

      }

      Console.WriteLine("The total amount of all books is:" +

            totalAmount);

}

Виждаме, че единствената промяна бе да сменим декларацията на масива prices като добавим params пред decimal[]. Въпреки това, в тялото на нашия метод, prices отново е масив от тип decimal, който използваме по познатия ни начин в тялото на метода.

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

static void Main()

{

      PrintTotalAmountForBooks(3m, 2.5m);

      PrintTotalAmountForBooks(1m, 2m, 3.5m, 7.5m);

}

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

The total amount of all books is: 5.5

The total amount of all books is: 14.0

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

static void Main()

{

      decimal[] pricesArr = new decimal[] { 3m, 2.5m };

 

      // Passing initialized array as var-arg:

      PrintTotalAmountForBooks(pricesArr);                             

}

Това е напълно легално извикване и резултатът от изпълнението на този код ще е следният:

The total amount of all books is: 5.5

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

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

Например, следният метод, приема като първи параметър елемент от тип string, а след него нула или повече елементи от тип int:

static void DoSomething(string strParam, params int[] x)

{

}

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

clip_image001[9]

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

Ако се опитаме да поставим декларацията на var-args параметъра x, от последния пример, да не бъде на последно място в списъка от параметри на метода:

static void DoSomething(params int[] x, string strParam)

{

}

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

A parameter array must be the last parameter in a formal parameter list

Ограничение на броя на параметрите за променлив брой аргументи

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

static void DoSomething(params int[] x, params string[] z)

{

}

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

A parameter array must be the last parameter in a formal parameter list

Това правило е частен случай на правилото за позицията на var-args параметъра – да бъде на последно място в списъка от параметри.

Особеност при извикване на метод с променлив брой пара­метри, без подаване на нито един параметър

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

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

static void Main()

{

      PrintTotalAmountForBooks();                          

}

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

The total amount of all books is: 0

Това е така, защото, въпреки че не сме подали нито една стойност на нашия метод, при извикването на метода, масивът decimal[] prices е създаден, но е празен (т.е. не съдържа нито един елемент).

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

Метод променлив брой параметри – пример

Имайки предвид как дефинираме методи с променлив брой аргументи, можем да запишем добре познатият ни Main() метод по следния начин:

public static void Main(params String[] args)

{

      // Method body comes here

}

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

Именувани и незадължителни параметри

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

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

static void SomeMethod(int x, int y = 5, int z = 7)

{

}

В горния пример y и z са незадължителни параметри и могат да бъдат пропуснати при извикване на метода:

static void Main()

{

      // Normal call of SomeMethod

      SomeMethod(1, 2, 3);

      // Оmitting z - equivalent to SomeMethod(1, 2, 7)

      SomeMethod(1, 2);

      // Omitting both y and z – equivalent to SomeMethod(1, 5, 7)

      SomeMethod(1);

}

Подаването на стойности на параметри по име става чрез задаване на името на параметъра, следвано от двоеточие и от стойността на параме­търа. Ето един пример:

static void Main()

{

      // Passing z by name

      SomeMethod(1, z: 3);

      // Passing both x and z by name

      SomeMethod(x: 1, z: 3);

      // Reversing the order of the arguments

      SomeMethod(z: 3, x: 1);

}

Всички извиквания в горния пример са еквивалентни – пропуска се параметърът y, а като стойности на параметрите x и z се подават съот­ветно 1 и 3. Единствената разлика е, че стойностите на параметрите се изчисля­ват в реда в който са подадени, така че в последното извикване 3 се изчислява преди 1 (в случая 3 е просто константа, но ако е някакъв по-сложен израз, редът на изчисление би могъл да е от значение).

Варианти на методи (method overloading)

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

Например, да си представим, че имаме задачата да напишем програма, която рисува на екрана букви и цифри. Съответно можем да си пред­ставим, че нашата програма, може да има методите за рисуване съответно на низове DrawString(string str), на цели числа – DrawInt(int number), на десетични числа – DrawFloat(float number) и т.н.:

static void DrawString(string str)

{

      // Draw string

}

 

static void DrawInt(int number)

{

      // Draw integer

}

 

static void DrawFloat(float number)

{

      // Draw float number

}

Но езикът C# позволява да си създадем съответно само варианти на един и същ метод Draw(…), който приема комбинации от различни типове параметри, в зависи­мост от това, какво искаме да нарисуваме на екрана:

static void Draw(string str)

{

      // Draw string

}

 

static void Draw(int number)

{

      // Draw integer

}

 

static void Draw(float number)

{

      // Draw float number

}

Горната дефиниция на методи е валидна и се компилира без грешки. Методът Draw(…) от примера се нарича предефиниран (overloaded).

Значение на параметрите в сигнатурата на метода

Както обяснихме по-горе, за спецификацията (сигнатурата) на един метод, в C#, единствените елементи от списъка с параметри, които имат значе­ние, са типовете на параметрите и последователността, в която са изброени. Имената на параметрите нямат значение за едно­значното дек­лариране на метода.

clip_image001[10]

За еднозначното деклариране на метод в C#, по отноше­ние на списъка с параметри на метода, единствено има значение неговата сигнатура, т.е.:

-     типът на параметрите на метода

-     последователността на типовете в списъка от пара­метри

Имената на параметрите не се вземат под внимание.

Например за C#, следните две декларации, са декларации на един и същ метод, тъй като типовете на параметрите в списъка от параметри са едни и същи – int и float, независимо от имената на променливите, които сме поставили – param1 и param2 или arg1 и arg2:

static void DoSomething(int param1, float param2) { }

static void DoSomething(int arg1, float arg2) { }

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

Type '<the_name_of_your_class>' already defines a member called 'DoSomething' with the same parameter types.

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

Напри­мер, ако във втория метод, вто­рият параметър от списъка на единия от методите – float arg2, го декларираме да не бъде от тип float, а int, тогава това ще бъдат два различни метода с различна сигнатура – DoSomething(int, float) и DoSomething(int, int). Вторият елемент от сигнатурата им – списъкът от параметри, е напълно различен, тъй като типовете на вторите им елементи от списъка са различни:

static void DoSomething(int arg1, float arg2) { }

static void DoSomething(int param1, int param2) { }

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

static void DoSomething(int param1, float param2) { }

static void DoSomething(int param1, int param2) { }

Компилаторът отново "няма възражения", ако декларираме вариант на метод, но този път вместо да подменяме типа на втория параметър, просто разменим местата на параметрите на втория метод:

static void DoSomething(int param1, float param2) { }

static void DoSomething(float param2, int param1) { }

Тъй като последователността на типовете на параметрите в списъка с параметри е различна, съответно и спецификациите (сигнатурите) на методите са различни. Щом списъците с параметри са различни, то еднаквите имена (DoSomething) нямат отношение към еднозначното дек­лариране на мето­дите в нашия клас – имаме различни сигнатури.

Извикване на варианти на методи (overloaded methods)

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

static void PrintNumbers(int intValue, float floatValue)

{

          Console.WriteLine(intValue + "; " + floatValue);

}

 

static void PrintNumbers(float floatValue, int intValue)

{

          Console.WriteLine(floatValue + "; " + intValue);

}

 

static void Main()

{

          PrintNumbers(2.71f, 2);

          PrintNumbers(5, 3.14159f);

}

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

2.71; 2

5; 3.14159

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

static void Main()

{

          PrintNumbers(2, 3);

}

Причината за грешката е, че компилаторът се опитва да преобразува двете цели числа към подходящи типове, за да ги подаде на един от двата метода с име PrintNumbers, но съответните преобразувания не са едно­значни. Има два варианта – или първият параметър да се преобразува към float и да се извика методът PrintNumbers(float, int) или вторият параметър да се преобразува към float и да се извика методът PrintNumbers(int, float). Това е нееднозначност, която компилаторът изисква да бъде разрешена ръчно, например по следния начин:

static void Main()

{

          PrintNumbers((float)2, (short)3);

}

Горния код ще се компилира успешно, тъй като след преобразованието на аргументите, става еднозначно кой точно метод да бъде извикан – PrintNumbers(float, int).

Методи със съвпадащи сигнатури

Накрая, преди да продължим със няколко интересни примера за използ­ване на методи, нека да разгледаме следния пример за некоректно предефиниране (overload) на методи:

static int Sum(int a, int b)

{

    return a + b;

}

 

static long Sum(int a, int b)

{

    return a + b;

}

 

static void Main()

{

    Console.WriteLine(Sum(2, 3));

}

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

Триъгълници с различен размер – пример

След като разгледахме как да декларираме и извикваме методи с параметри и как да връщане резултати от извикване на метод, нека сега дадем един по-цялостен пример, с който да покажем къде може да се използват методите с параметри. Да предположим, че искаме да напишем програма, която отпечатва триъгълници, като тези, показани по-долу:

                                                                                    1

                        1                                                           1 2

                        1 2                                                   1 2 3

                        1 2 3                                           1 2 3 4

                        1 2 3 4                                               1 2 3 4 5

n=5   ->          1 2 3 4 5               n=6   ->          1 2 3 4 5 6

                        1 2 3 4                                               1 2 3 4 5

                        1 2 3                                           1 2 3 4

                        1 2                                                   1 2 3

                        1                                                           1 2

                                                                                    1

Едно възможно решение на задачата е дадено по-долу:

Triangle.cs

using System;

 

class Triangle

{

      static void Main()

      {

            // Entering the value of the variable n

            Console.Write("n = ");

            int n = int.Parse(Console.ReadLine());

            Console.WriteLine();

           

            // Printing the upper part of the triangle

            for (int line = 1; line <= n; line++)

            {

                  PrintLine(1, line);

            }

 

            // Printing the bottom part of the triangle

            // that is under the longest line

            for (int line = n - 1; line >= 1; line--)

            {

                  PrintLine(1, line);

            }

      }

 

      static void PrintLine(int start, int end)

      {

            for (int i = start; i <= end; i++)

            {

                  Console.Write(" " + i);

            }

            Console.WriteLine();

      }

}

Нека разгледаме как работи примерното решение. Тъй като, можем да печатаме в конзолата ред по ред, разглеждаме триъгълниците, като поредици числа, разположени в отделни редове. Следователно, за да ги изведем в конзолата, трябва да имаме средство, което извежда отделните редове от триъгълниците. За целта, създаваме метода PrintLine(…).

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

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

След това създаваме алгоритъм за отпечатването на триъгълниците, като цялостни фигури, в метода Main(). Чрез метода int.Parse въвеждаме стойността на променливата n и извеждаме празен ред.

След това, в два последователни for-цикъла конструираме триъгълника, който трябва да се изведе, за даденото n. В първия цикъл отпечатваме последователно всички редове от горната част на триъгълника до средния – най-дълъг ред, включително. Във втория цикъл, отпечатваме редовете на триъгълника, които трябва да се изведат под средния (най-дълъг) ред.

Както отбелязахме по-горе, номерът на реда, съответства на броя на елементите (числа) намиращи се на съответния ред. И тъй като винаги започваме от числото 1, номерът на реда, в горната част от триъгълника, винаги ще е равен на последния елемент на редицата, която трябва да се отпечата на дадения ред. Следователно, можем да използваме това при извикването на метода PrintLine(…), тъй като той изисква точно тези параметри за изпълнението на задачата си.

Прави ни впечатление, че броят на елементите на редиците, се увеличава с единица и съответно, последният елемент на всяка по-долна редица, трябва да е с единица по-голям от последния елемент на редицата от предходния ред. Затова, при всяко "завъртане" на първия for-цикъл, подаваме на метода PrintLine(…), като първи параметър 1, а като втори – текущата стойност на променливата line. Тъй като при всяко изпълнение на тялото на цикъла line се увеличава с единица, на при всяка итерация методът PrintLine(…) ще отпечатва редица с един елемент повече от предходния ред.

При втория цикъл, който отпечатва долната част на триъгълника, след­ваме обратната логика. Колкото по-надолу печатаме, редиците трябва да се смаляват с по един елемент и съответно последният елемент на всяка редица, трябва да е с единица по-малък от последния елемент на реди­цата от предходния ред. От тук задаваме началното условие за стойността на променливата line във втория цикъл: line = n-1. След всяко завър­тане на цикъла намаляваме стойността на line с единица и я подаваме като втори параметър на PrintLine(…).

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

static void Main()

{

      Console.Write("n = ");

      int n = int.Parse(Console.ReadLine());

      Console.WriteLine();

 

      PrintTriangle(n);

}

 

static void PrintTriangle(int n)

{

      // Printing the upper part of the triangle

      for (int line = 1; line <= n; line++)

      {

            PrintLine(1, line);

      }

 

      // Printing the bottom part of the triangle

      // that is under the longest line

      for (int line = n - 1; line >= 1; line--)

      {

            PrintLine(1, line);

      }

}

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

n = 3

 

 1

 1 2

 1 2 3

 1 2

 1

Връщане на резултат от метод

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

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

Ако погледнем отново как декларираме метод:

static <return_type> <method_name>(<parameters_list>)

Ще си припомним, че когато обяснявахме за това, споменахме, че на мястото на <return type> поставяме void. Сега ще разширим дефиницията, като кажем, че на това място може да стои не само void, но и произволен тип – примитивен (int, float, double, …) или референтен (например string или ма­сив), в зави­си­мост от какъв тип е резултатът от изпълнението на метода.

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

static double CalcSquareSurface(double sideLength)

Вижда се, че резултатът от пресмятането на лицето е от тип double.

Употреба на връщаната стойност

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

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

Може да присвоим резултата от изпълнението на метода на променлива от подходящ тип:

// GetCompanyLogo() returns a string

string companyLogo = GetCompanyLogo();

Употреба в изрази

След като един метод върне резултат, този резултат може да бъде използван в изрази.

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

float totalPrice = GetSinglePrice() * quantity;

Подаване като стойност в списък от параметри на друг метод

Можем да подадем резултата от работата на един метод като стойност в списъка от параметри на друг метод:

Console.WriteLine(GetCompanyLogo());

В този пример, отначало извикваме метода GetCompanyLogo(), подавайки го като аргумент на метода WriteLine(). Веднага, след като методът GetCompanyLogo() бъде изпълнен, той ще върне резул­тат, например "Microsoft Corporation". Тогава C# ще "подмени" извик­ва­не­то на ме­тода, с резултата, който е върнат от изпълнението му и можем да прие­мем, че в кода имаме:

Console.WriteLine("Microsoft Corporation");

Тип на връщаната стойност

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

Операторът return

За да накараме един метод да връща стойност, трябва в тялото му, да използваме ключовата дума return, следвана от израз, който да бъде върнат като резултат от метода:

static <return_type> <method_name>(<parameters_list>)

{

  // Some code that is preparing the method’s result comes here

  return <method’s_result>;

}

Съответно <method’s_result>, е от тип <return_type>. Например:

static long Multiply(int number1, int number2)

{

      long result = number1 * number2;

      return result;

}

В този метод, след умножението, благодарение на return, методът ще върне като резултат от изпълнението на метода целочислената промен­лива result.

Резултат от тип, съвместим, с типа на връщаната стойност

Резултатът, който се връща от метода, може да е от тип, който е съвместим (който може неявно да се преобразува) с типа на връщаната стойност <return_type>.

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

static float Multiply(int number1, int number2)

{

      int result = number1 * number2;

      return result;

}

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

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

Позволено е (когато това няма да направи кода трудно четим) след ключовата дума return, да поставяме директно изрази:

static int Multiply(int number1, int number2)

{

      return number1 * number2;

}

В тази ситуация, след като изразът number1 * number2 бъде изчислен, ре­зултатът от него ще бъде заместен на мястото на израза и ще бъде вър­нат от оператора return.

Характеристики на оператора return

При изпълнението си операторът return извършва две неща:

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

-     Връща резултата от изпълнението на метода към извикващия метод.

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

Ако все пак направим това, компилаторът ще покаже предупреждение:

static int Add(int number1, int number2)

{

      int result = number1 + number2;

      return result;

 

      // Let us try to "clean" the result variable here:

      result = 0;

}

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

Unreachable code detected

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

static void PrintPositiveNumber(int number)

{

      if (number <= 0)

      {

            // If the number is NOT positive, terminate the method

            return;

      }

      Console.WriteLine(number);

}

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

Нека разгледаме примера за метод, който получава като параметри две числа и в зависимост дали първото е по-голямо от второто, двете са равни, или второто е по-голямо от първото, връща съответно 1, 0 и -1:

static int CompareTo(int number1, int number2)

{

      if (number1 > number2)

      {

            return 1;

      }

      else if (number1 == number2)

      {

            return 0;

      }

      else

      {

            return -1;

      }

}

Защо типът на връщаната стойност не е част от сигнатурата на метода?

В C# не е позволено да имаме няколко метода, които имат еднакви име и  параметри, но различен тип на връщаната стойност. Това означава, че следния код няма да се компилира:

static int Add(int number1, int number2)

{

      return (number1 + number2);

}

 

static double Add(int number1, int number2)

{

      return (number1 + number2);

}

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

Type '<the_name_of_your_class>' already defines a member called 'Add' with the same parameter types

където <the_name_of_your_class> е името на класа, в който се опитваме да декларираме двата метода.

Преминаване от Фаренхайт към Целзий – пример

В следващата задача се изисква да напишем програма, която при пода­дена от потребителя телесна температура, измерена в градуси по Фаренхайт, да я преобразува и изведе в съответстващата й температура в градуси по Целзий със следното съобщение: "Your body temperature in Celsius degrees is X", където Х е съответно градусите по Целзий. В допълнение, ако измерената температура в градуси Целзий е по-висока от 37 градуса, програмата трябва да предупреждава потребителя, че е болен, със съобщението "You are ill!".

Като за начало можем да направим бързо проучване в Интернет и да прочетем, че формулата за преобразуване на температури е ºC = (ºF - 32) * 5 / 9, където съответно с ºC отбелязваме темпе­ра­ту­рата в градуси Целзий, а с ºF – съответно тази в градуси Фаренхайт.

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

-     Вземаме температурата измервана в градуси по Фаренхайт като вход от клавиатурата (потребителят ще трябва да я въведе).

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

-     Извеждаме съобщение за преобразуваната температура в Целзий.

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

Ето едно примерно решение:

TemperatureConverter.cs

using System;

 

class TemperatureConverter

{

      static double ConvertFahrenheitToCelsius(double temperatureF)

      {

            double temperatureC = (temperatureF - 32) * 5 / 9;

            return temperatureC;

      }

 

      static void Main()

      {

            Console.Write(

                  "Enter your body temperature in Fahrenheit degrees: ");

            double temperature = double.Parse(Console.ReadLine());

 

            temperature =       ConvertFahrenheitToCelsius(temperature);

 

            Console.WriteLine(

                  "Your body temperature in Celsius degrees is {0}.",

                  temperature);

 

            if (temperature >= 37)

            {

                  Console.WriteLine("You are ill!");

            }

      }

}

Операциите по въвеждането на температурата и извеждането на съобще­нията са тривиални, и за момента прескачаме решението им, като се съсредоточаваме върху преобразуването на температурите. Виждаме, че това е логически обособено действие, което може да отделим в метод. Това, освен че ще направи кода ни по-четим, ще ни даде възможност в бъдеще, да правим подобно преоб­разование отново като преизползваме този метод. Декларираме метода ConvertFahrenheitToCelsius(…), със списък от един параметър с името temperatureF, който представлява измерената температура в градуси по Фаренхайт и връща съответно число от тип double, което представлява преобразуваната температура в градуси по Целзий. В тялото му ползваме намерената в Интернет формула чрез синтаксиса на C#.

След като сме приключили с тази стъпка от решението на задачата, решаваме, че останалите стъпки няма нужда да ги извеждаме в методи, а е достатъчно да ги имплементираме в метода Main() на класа.

С помощта на метода double.Parse(…), получаваме телесната темпера­тура на потребителя, като предварително сме го попитали за нея със съобщението "Enter your body temperature in Fahrenheit degrees".

След това извикваме метода ConvertFahrenheitToCelsius() и съхраня­ваме върнатия резултат в променливата temperature.

С помощта на метода Console.WriteLine() извеждаме съобщението "Your body temperature in Celsius degrees is X", където X заменя­ме със стойността на temperature.

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

Ето примерен изход от програмата:

Enter your body temperature in Fahrenheit degrees: 100

Your body temperature in Celsius degrees is 37,777778.

You are ill!

Разстояние между два месеца – пример

Да разгледаме следната задача: искаме да напишем програма, която при зададени две числа, които трябва да са между 1 и 12, за да съответстват на номер на месец от годината, да извежда броя месеци, които делят тези два месеца. Съобщението, което програмата трябва да отпечатва в конзолата трябва да е "There is X months period from Y to Z.", където Х е броят на месеците, който трябва да изчислим, а Y и Z, са съответно имената на месеците за начало и край на периода.

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

-     Да въведем номерата на месеците за начало и край на периода.

-     Да пресметнем периода между въведените месеци.

-     Да изведем съобщението.

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

Ето едно възможно решение на поставената задача:

Months.cs

using System;

 

class Months

{

      static string GetMonth(int month)

      {

            string monthName;

            switch (month)

            {

            case 1:

                  monthName = "January";

                  break;

            case 2:

                  monthName = "February";

                  break;

            case 3:

                  monthName = "March";

                  break;

            case 4:

                  monthName = "April";

                  break;

            case 5:

                  monthName = "May";

                  break;

            case 6:

                  monthName = "June";

                  break;

            case 7:

                  monthName = "July";

                  break;

            case 8:

                  monthName = "August";

                  break;

            case 9:

                  monthName = "September";

                  break;

            case 10:

                  monthName = "October";

                  break;

            case 11:

                  monthName = "November";

                  break;

            case 12:

                  monthName = "December";

                  break;

            default:

                  Console.WriteLine("Invalid month!");

                  return null;

            }

            return monthName;

      }

 

      static void SayPeriod(int startMonth, int endMonth)

      {

            int period = endMonth - startMonth;

            if (period < 0)

            {

                  // Fix negative distance

                  period = period + 12;

            }

            Console.WriteLine(

                  "There is {0} months period from {1} to {2}.",

                  period, GetMonth(startMonth), GetMonth(endMonth));

      }

 

      static void Main()

      {

            Console.Write("First month (1-12): ");

            int firstMonth = int.Parse(Console.ReadLine());

 

            Console.Write("Second month (1-12): ");

            int secondMonth = int.Parse(Console.ReadLine());

 

            SayPeriod(firstMonth, secondMonth);

      }

}

Решението на първата подзадача е тривиално. В метода Main()  използваме метода int.Parse(…) и получаваме номерата на месеците за периода, чиято дължина искаме да пресметнем.

След това забелязваме, че пресмятането на периода и отпечатването на съобщението може да се обособи логически като подзадачка, и затова създаваме метод SayPeriod(…) с два параметъра – числа, съответстващи на номерата на месеците за начало и край на периода. Той няма да връща стойност, но ще пресмята периода и ще отпечатва съобщението описано в условието на задачата с помощта на стандартния изход – Console. WriteLine(…).

Очевидното решение, за намирането на дължината на периода между два месеца, е като извадим поредния номер на началния месец от този на месеца за край на периода. Съобразяваме обаче, че ако номерът на втория месец е по-малък от този на първия, тогава потребителят е имал пред­вид, че вторият месец, не се намира в текущата година, а в следва­щата. Затова, ако разликата между двата месеца е отрицателна, към нея добавяме 12 – дължината на една година в брой месеци, и полу­чаваме дължината на търсения период. След това извеждаме съобще­ние­то, като за отпечатването на имената на месеците, чийто пореден номер получа­ваме от потребителя, използваме метода GetMonth(…).

Методът за извличане на име на месец по номера му можем да реа­лизираме чрез условната конструкция switch-case, с която да съпоставим на всяко число, съответстващото му име на месец от годината. Ако стойността на входния параметър не е някоя между стойностите 1 и 12, съобщаваме за грешка. По-нататък в главата "Обработка на изключе­ния" ще обясним как можем да съобщаваме за грешка по-начин, който позволява грешката да бъде прихващана и обработвана, но за момента просто ще отпечатваме съобщение за грешка на конзолата.

Накрая, в метода Main()извикваме метода SayPeriod(), подавайки му въведените от потребителя числа за начало и край на периода и с това сме решили задачата.

Ето какъв би могъл да е изходът от програмата при входни данни 2 и 6:

First month (1-12): 2

Second month (1-12): 6

There is 4 months period from February to June.

Валидация на данни – пример

В тази задача, трябва да напишем програма, която пита потребителя колко е часът (с извеждане на въпроса "What time is it?"). След това потребителят, трябва да въведе две числа, съответно за час и минути. Ако въведените данни представляват валидно време, програмата, трябва да изведе съобщението "The time is HH:mm now.", където с НН съответно сме означили часа, а с mm – минутите. Ако въведените час или минути не са валидни, програмата трябва да изведе съобщението "Incorrect time!".

След като прочитаме условието на задачата внимателно, стигаме до извода, че решението на задачата може да се разбие на следните подзадачи:

-     Получаване на входа за час и минути.

-     Проверка на валидността на входните данни.

-     Извеждаме съобщение за грешка или валидно време.

Знаем, че обработката на входа и извеждането на изхода няма да бъдат проблем за нас, затова решаваме да се фокусираме върху проблема с валидността на входните данни, т.е. валидността на числата за часове и минути. Знаем, че часовете варират от 0 до 23 включително, а минутите съответно от 0 до 59 включително. Тъй като данните (часове и минути) не са еднородни решаваме да създадем два отделни метода, единият от които проверява валидността на часовете, а другия – на минутите.

Ето едно примерно решение:

DataValidation.cs

using System;

class DataValidation

{

      static void Main()

      {

            Console.WriteLine("What time is it?");

 

            Console.Write("Hours: ");

            int hours = int.Parse(Console.ReadLine());

 

            Console.Write("Minutes: ");

            int minutes = int.Parse(Console.ReadLine());

 

            bool isValidTime =

                  ValidateHours(hours) && ValidateMinutes(minutes);

            if (isValidTime)

            {

                  Console.WriteLine("The time is {0}:{1} now.",

                        hours, minutes);

            }

            else

            {

                  Console.WriteLine("Incorrect time!");

            }

      }

 

      static bool ValidateHours(int hours)

      {

            bool result = (hours >= 0) && (hours < 24);

            return result;

      }

 

      static bool ValidateMinutes(int minutes)

      {

            bool result = (minutes >= 0) && (minutes <= 59);

            return result;

      }

}

Методът, който проверява часовете, именуваме ValidateHours(), като той приема едно число от тип int за часовете и връща резултат от тип bool, т.е. true ако въведеното число е валиден час и false в противен случай:

static bool ValidateHours(int hours)

{

  bool result = (hours >= 0) && (hours < 24);

  return result;

}

По подобен начин, декларираме метод, който проверява валидността на минутите. Наричаме го ValidateMinutes(), като той приема един пара­метър цяло число за минутите и има тип на връщана стойност – bool. Ако въведеното число удовлетворява условието, което описахме по-горе (да е между 0 и 59 включително), методът ще върне като резултат true, а иначе – false:

static bool ValidateMinutes(int minutes)

{

      bool result = (minutes >= 0) && (minutes <= 59);

      return result;

}

След като сме готови с най-сложната част от задачата, декларираме метода Main(). В тялото му, извеждаме въпроса според условието на задачата – "What time is it?". След това с помощта на метода int.Parse(…), прочитаме от потребителя числата за часове и минути, като резултатите ги съхраняваме в целочислените променливи, съответно hours и minutes:

Console.WriteLine("What time is it?");

 

Console.Write("Hours: ");

int hours = int.Parse(Console.ReadLine());

 

Console.Write("Minutes: ");

int minutes = int.Parse(Console.ReadLine());

Съответно, резултата от валидацията съхраняваме в променлива от тип boolisValidTime, като последователно извикваме методите, които вече декларирахме – ValidateHours() и ValidateMinutes(), като съответ­но им подаваме като аргументи промен­ливите hours и minutes. За да ги валидираме едновременно, обединяваме резултатите от извикването на методите с оператора за логическо "и" &&:

bool isValidTime =

      ValidateHours(hours) && ValidateMinutes(minutes);

След като сме съхранили резултата, дали въведеното време е валидно или не, в променливата isValidTime, го използваме в условната конструкция if, за да изпълним и последния подпроблем от цялостната задача – извеж­дането на информация към потребителя дали времето, въведено от него е валидно или не. С помощта на Console.WriteLine(…), ако isValidTime е true, на конзолата извеждаме "The time is HH:mm now.", където HH е съответно стойността на променливата hours, а mm – тази на променливата minutes. Съответно в else частта от условната конструкция извеждаме, че въведе­ното време е невалидно – "Incorrect time!".

Ето как изглежда изходът от програмата при въвеждане на коректни данни:

What time is it?

Hours: 17

Minutes: 33

The time is 17:33 now.

Ето какво се случва при въвеждане на некоректни данни:

What time is it?

Hours: 33

Minutes: -2

Incorrect time!

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

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

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

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

-     Как да извършим сортирането на тези числа.

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

static int[] Sort(int[] numbers)

{

      // The sorting logic comes here...

 

      return numbers;

}

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

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

static int[] Sort(params int[] numbers)

{

      // The sorting logic comes here...

 

      return numbers;

}

Сега трябва да решим как да сортираме нашия масив. Един от най-лесните начини това да бъде направено е чрез така наречения метод на пряката селекция (selection sort algorithm). При него масивът се разделя на сортирана и несортирана част. Сортираната част се намира в лявата част на масива, а несортираната – в дясната. При всяка стъпка на алгоритъма, сортираната част се разширява надясно с един елемент, а несортираната  – намалява с един от ляво.

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

clip_image012

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

clip_image014

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

clip_image016

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

clip_image018

Тя се разменя с първия елемент от оставащата несортирана част:

clip_image020

clip_image022

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

clip_image024

clip_image026

Накрая масивът е сортиран:

clip_image028

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

static int[] Sort(params int[] numbers)

{

      // The sorting logic:

      for (int i = 0; i < numbers.Length - 1; i++)

      {

            // Loop operating over the unsorted part of the array

            for (int j = i + 1; j < numbers.Length; j++)

            {

                  // Swapping the values

                  if (numbers[i] > numbers[j])

                  {

                        int temp = numbers[i];

                        numbers[i] = numbers[j];

                        numbers[j] = temp;

                  }

            }

      } // End of the sorting logic

      return numbers;

}

Нека декларираме и един метод PrintNumbers(params int[]) за извеж­дане на списъка с числа в конзолата и тестваме с нашия примерен масив от числа като напишем няколко реда в Main(…) метода:

SortingEngine.cs

using System;

 

class SortingEngine

{

      static int[] Sort(params int[] numbers)

      {

            // The sorting logic:

            for (int i = 0; i < numbers.Length - 1; i++)

            {

                  // Loop that is operating over the un-sorted part of

                  // the array

                  for (int j = i + 1; j < numbers.Length; j++)

                  {

                        // Swapping the values

                        if (numbers[i] > numbers[j])

                        {

                              int oldNum = numbers[i];

                              numbers[i] = numbers[j];

                              numbers[j] = oldNum;

                        }

                  }

            } // End of the sorting logic

            return numbers;

      }

 

      static void PrintNumbers(params int[] numbers)

      {

            for (int i = 0; i < numbers.Length; i++)

            {

                  Console.Write("{0}", numbers[i]);

                  if (i < (numbers.Length - 1))

                  {

                        Console.Write(", ");

                  }

            }

      }

 

      static void Main()

      {

            int[] numbers = Sort(10, 3, 5, -1, 0, 12, 8);

            PrintNumbers(numbers);

      }

}

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

-1, 0, 3, 5, 8, 10, 12

Утвърдени практики при работа с методи

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

-     Всеки метод трябва да решава самостоятелна, добре дефинирана задача. Това свойство се нарича strong cohesion, т.е. фокусиране върху една, единствена задача, а не няколко несвързани задачи. Ако даден метод прави само едно нещо, кодът му е по-разбираем и по-лесен за поддръжка. Един метод не трябва да решава няколко задачи едновременно!

-     Един метод трябва да има добро име, т.е. име, което описва какво прави той. Примерно метод, който сортира числа, трябва да се казва SortNumbers(), а не Number() или Processing() или Method2(). Ако не можете да измис­лите подходящо име за даден метод, то най-вероятно методът решава повече от една задача и трябва да се раздели на няколко отделни метода.

-     Имената на методите е препоръчително да изразяват действие, поради което трябва да бъдат съставени от глагол или от глагол + съществително име (евентуално с прилагателно, което пояснява съществителното), примерно FindSmallestElement() или Sort(int[] arr) или ReadInputData().

-     Имената на методите в C# е прието да започват с голяма буква. Използва се правилото PascalCase, т.е. всяка нова дума, която се долепя в задната част на името на метода, започва с главна буква, например SendEmail(…), a не sendEmail(…) или send_email(…).

-     Един метод или трябва да свърши работата, която е описана от името му, или трябва да съобщи за грешка. Не е коректно методите да връщат грешен или странен резултат при некоректни входни данни. Методът или решава задачата, за която е предназна­чен, или връща грешка. Всякакво друго поведение е некоректно. Ще обясним в детайли по какъв начин методите могат да съобщават за грешки в главата "Обработка на изключения".

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

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

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

Упражнения

1.      Напишете метод, който при подадено име отпечатва в конзолата "Hello, <name>!" (например "Hello, Peter!"). Напишете програма, която тества този метод дали работи правилно.

2.      Създайте метод GetMax() с два целочислени (int) параметъра, който връща по-голямото от двете числа. Напишете програма, която прочита три цели числа от конзолата и отпечатва най-голямото от тях, изпол­звайки метода GetMax().

3.      Напишете метод, който връща английското наименование на послед­ната цифра от дадено число. Примери: за числото 512 отпечатва "two"; за числото 1024 – "four".

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

5.      Напишете метод, който проверява дали елемент, намиращ се на дадена позиция от масив, е по-голям, или съответно по-малък от двата му съседа. Тествайте метода дали работи коректно.

6.      Напишете метод, който връща позицията на първия елемент на масив, който е по-голям от двата свои съседи едновременно, или -1, ако няма такъв елемент.

7.      Напишете метод, който отпечатва цифрите на дадено десетично число в обратен ред. Например 256, трябва да бъде отпечатано като 652.

8.      Напишете метод, който пресмята сумата на две цели положителни цели числа. Числата са представени като масив от цифрите си, като последната цифра (единиците) са записани в масива под индекс 0. Направете така, че метода да работи за числа с дължина до 10 000 цифри.

9.      Напишете метод, който намира най-големия елемент в част от масив. Използвайте метода за да сортирате възходящо/низходящо даден масив.

10.   Напишете програма, която пресмята и отпечатва n! за всяко n в интервала [1100].

11.   Напишете програма, която решава следните задачи:

-     Обръща последователността на цифрите на едно число.

-     Пресмята средното аритметично на дадена поредица от числа.

-     Решава линейното уравнение a * x + b = 0.

Създайте подходящи методи за всяка една от задачите.

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

Направете проверка на входните данни:

-     Десетичното число трябва да е неотрицателно.

-     Редицата не трябва да е празна.

-     Коефициентът a не трябва да е 0.

12.   Напишете метод, който събира два полинома с цели коефициенти, например (3x2 + x - 3) + (x - 1) = (3x2 + 2x - 4).

13.   Напишете метод, който умножава два полинома с цели коефициенти, например (3x2 + x - 3) * (x - 1) = (3x3 - 2x2 - 4x + 3).

Решения и упътвания

1.      Използвайте метод с параметър string. Ако ви е интересно, вместо да правите програма, която да тества дали даден метод работи коректно, можете да потърсите в Интернет информация за "unit testing" и да си напишете собствени unit тестове върху методите, които създавате. За всички добри софтуерни продукти се пишат unit тестове.

2.      Използвайте свойството Max(a, b, c) = Max(Max(a, b), c).

3.      Използвайте остатъка при деление на 10 и switch конструкцията.

4.      Методът трябва да приема като параметър масив от числа (int[]) и търсеното число (int).

5.      Елементите на първа и последна позиция в масива, ще бъдат сравнявани съответно само с десния и левия си съсед.

6.      Модифицирайте метода, имплементиран в предходната задача.

7.      Има два начина:

Първи начин: Нека числото е num. Докато num ≠ 0 отпечатваме последната му цифра (num % 10) и след това разделяме num на 10.

Втори начин: преобразуваме числото в string и го отпечатваме отзад напред чрез for цикъл.

8.      Трябва да имплементирате собствен метод за събиране на големи числа. На нулева позиция в масива пазете единиците, на първа позиция – десетиците и т.н. Когато събирате 2 големи числа, единиците на сумата ще е (firstNumber[0] + secondNumber[0]) % 10, десетиците ще са равни на (firstNumber[1] + secondNumber[1]) % 10 + (firstNumber[0] + secondNumber[0]) / 10 и т.н.

9.      Първо напишете метод, който намира максималния елемент в целия масив, и след това го модифицирайте да намира такъв елемент от даден интервал.

10.   Трябва да имплементирате собствен метод за събиране на големи цели числа, тъй като 100! не може да се събере в long. Можете да представите числата в масив в обратен ред, с по една цифра във всеки елемент. Например числото 512 може да се представи като {2, 1, 5}. След това умножението можете да реализирате, както сте учили в училище (умножавате цифра по цифра и събирате резултатите с отместване на разрядите).

Друг, по-лесен вариант да работите с големи числа като 100!, е чрез библиотеката System.Numerics.dll, която можете да използвате в проектите си като преди това добавите референция към нея. Потърсете информация в Интернет как да използвате библиотеката и класът System.Numerics.BigInteger.

11.   Създайте първо необходимите ви методи. Менюто реализирайте чрез извеждане на списък от номерирани действия (1 - обръщане, 2 - средно аритметично, 3 - уравнение) и избор на число между 1 и 3.

12.   Използвайте масиви за представяне на многочлените и правилата за събиране, които познавате от математиката. Например многочленът (3x2 + x - 3) може да се представи като масив от числата [-3, 1, 3]. Обърнете внимание, че е по-удачно на нулева позиция да поставим коефициентът пред x0 (за нашия пример -3), на първа – коефициентът пред x1 (за нашия пример 1) и т.н.

13.   Използвайте упътването от предходната задача и правилата за умножение на полиноми от математиката.

Демонстрации (сорс код)

Изтеглете демонстрационните примери към настоящата глава от книгата: Методи-Демонстрации.zip.

Дискусионен форум

Коментирайте книгата и задачите в нея във: форума на софтуерната академия.


10 отговора до “Глава 9. Методи”

  1. svetlio says:

    имам въпрос който не знам къде да задам за това ще го направя тук. Методите във всеки клас в C# на изуст ли трябва да се знаят ?и ако може да ме ориентирате към някой форум на сайта ви или на книгата ако има такъв 🙂

  2. nakov says:

    Всякакви въпроси около книгата за програмиране задавайте във форума: https://softuni.bg/forum/.

  3. Anonymous says:

    Видеото към тази тема не съответства. Същото е като Глава 12. Обработка на изключения

  4. Stivan says:

    moje li nqkoi da mi pomogne po c++ na edna zadacha- kakvo izvejda na ekraana slednoto : int f=0
    int i=(!f)? 5:7
    cout<<f<< i;

  5. Валентин says:

    i=5, ako !f==true,(f==false)
    i=7, ako !f==false,(f==true)

  6. Валентин says:

    int f=0 => f==false => i==5

  7. Валентин says:

    int f=0 => f==false => !f==true => i==5

  8. cvetka says:

    Имам един въпрос… Как да създам метод, който да извежда на конзолата подниз X, който е предшестван или последван от поднизове A и B.. Много интересна задача на C#, Но не мога да я направя. Всякакви предложения са добре дошли 🙂

  9. Николай says:

    Здравейте,
    Има грешка в “Решения и упътвания” т.8 е написано “собствен метод за УМНОЖЕНИЕ на големи числа” , вместо “собствен метод за СЪБИРАНЕ на големи числа”. Не съвпада с условието от “Упражнения” т.8.

Отговори на Svetlin Nakov

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