--
Протоколът е "договор", който всеки тип данни (клас, структура или изброим тип) се съгласява да удовлетвори. "Договор", е списък от изисквания, които определят начина по който ще изглежда даденият тип.
protocol Sellable {
var pricePerUnit: Double { get }
var isAvailable: Bool { set get }
}
Протоколът може да задължава всички имплементиращи го да добавят пропъртита. Има два варианта:
--
Когато имаме гетър, тогава наложените ограничения са най-малки. Те не ни задължават да имаме сетър. Можем да имаме изчислимо пропърти или стандартно (set и get). Дори можем да имаме и read–only
пропърти.
--
В този случай трябва да имаме и двата метода - get
и set
. Т.е. ограниченията са такива, че можем да имаме или стандартно пропърти var
(то има гет и сет) или да имплементираме изчислимо, но и с двата му варианта, за да има set
и get
.
Като от всеки от горе изброените два варианта, може да се прилага към типа (static
) или към инстанцията - тук говорим за член пропъртита.
Трябва да знаем, че не можем да дадем имплементация по-подразбиране на статичните функции.
Пример:
protocol Printable {
var description: String { get }
static var version: String { get }
}
В протокола може да изискваме различни методи, като фиксираме името на метода, имената и типовете на параметрите (дали имат име или не) и типа на връщания резултат.
Това са методи, които ще са характерни за инстанция (обект) от типа, който имплементира протокола.
Това са методи, които са характерни за типа, който имплементира протокола. Тези функции (можем спокойно да си мислим и за пропъртита)
Пример:
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 библиотека в три групи:
“Can-do” протоколи.
“Is-a” протоколи.
“Can-be” протоколи.
Те описват неща, които типът може да прави. Обикновено завършват на "-able", което ги прави лесни за разпознаване.
Например, когато тип имплементира Hashable протокола, то значи, че може да го хешираме до Int. Equatable and Comparable протоколите указват, че два обекта от този тип могат да бъдат сравнявани с операторите за еквивалентност (==) и сравнение (>/<).
Малка част от протоколите в тази група се грижат и за алтернативни изгледи за типовете. Например CustomPlaygroundQuickLookable - означава, че обект от този тип може да бъде разглеждат чрез функцията "quick look" на Playground, като за целта трябва да предоставим този изглед/формат.
Идеята е, че самата стойност остава същата, а погледнат "отвън", обектът е различен (неговата презентация). Операцията в случая е "надникване"/quick-look.
Те описват какъв видът. За разлика от 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.
Последната група протоколи от стандартната библиотека означава, че един тип може да бъде конвертиран от и към нещо друго. Обикновено имат имена, съдържащи 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
}
}