Skip to content

Latest commit

 

History

History
executable file
·
246 lines (166 loc) · 13.6 KB

Лекция-06.md

File metadata and controls

executable file
·
246 lines (166 loc) · 13.6 KB

Лекция №7

--

Протоколи

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

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

Синтаксис

protocol Sellable {
	var pricePerUnit: Double { get }
	
	var isAvailable: Bool { set get }
}

Пропъртита - properties

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

Гетъри - get

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

Сетъри и гетъри - Set & Get

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

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

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

Пример:

protocol Printable {
	var description: String { get }
	
	static var version: String { get }
}

Методи:

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

1. към инстанцията (методи към обект)

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

2. към типа (статични методи)

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

Пример:

protocol PersonalComputer {
	func getRamSize() -> Int
	
	// Convert X bytes to "KB" or "MB" or "GB" or "TB"
	static convert(bytes: Int, to:String) -> Double
}

Инициализиращи методи

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

Когато ги имплементираме, добавяме ключовата дума required

Пример:

protocol Printable {
	var description: String { get }
	static var version: String { get set}    
	
	init()
	
	init(a:Int, b:Int)
}
	
class Machine: Printable {
	var description = ""
	var powerConsumption = 0
	var name = "Missing name"
	static var version: String = "v. 2.0"

	//без този конструктор не се компилира
	required init() { }

	required init(a:Int, b:Int) {
		print("Machine")
	} 
}

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

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

protocol PersonalComputer: class {
	func getRamSize() -> Int
	static func convert(bytes: Int, to:String) -> Double
}

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

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

“Can-do” протоколи.

“Is-a” протоколи.

“Can-be” протоколи.

Can-do Протоколи

Те описват неща, които типът може да прави. Обикновено завършват на "-able", което ги прави лесни за разпознаване.

Например, когато тип имплементира Hashable протокола, то значи, че може да го хешираме до Int. Equatable and Comparable протоколите указват, че два обекта от този тип могат да бъдат сравнявани с операторите за еквивалентност (==) и сравнение (>/<).

Малка част от протоколите в тази група се грижат и за алтернативни изгледи за типовете. Например CustomPlaygroundQuickLookable - означава, че обект от този тип може да бъде разглеждат чрез функцията "quick look" на Playground, като за целта трябва да предоставим този изглед/формат.

Идеята е, че самата стойност остава същата, а погледнат "отвън", обектът е различен (неговата презентация). Операцията в случая е "надникване"/quick-look.

Is-a протоколи

Те описват какъв видът. За разлика от Can-do протоколите те по-скоро придават идентичност на типа.

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

Collection(Type), който Array, Dictionary и Set имплементират. И Sequence(Type) and Generator(Type) пък се ползват, ако искаме да придадем итеративност на дадена колекция (да може да се обхожда). Имаме и Error(Type), който се ползва в модела за грешките в Swift. За него имаме и цяла лекция.

Съществува и MirrorPath(Type), който има забавен коментар към документацията си:

/// A protocol for legitimate arguments to Mirror's descendant /// method. /// /// Do not declare new conformances to this protocol; they will not /// work as expected.

Can-be протоколи

Последната група протоколи от стандартната библиотека означава, че един тип може да бъде конвертиран от и към нещо друго. Обикновено имат имена, съдържащи ExpressibleBy...Literal (ExpressibleByNilLiteral, ExpressibleByStringLiteral и др.).

Има такива, които изискват имплементирането на един инициализатор, който връща конкретния тип, като например ExpressibleByFloatLiteral - при подаден Float, бихме могли да конструираме нашия тип; init(floatLiteral value: Self.FloatLiteralType)

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

В обобщение

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

Ето и някои общи модели, за които да мислим, пишейки своя код::

Операции - Ако има общ набор от операции, които бихме извършвали с/върху нашите типове, обмислете да ги дефинирате с протокол(и).

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

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

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

Разширения

Какво са разширенията?

Разширенията (extensions, в други езици - категории) позволяват добавянето на нова функционалност към тип, който вече е дефиниран. Могат да се използват за разширяване/допълване на функционалности - добавяне на пропъртита и функции, на типовете от стандартната Swift библиотека Int, String, Bool и т.н. или на дефинирани от нас типове. Разширенията могат да са полезни и за организиране на кода в логически свързани блокове. Най-често ще използваме разширения за добавяне на функционалности към типове, които не можем да редактираме директно и организиране на кода. Да се има предвид, че разширенията добавят функционалност към всички инстанции на този тип във вашата програма.

Синтаксис

Използваме запазената дума extension, последвана от името на типа, който искаме да разширим.

extension SomeType {
	// Code goes here
}

Добавяне на пропъртита (изчислими)

Добавяме статична променлива, която връща любимото ни цяло число:

extension Int {
	static var favoriteNumber: Int {
		return 23
	}
}

Добавяне на методи към инстанция

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

extension Int {
	func squared() -> Int {
		return self * self
	}
}

Органициране на кода

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

class Person {
	let firstName: String
	var lastName: String
	var age: Int
	var phoneNumber: String
}

extension Person {
	var fullName: String {
		return "\(firstName) \(lastName)"
	}
}

extension Person {
	func growOlder() {
		age += 1
	}
	
	func greet() {
		print("Hello, my name is \(fullName)")
	}
}

Имплементиране на протоколи

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

extension Person: Equatable {
	static func ==(lhs: Person, rhs: Person) -> Bool {
		return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.age == rhs.age && lhs.phoneNumber == rhs.phoneNumber
	}
}