Skip to content

Latest commit

 

History

History
executable file
·
606 lines (433 loc) · 39.2 KB

Лекция-04.md

File metadata and controls

executable file
·
606 lines (433 loc) · 39.2 KB

Лекция №4

--

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

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

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

  • Какво помним за функциите в swift?

    • за имената
    • за параметрите
    • за връщаните типове
    • за ?
  • Какви видове оператори има?

Комбинирани типове данни

До сега се запознахме с основните типове данни в програмния език Swift - Int, Double, String, Bool, Array. Това ни дава възможността да решаваме много задачи, но е време да въведем по-сложни типове данни, които ще ни позволят да моделираме сложни системи от реалността с лекота в нашите програми. Например, за да реализираме система за онлайн магазин - ние трябва да можем да представим всеки един продукт. Един продукт има различни характеристики. Нека да изброим някои основни - име на продукта (String), цена на бройка (Double), наличен ли е или не (Bool). Със знанията ни до момента най-близкият начин за моделиране е да използваме N-торките (вързопчетата с данни от различни типове). Нека да въведем един възможен начин да дефинираме сложен тип от данни. Такъв който да има няколко характеристики и възможни операции над него.

Структури

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

Структурите са сложен тип от данни, който обединява няколко различни типа данни (характеристи) и дава възможност за лесно боравене с тях. До сега сме се запознали с N-торките (tuples), но към структурите можем да добавяме операции, т.е. можем да определяме функции, които са част от интерфейса на структура, която да борави с данните в самата структура. Функциите, които познаваме до момента, могат да изпълняват тези задачи, но е много по-лесно и интуитивно да се разсъждава от гледна точка на структурата в сравнение с глобалната гледна точка, която е типична за глобалните функции.

При някои програмни езици декларацията на интерфейса на структура и дефиницията (логиката и организацията) стоят в различни файлове. Това не е характерно за Swift. Тук всичко се намира в един файл.

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

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

struct Merchandise {
	var name: String
	var pricePerUnit: Double
	var isAvailable: Bool
}

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

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

var phone:Merchandise = Merchandise(name:"Nokia", pricePerUnit:200, isAvailable:false)

func printInfoFor(merchandise:Merchandise) {
	print("Product : \(merchandise.name) - 
	\(merchandise.pricePerUnit) - 
	\(merchandise.isAvailable ? "available" : "unavailable")")
}

printInfoFor(merchandise: phone)
//Product : Nokia - 200.0 - unavailable

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

Създаваме променлива phone от вече дефинираната структура Merchandise (стока). В помощната функция printInfoFor(merchandise:Merchandise) ние използваме нотацията с . merchandise.name, за да достъпим името на стоката.

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

Не можем да създадем променлива от типа Merchandise, която да не е инициализирана без да подадем данни. Това е характерно за Swift - всяко поле трябва да има стойност.

Тази специална функция ще наричаме конструктор или init метод. Метод за инициализиране на променливи от този тип.

Когато опишем полетата (елементите) на една структура, тогава имаме неявен конструктор, който включва всички полета като аргументи. Ето какво получваме на готово в случая с нашия тип:

init(name: String, pricePerUnit: Double, isAvailable:Bool) {
	self.name = name
	self.pricePerUnit = pricePerUnit
	self.isAvailable = isAvailable
}

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

Какво означава self?

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

init функция ли е?

Приемаме, че 'init' е специална функция (както я определихме по-горе - конструктор). Тя няма тип на резултата, който връща и не използва запазената дума func. Можем да имаме много разновидности на init.

Защо не извикваме явно функцията init?

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

struct Merchandise {
	var name: String
   	var pricePerUnit: Double
	var isAvailable: Bool
    
    init(name: String, pricePerUnit: Double, isAvailable:Bool) {
		self.name = name
   	   	self.pricePerUnit = pricePerUnit
   		self.isAvailable = isAvailable 
	}
}

Ако искаме да имаме конструктор без параметри, трябва да декларираме такъв. Това е друга "версия" на конструктора - такава без параметри. Ето как можем да направим това:

init() {
	self.name = "Noname"
	self.pricePerUnit = 0
	self.isAvailable = false
}

Нужно да се отбележи, че всяко поле трябва да има стойност или да му бъде присвоена такава в конструктора init метода. Изключение са полетата със стойност по подразбиране. Можем да дадем такава на всяко поле в нашата структура. Ето как можем да си спестим конструктора без параметри:

struct Merchandise {
	var name: String = "noname"
   	var pricePerUnit: Double = 0.0
	var isAvailable: Bool = false
}

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

var newPhone:Merchandise = Merchandise()
		
printInfoFor(merchandise: newPhone)
//Product : Noname - 0.0 - unavailable

Логично е да си зададем въпроса: ако имаме инициализиране, какво става, когато освобождаваме паметта. Процеса можем да наричаме деинициализация. Структурите нямат деинициализиращ метод deinit. Ако се опитате да декларирате такъв, компилаторът ще ви подскаже, че това е метод, който се среща само при класовете. Така че, когато ползваме структури, трябва да мислим за тяхната иницализация, а всичко останало оставяме на Swift.

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

Ще добавим функция printInfo, която отпечатва данните за един продукт. Ще взаимстваме логиката от вече познатата ни функция func printInfoFor(merchandise:Merchandise)

func printInfo() {
	print("Product : \(name) - \(pricePerUnit) - \(isAvailable ? "available" : "unavailable")")
}

Цялостния вид на нашата структура е следния:

struct Merchandise {
	var name: String
	var pricePerUnit: Double
	var isAvailable: Bool
    
	init() {
		self.name = "Noname"
		self.pricePerUnit = 0
		self.isAvailable = false
    	}
    	
	init(name: String, pricePerUnit: Double, isAvailable:Bool) {
		self.name = name
		self.pricePerUnit = pricePerUnit
		self.isAvailable = isAvailable
    	}
    
	func printInfo() {
		print("Product : \(name) - \(pricePerUnit) - \(isAvailable ? "available" : "unavailable")")
	}
}

Нека сега да сравним тази функция с горната. Нямаме резултат, който да връщат. Това е еднакво. Едната взима параметър, а другата не взима. Това е така, защото функцията, която е част от структурата, може да оперира с данните в структурата. Това обяснява защо и от къде идват полетата name, pricePerUnit, isAvailable, респективно данните в тях. Да, можем да използваме self пред всяко поле, т.е. self.name и name означават едно и също.

Множеството от полета и функции (операции) над структурата определят интерфейса за боравене с този тип данни.

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

func maxValue(params numbers:Int...) -> Int {
	var max = Int.min
	for v in numbers {
		if max < v {
			max = v
		}
	}
    
	return max
}

Можем и да връщаме резултат от определен тип.

Нека направим леко отклонение и да разясним някои базови типове като Array. Това е колекция от еднакви по тип данни.

Ето пример за списък от цели числа:

let integers = [1, 2, 3]
print(integers)
let reversed = integers.reversed()
//print(reversed) //this is from strange type
print([Int](reversed))

//[1, 2, 3]
//[3, 2, 1]

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

Предаване по стойност

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

Какво е предаване по стойност?

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

var a = 1
var b = a
print("Initial values:")
print("a = \(a)")
print("b = \(b)")
	
a += 5
print("Modify a += 5.")
print("a = \(a)")
print("b = \(b)")
	
func modify(value:inout Int) {
	value = 3
}
	
print("Example with functions.")
print("b = \(b)")
modify(value: &b)
print("Modify b = 3.")
print("b = \(b)")
	
//изход в конзолата:
//Initial values:
//a = 1
//b = 1
//Modify a += 5.
//a = 6
//b = 1
//Example with functions.
//b = 1
//Modify b = 3.
//b = 3

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

var nokia = Merchandise(name: "Nokia 3310", pricePerUnit: 100, isAvailable: false)
nokia.printInfo()
var nokiaDiscounted = nokia
nokiaDiscounted.pricePerUnit *= (100.0 - 20.0) / 100.0 // -20%
nokiaDiscounted.isAvailable = true
nokiaDiscounted.printInfo()
nokia.printInfo() 

Какво остава да научим за структурите?

Subscripts - достъп до елементи, използвайки синтаксиса []

Индексирането е лесен начин за достъп до елементите на колекция, списък или последователност от елементи. Употребява се при работата с колекции от стандартните типове, като масив или речник(dictionary).

var goods = [Merchandise(), Merchandise(), Merchandise()]
goods[1].isAvailable = true // достъп до първия елемент

Класическата форма е:

subscript (index:Int) -> Any {
	get {
    		//връщаме подходяща стойност 
   	}
        
	set(newValue) {
    		//променяме данните в структурата
    	}
}

Имаме get и set част. Като различна част от кода се задейства в зависимост от това дали ще присвояваме стойност с = или ще използваме стойността връщана от get. Можем да дефинираме произволни форми, които взимат параметър от тип различен от цяло число.

Примерно:

subscript (index:(Int,Int)) -> Any {
   	return "tuple nothing"
}

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

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

Extensions или разширения

Разширенията (Extensions) дават възможност да добавяме допълнителна функционалност към структури и класове. Можем да добавим:

  • методи към обекта (функции) или към типа данни (static)
  • да дефинираме достъп чрез subscript - []
  • добавяне на вложените типове

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

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

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

Ето и пример:

let g1 = Merchandise()
//това предизвиква грешка 
g1.name = "New name" //note: change 'let' to 'var' to make it mutable

Модифициращи методи

Структурите имат член данни, които можем да достъпваме с точкова нотация newPhone.printInfo(). Ако искаме да напишем функция, която модифицира член данните, трябва да използваме запазената дума mutating, за да подскажем на компилатора, че дадената функция ще модифицира структурата, използвайки self. Това става, като слагаме mutating преди func.

Пропъртита

Член данните които дефинираме във нашата структура се наричат пропъртита. Те са една от основните черти на интерфейса определящ нашата структура. Такива пропъртита можем да наричаме член данни или пропъртита съхраняващи данни. Това не е единствения вид пропъртита.

Всички пропъртита можем да ги достъпваме с точковата нотация.

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

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

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

Пропъртита, които са част от обект, дефинираме без да добавим ключовата дума static. Ако я използваме, тогава пропъртита стават част от типа (от клас или структурата).

Изчислими пропъртита

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

Изчислимото пропърти е добавено в отделно разширение.

extension Merchandise {
 	//изчислими пропъртита
   	var incomePerUnit:Double {
    		get {
    			return self.pricePerUnit * 0.2
		}
	}
}
	
var macBookPro = Merchandise(name:"MacBook Pro 15\"", pricePerUnit: 3200.0, isAvailable: true)
print("Income per product: \(macBookPro.incomePerUnit)")

Изчислими пропъртита с допълнителна логика

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

extension Merchandise {	
	var realPricePerUnit:Double {
		get {
	    		return self.pricePerUnit * 0.8
	    	}
	        
	    	set (newValue) {
	    		self.pricePerUnit = newValue * 1.25
	    	}
	}
}

Има и къс вариант на тази дефиниция:

extension Merchandise {	
	var realPricePerUnit:Double {
		get {
	    		return self.pricePerUnit * 0.8
	    	}
	        
		set {
	    		//newValue е името на параметъра. Типът е Double
	        	self.pricePerUnit = newValue * 1.25
	    }
	}
}

Когато имаме само get метод, тогава говорим за read–only пропъртита. Такива, които можем само да използваме като част от израз, но не и да им присвояваме някаква стойност. Такъв пример имаме по-горе incomePerUnit. Можем да използваме по-кратък синтаксис, за да го запишем така:

extension Merchandise {
    
	//изчислими пропъртита
	var incomePerUnit:Double {
    		return self.pricePerUnit * 0.2
   	}
}

Мързеливи пропъртита

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

struct LazyStruct {
	var count: Int
	init (count:Int) {
		print("\(LazyStruct.self) се конструира чрез -> \(#function)")
		self.count = count
	}
}

struct NormalStruct {
	var count: Int
	init (count:Int) {
		print("\(NormalStruct.self) се конструира чрез -> \(#function)")
		self.count = count
	}
}
	
struct ExampleLazyProperty {
	lazy var s:LazyStruct = LazyStruct(count: 5)
	var normal:NormalStruct = NormalStruct(count: 10)
	var regularInt = 5
	    
	init() {
		print("\(ExampleLazyProperty.self) се конструира чрез -> \(#function)")
	}
}
	
var lazyPropExample = ExampleLazyProperty()
lazyPropExample.regularInt = 15
print("Стойноста в нормалното пропърти 'regularInt' e \(lazyPropExample.regularInt)")
print("Стойноста в нормалното пропърти 'normal' e \(lazyPropExample. normal.count)")
print("Стойноста на мързеливото пропърти е \(lazyPropExample.s.count)")
print("Стойноста в нормалното пропърти 'regularInt' е \(lazyPropExample.regularInt)")

Ако изпълним примера, виждаме че пропъртито s от тип LazyStruct не се инициализира незабавно след като сме инициализирали обекта.

Наблюдатели на пропъртита

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

Може да се добавят наблюдатели към всички видове пропъртита освен към мързеливите - lazy.

Има два основни вида наблюдатели:

  • willSet - извиква се преди да се запише новата стойност. Позволява ни да направим промени преди записване на стойността
  • didSet - извиква се след записване на новата стойност. Позволява ни да направим промени след запива на новата стойност.
struct Merchandise {
    var name: String
    var pricePerUnit: Double {
        willSet {
            print("Сменяме цената с нова \(newValue)")
            print("Старата цена е \(pricePerUnit)")
        }
        didSet(oldPrice) {
            if oldPrice > pricePerUnit {
                print("Намаление!")
            } else {
                print("Всичко поскъпва!")
            }
        }
    }
    var isAvailable: Bool
}

Можем да именуваме променливата, която получава в наблюдателите. Съществена е разликата, че когато сме в willSet променливата съдържа новата стойност, а чрез пропъртито можем да достъпим старта. В didSet нещата стоят огледално. Новата стойност е вече в пропъртита, а старта е в параметъра.

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

Видимост на член-данните и функциите

Чрез разлини модификатори можем да ограничим нивото на достъп до части от нашите типове.

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

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

Модул е едно приложение. Модул е и един фреймуорк (framework).

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

Често срещана практика е да се разделя дефиницията в няколко файла чрез extensions.

Нивата на достъп са пет, като ще ги изброим от най-отворените (най-свободните) към най-затворените (най-ограничените):

  • open - използва се когато се дефинира интерфейса на framework. Разликата между open и public е в това, че класовете маркирани като open могат да се наследяват извън рамките на един модул.
  • public - използва се когато се дефинира интерфейс на framework и позовлява директното изпозлване на типа данни. Не не е позволено да се наследяват извън рамките на модула. Наследяването е възможно само в рамките на текущия модул. Това е валидно и за всички по-ограничени нива на достъп.
  • internal - ограничава достъпа до рамките на текущия модул. Изполва се за дефиниране на вътрешната структура и скриване на детайлите и от външните модули. Това е нивото за достъп, което се подразбира, ако не са ползвани други модификатори.
  • fileprivate - достъпът е ограничен само до нивото на файлва, в който е дефинира типа или функцията. Може да се използва за да се скрие вътрешната организация на даден тип за остналата част от модула.
  • private - пълно информационно скриване. Всичко извън рамките на дадената дефиниця няма достъп.

Самите нива за достъп от горе open, public, internal, fileprivate и private са ключови думи, които използваме за да дефинираме нивото на достъп в типвое данни и тяхните член данни или методи.

internal struct Merchandise {
	internal var name: String
	public var pricePerUnit: Double 
	fileprivate var isAvailable: Bool
	
	private var realPricePerUnit:Double {
        get {
                return self.pricePerUnit * 0.8
            }
            
            set (newValue) {
                self.pricePerUnit = newValue * 1.25
            }
    }
    
    public func printInfo() {
        print("Product : \(name) - \(pricePerUnit) - \(isAvailable ? "available" : "unavailable")")
    }
}

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

Дизйана на API (на публичните интерфейс) не е лесна задача и трябва да се обмисля детайлно преди да се финализира.

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

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

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

	internal func printInfo(m: Merchandise) { 
		//...
	}

Ако се опитаме да ползваме public или open кода няма да се компилира.

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

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

При стандартните пропъртита:

Може да намаляваме нивото на достъп за setter и да имаме друго ниво да за достъп на getter-ите.

Това е възможно дори и при нормалните член данни, където компилатора ни генерира set и get метод. Става с експлицитно обявяване на нивото на достъп internal(set).

struct Merchandise {
    public var name: String
    fileprivate(set) public var pricePerUnit: Double
    private(set) public var isAvailable: Bool = false
}

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

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

Полезни ресурси

Ето къде можем да прочетем повече за дизайна на API-та и генерализирането на функции

Тук може да намерим примерни playground

Задачи върху рекурсия

  1. Бързо повдигане на степен?

  2. Задачи с рекурсия върху символни низове?

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

     $$$$$$$$$$^^^^^ 
     $$$$$оооо^^^^^^
     $$$$$оо^^^^хххх
     ххххххххххххххх
     ххххххххххххххх 
     
     отговор: 4 	
    

Решения на задачите може да намерите в репозиторито, в секцията playgrounds.