Skip to content

Latest commit

 

History

History
executable file
·
265 lines (174 loc) · 24.7 KB

Лекция-03.md

File metadata and controls

executable file
·
265 lines (174 loc) · 24.7 KB

Лекция №3

--

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

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

Въпроси от предишната лекция

Следват няколко основни въпроса, които маркират основните ключови моменти от предишната лекция.

  • Какво е характерно за if оператор в swift?
  • Какво е условен оператор switch
  • Какво са последователности?
  • Кои са основните цикли?

Функции

Как се справяме с големите програми?

Преди време, когато компютрите са били големи чудовищини машини, отнемащи пространство повече от една нормална стая (~15 м. кв.), изчислителната им мощ е била хиляди пъти по-малка от тази на съвременните "умни" телефони. Размера на оперативната им памет е бил малък, много по-малък от 1 МB (за справка, днешните домашни компютри имат средно около 4 до 8 GB, 1GB = 1024 MB). Това налага ограничение на размера на програмите, които могат да се пишат, за да могат да се управляват от хардуера. Трудността в писането на компютърни програми по това време идва и от липсата на хубави програмни езици. Какво поражда липсата на хубави програмни езици по това време - липсата на компилатори. т.е. преди всичко се е пишело на по-ниско ниво. Отнемало много време и е било трудно да се следи за корекността на програмите, понеже кодът е бил сложен за четене и анализ от програмистта, но пък е бил по-лесен за превод към устройстовото. Днес инструкциите, до които се превежда написаният от нас код на Swift, се наричат инструкции на ниско ниво, асемблер и машинен език. Те остават скрити за съвременният широк кръг от програмисти, но това са били първите варианти на езиците за прогрмаиране. С течение на времето, сложността на програмите започва да нараства и нуждата от нови парадигми в програмирането ражда нови езици и начини на организране на код-а. От програмиране на Асемблер се минава към проргамни езици, които се компилират. Има движение, което издига функционалните езици. Това е друга категория програмни езици, които поритежават хубави логически свойства в сравнение с класическите императивни езици. Последните еволюират, като се появява Обектно ориентираният подход (C++). Можем да говорим много за историята на програмните езици, но е важно да отбележим, че се заражда едно движение за презиползване на вече написаните неща. Т.е. появава се процес по надграждане, какъвто има и в научните среди в различните индустрии. Натрупват се знания, които тласкат прогреса и човечеството напред.

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

Как се дефинират функции в Swift? Ще започнем с базов пример - функция, която изпълнява набор от стъпки. В последствие ще разширим примера докато достигнем пълния потенциал на функциите като изразно средство.

//Нека да напишем първата функция, която обединява няколко действия.
func severalActions() {
	print("Action 1")
	print("Action 2")
	print("Action 3")
}

//тук ще активираме нашата функция. Може да мислим, че я "извикваме" (call)
severalActions()
severalActions()

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

Ще опишем общия вариант на функция в Swift.

func functionName(labelName variableName:String) -> String {
	let returnedValue = variableName + " was passed"
	return returnedValue
}

//ето и извикването на функцията
functionName(labelName: "Nothing")

Всичко започва с думата func, която е запазената дума в Swift за деклариране на функция. След това следва името на функцията - functionName, като тук следваме правилата за име на променлива, като внимаваме да няма многозначност (т.е. да не се повтаря името с име на променлива и някоя друга запазена дума!). Продължаваме с параметрите, които може да са повече от един. Всеки параметър има име(label ), което се използва при извикване на функцията и име (variable), което се среща в тялото на функцията. Следва запазеният символ -> и типа на връщания резултат. После в { } е отделено тялото на функцията, като трябва да има return случай, който указва каква стойност ще връща функцията (може да е конкретна стойност от обявения по-горе тип, може да е и променлива или израз, стига оценяването му да дава правилния тип стойност).

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

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

//вижте разновидностите на функцията
	
//ако променим името на аргумента, swift счита това за нова различна функция. Т.е. имената на аргумента
func functionName(labelNameNew variableName:String) -> String {
	let returnedValue = variableName + " NEW was passed"
	return returnedValue
}

//когато изпуснем името на аргумента, името на променливата се изпозлва за негово име. т.е. двете съвпадат
func functionName(variableName:String) -> String {
	let returnedValue = variableName + " was passed. v2"
		return returnedValue
	}

Какво ни прави впечатление? Променливите имат имена.

Имената на променливите са нещо нетрадиционно за класическите езици като Java, C, C++. Ако искаме да симулираме старото поведение, трябва да използваме _(undescore) - долна черта (wildcard), за да маркираме името на параметара като празен. Вече сме запознати с този символ. Той влизаше в полза, когато запазваме стойността в променлива, която няма участие в кода, който следва. Подобна е и логиката тук. Не ни трябва име на параметъра.

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

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

1. функции без параметри, които връщат резултат

//функция, която връща резултат
func seven() -> Int {
	return 7
}
		
let s = seven()
print("Това е числото \(s).")

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

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

//сума на две числа
func printSum(a:Int, b:Int) {
	print("Sum \(a) + \(b) = \(a + b)")
}
		
//извикване на функцията
printSum(a:3, b:5)

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

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

3. функция с няколко резултата

За да върнем повече от един тип резултат, ние трябва да използваме N-торки (tuples). Това са пакетчета от няколко различни типа данни, които са окомплектовани в едно. Те може да имат имена, асоциирани с всеки член на N-торката. Ако такива имена липсват, тогава можем да използваме индексите. Данните в пакетчето се предават по стойност. Това означава, че те се копират в паметта (в следващата лекция ще разгледаме разликата между референтините типове (reference types) и тези, които се копират по стойност (value types)). Нека да видим няколко примера, за да знаем как да ги ползваме:

/* N-торки */
let person = (name: "Иван", familyName: "Петров", age: 25)
let p:(String, String, age: Int)? = person
print("Здравей, \(person.name)!")
		
print("Здравей, г-н \(person.familyName)!")
		
print("Здравей, г-н \(person.1)!")
if let pp = p, pp.age > 20 {
	print("Г-н \(pp.1), Вие сте на \(pp.age) години.")
}

Ето и пример за функция, която ги използва активно.

func maxItemIndex(numbers:[Int]) -> (item:Int, index:Int) {
    	var index = -1
	var max = Int.min
    
	for (i, val) in numbers.enumerated() {
		if max < val {
	          	max = val
          		index = i
        	}
	}
    	
    	return (max, index)
}
		
let maxItemTuple = maxItemIndex(numbers: [12, 2, 6, 3, 4, 5, 2, 10])
if maxItemTuple.index >= 0 {
	print("Max item is \(maxItemTuple.item).")
}

4. функция с inout параметри

func updateVar(oldVariable x: inout Int, newValue: Int = 5) {
	x = newValue
}

var ten = 10
print(ten)
updateVar(oldVariable: &ten, newValue: 15)
print(ten)

В примера по-горе виждаме още един начин как да връщаме "резултат" от функция. Трябва да спазим някои основни правила. inout се поставя преди типа параметъра x: inout Int, когато дефинираме функцията. В момента на извикване на функцията можем да подаваме само променливи където се изискват inout параметри. Това е нужно, за да може функцията да запише стойността в тази част от паметта. Това се отбелязва визуално с & пред името на променливата.

Интересно е да се отбележи, че всички променливи, които са част от декларацията на функцията и не са inout, са константи. Т.е. не може в тялото на функцията да бъде присвоена стойност, освен ако не са обявени като inout. Ето и един пример:

var sumAB:Int = 0
sum(a:3, b:4, in:&sumAB)

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

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

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

Оператори

prefix и postfix

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

-prefix, ако операторът се изписва преди името на инстанцията на класа или структурата (Например: +x)

-postfix, ако операторът се изписва след името на инстанцията на класа или структурата (Например: x+).

infix

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

    extension String {
        static func * (left: String, right: Int) -> String {
            if  right <= 0 {
                return ""
            }
            
            var result = left
            for _ in 1..<right {
                result += left
            }
            return result
        }
    }

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

    let someString = "hi "
    let newString = someString * 3
    //Стойността на newString е "hi hi hi "

Оператори от тип prefix и postfix

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

-prefix, ако операторът се изписва преди името на инстанцията на класа или структурата (Например: +x)

-postfix, ако операторът се изписва след името на инстанцията на класа или структурата (Например: x+).

Оператори за комбинирано присвояване

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

Оператори за сравнение

Класовете и структурите, които ние създаваме, не имплементират оператори за сравнение като (==) и (!=). Това е, защото Swift няма как да прецени какво за нас е важно при сравнението им. Имплементацията на този тип оператори се изписва по същия начин както при операторите от тип infix.

Персонални оператори

В Swift можете да добавяте и персонални оператори. Те се декларират на глобално ниво чрез запазената дума operator и могат бъдат от тип prefix, postfix и infix.

Вашите оператори могат да използват следните ASCII символи: "/", "=", "-", "+", "!", "*", "%", "<", ">", "&", "|", или "~". Също така и Unicode символи от "Mathematical operators" сета.

Приоритети на операторите

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

Структури

Примери: