Протокольно-ориентированное программирование и дженерики

Посмотреть на TensorFlow.org Посмотреть исходный код на GitHub

В этом руководстве будет рассмотрено программирование, ориентированное на протоколы, а также различные примеры того, как их можно использовать с дженериками в повседневных примерах.

Протоколы

Наследование — это мощный способ организации кода на языках программирования, который позволяет совместно использовать код между несколькими компонентами программы.

В Swift существуют разные способы выражения наследования. Возможно, вы уже знакомы с одним из таких способов из других языков: наследованием классов. Однако у Swift есть другой путь: протоколы.

В этом уроке мы рассмотрим протоколы — альтернативу созданию подклассов, которая позволяет достигать аналогичных целей за счет различных компромиссов. В Swift протоколы содержат несколько абстрактных членов. Классы, структуры и перечисления могут соответствовать нескольким протоколам, и отношения соответствия могут быть установлены задним числом. Все это позволяет реализовать некоторые проекты, которые нелегко выразить в Swift с помощью подклассов. Мы рассмотрим идиомы, поддерживающие использование протоколов (расширения и ограничения протоколов), а также ограничения протоколов.

Типы значений Swift 💖!

Помимо классов, имеющих ссылочную семантику, Swift поддерживает перечисления и структуры, передаваемые по значению. Перечисления и структуры поддерживают множество функций, предоставляемых классами. Давайте взглянем!

Во-первых, давайте посмотрим, чем перечисления похожи на классы:

enum Color: String {
    case red = "red"
    case green = "green"
    case blue = "blue"
    // A computed property. Note that enums cannot contain stored properties.
    var hint: String {
        switch self {
            case .red:
                return "Roses are this color."
            case .green:
                return "Grass is this color."
            case .blue:
                return "The ocean is this color."
        }
    }

    // An initializer like for classes.
    init?(color: String) {
        switch color {
        case "red":
            self = .red
        case "green":
            self = .green
        case "blue":
            self = .blue
        default:
            return nil
        }
    }
}

// Can extend the enum as well!
extension Color {
    // A function.
    func hintFunc() -> String {
        return self.hint
    }
}

let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")

let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color.
is invalidColor nil: true

Теперь давайте посмотрим на структуры. Обратите внимание, что мы не можем наследовать структуры, но вместо этого можем использовать протоколы:

struct FastCar {
    // Can have variables and constants as stored properties.
    var color: Color
    let horsePower: Int
    // Can have computed properties.
    var watts: Float {
       return Float(horsePower) * 745.7
    }
    // Can have lazy variables like in classes!
    lazy var titleCaseColorString: String = {
        let colorString = color.rawValue
        return colorString.prefix(1).uppercased() + 
               colorString.lowercased().dropFirst()
    }()
    // A function.
    func description() -> String {
        return "This is a \(color) car with \(horsePower) horse power!"
    }
    // Can create a variety of initializers.
    init(color: Color, horsePower: Int) {
        self.color = color
        self.horsePower = horsePower
    }
    // Can define extra initializers other than the default one.
    init?(color: String, horsePower: Int) {
        guard let enumColor = Color(color: color) else {
            return nil
        }
        self.color = enumColor
        self.horsePower = horsePower
    }
}

var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power!
Horse power in watts: 186425.0
Red

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

// Notice we have no problem modifying a constant class with 
// variable properties.
class A {
  var a = "a"
}

func foo(_ a: A) {
  a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)

/* 
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/

// func modify(car: FastCar, toColor color: Color) -> Void {
//   car.color = color
// }

// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a
foo

Давайте использовать протоколы

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

protocol Car {
    var color: Color { get set }
    var price: Int { get }
    func turnOn()
    mutating func drive()
}

protocol Electric {
    mutating func recharge()
    // percentage of the battery level, 0-100%.
    var batteryLevel: Int { get set }
}

protocol Gas {
    mutating func refill()
    // # of liters the car is holding, varies b/w models.
    var gasLevelLiters: Int { get set }
}

В объектно-ориентированном мире (без множественного наследования) вы могли создать абстрактные классы Electric и Gas , затем использовать наследование классов, чтобы оба наследовались от Car , а затем сделать конкретную модель автомобиля базовым классом. Однако здесь оба совершенно отдельные протоколы с нулевой связью! Это делает всю систему более гибкой в ​​плане ее проектирования.

Давайте определим Теслу:

struct TeslaModelS: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)

Это определяет новую структуру TeslaModelS , которая соответствует обоим протоколам Car и Electric .

Теперь давайте определим бензиновый автомобиль:

struct Mustang: Car, Gas{
    var color: Color
    let price: Int
    var gasLevelLiters: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Time to drive!")
        gasLevelLiters -= 1
    }

    mutating func refill() {
        print("Filling the tank...")
        gasLevelLiters = 25
    }
}

var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)

Расширьте протоколы поведением по умолчанию

Из примеров можно заметить, что у нас есть некоторая избыточность. Каждый раз, когда мы заряжаем электромобиль, нам необходимо установить процентный уровень заряда батареи равным 100. Поскольку все электромобили имеют максимальную емкость 100%, но бензиновые автомобили различаются в зависимости от емкости бензобака, мы можем по умолчанию установить уровень 100 для электромобилей. .

Вот тут-то и могут пригодиться расширения в Swift:

extension Electric {
    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

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

Протокол комиксов

Спасибо Рэю Вендерлиху за комикс!

Однако следует обратить внимание на следующее. В нашей первой реализации мы определяем foo() как реализацию по умолчанию для A , но не делаем это обязательным в протоколе. Поэтому, когда мы вызываем a.foo() , мы получаем напечатанное значение « A default ».

protocol Default {}

extension Default {
    func foo() { print("A default")}
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
A default

Однако, если мы сделаем foo() обязательным для A , мы получим « Inst »:

protocol Default {
    func foo()
}

extension Default {
    func foo() { 
        print("A default")
    }
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
Inst

Это происходит из-за разницы между статической отправкой в ​​первом примере и статической отправкой во втором для протоколов Swift. Для получения дополнительной информации обратитесь к этому сообщению на Medium .

Переопределение поведения по умолчанию

Однако, если мы захотим, мы все равно можем переопределить поведение по умолчанию. Важно отметить, что это не поддерживает динамическую отправку .

Допустим, у нас есть старая версия электромобиля, поэтому работоспособность аккумулятора снижена до 90%:

struct OldElectric: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func reCharge() {
        print("Recharging the battery...")
        batteryLevel = 90
    }
}

Использование протоколов в стандартной библиотеке

Теперь, когда у нас есть представление о том, как работают протоколы в Swift, давайте рассмотрим несколько типичных примеров использования протоколов стандартной библиотеки.

Расширить стандартную библиотеку

Давайте посмотрим, как мы можем добавить дополнительную функциональность к типам, которые уже существуют в Swift. Поскольку типы в Swift не встроены, а являются частью стандартной библиотеки как структуры, это легко сделать.

Давайте попробуем выполнить двоичный поиск по массиву элементов, а также проверим, что массив отсортирован:

extension Collection where Element: Comparable {
    // Verify that a `Collection` is sorted.
    func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
        var i = index(startIndex, offsetBy: 1)

        while i < endIndex {
            // The longer way of calling a binary function like `<(_:_:)`, 
            // `<=(_:_:)`, `==(_:_:)`, etc.
            guard order(self[index(i, offsetBy: -1)], self[i]) else {
                return false
            }
            i = index(after: i)
        }
        return true
    }

    // Perform binary search on a `Collection`, verifying it is sorted.
    func binarySearch(_ element: Element) -> Index? {
        guard self.isSorted(<=) else {
            return nil
        }

        var low = startIndex
        var high = endIndex

        while low <= high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)

            if self[mid] == element {
                return mid
            } else if self[mid] < element {
                low = index(after: mid)
            } else {
                high = index(mid, offsetBy: -1)
            }
        }

        return nil
    }
}

print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2
1
2

Мы делаем это, расширяя протокол Collection , который определяет «последовательность, элементы которой могут быть пройдены несколько раз неразрушающим образом и доступны с помощью индексированного индекса». Поскольку массивы можно индексировать с использованием квадратных скобок, именно этот протокол мы хотим расширить.

Аналогично, мы хотим добавить эту служебную функцию только к массивам, элементы которых можно сравнивать. По этой причине у нас есть where Element: Comparable .

where является частью системы типов Swift, которую мы скоро рассмотрим, но вкратце позволяет нам добавлять дополнительные требования к расширению, которое мы пишем, например, требовать, чтобы тип реализовывал протокол, чтобы требовалось, чтобы два типа были то же самое или потребовать, чтобы класс имел определенный суперкласс.

Element — это связанный тип элементов в типе, соответствующем Collection . Element определен в протоколе Sequence , но поскольку Collection наследуется от Sequence , он наследует связанный тип Element .

Comparable — это протокол, определяющий «тип, который можно сравнивать с помощью реляционных операторов < , <= , >= и > ». . Поскольку мы выполняем двоичный поиск в отсортированной Collection , это, конечно, должно быть правдой, иначе мы не знаем, следует ли рекурсивно/итерировать влево или вправо при двоичном поиске.

В качестве примечания о реализации: дополнительную информацию об использованной функции index(_:offsetBy:) можно найти в следующей документации .

Дженерики + протоколы = 💥

Обобщения и протоколы могут стать мощным инструментом, если их правильно использовать и избегать дублирования кода.

Во-первых, просмотрите еще одно руководство, A Swift Tour , в котором кратко рассматриваются дженерики в конце книги Colab.

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

Если к одному типу предъявляется несколько требований, например, тип соответствует нескольким протоколам, в вашем распоряжении есть несколько вариантов:

typealias ComparableReal = Comparable & FloatingPoint

func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
    return a > b
}

func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
    return a > b
}

func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
    return a > b
}

func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
    return a > b
}

func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
    return a > b
}

print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false
false
false
false
false

Обратите внимание на использование typealias вверху. Это добавит в вашу программу именованный псевдоним существующего типа. После объявления псевдонима типа псевдоним можно использовать вместо существующего типа повсюду в вашей программе. Псевдонимы типов не создают новые типы; они просто позволяют имени ссылаться на существующий тип.

Теперь давайте посмотрим, как мы можем использовать протоколы и дженерики вместе.

Давайте представим, что мы компьютерный магазин со следующими требованиями к любому ноутбуку, который мы продаем, для определения того, как мы их организуем в задней части магазина:

enum Box {
    case small
    case medium
    case large
}

enum Mass {
    case light
    case medium
    case heavy
}

// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

Однако у нас появилось новое требование группировать Laptop по массе, поскольку полки имеют ограничения по весу.

func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
    return laptops.filter { $0.mass == mass }
}

let laptops: [Laptop] = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]

Однако что, если мы хотим фильтровать по чему-то другому, кроме Mass ?

Один из вариантов — сделать следующее:

// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
    associatedtype Device
    func shouldKeep(_ item: Device) -> Bool
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
    typealias Device = Laptop
    var box: Box 

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.box == box
    }
}

struct MassFilter: DeviceFilterPredicate {
    typealias Device = Laptop  
    var mass: Mass

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.mass == mass
    }
}

// Make sure our filter conforms to `DeviceFilterPredicate` and that we are 
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
    _ laptops: [Laptop], 
    by filter: F
) -> [Laptop] where Laptop == F.Device {
    return laptops.filter { filter.shouldKeep($0) }
}

// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)]
[(c medium heavy)]

Потрясающий! Теперь мы можем фильтровать данные на основе любого ограничения ноутбука. Однако мы можем фильтровать только Laptop .

А как насчет возможности фильтровать все, что находится в коробке и имеет массу? Возможно, этот склад ноутбуков будет использоваться и для серверов с другой клиентской базой:

// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
    var mass: Mass { get }
}

protocol Boxed {
    var box: Box { get }
}

// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

struct Server: CustomStringConvertible, Boxed, Weighable {
    var isWorking: Bool
    var name: String
    let box: Box
    let mass: Mass

    var description: String {
        if isWorking {
            return "(working \(self.name) \(self.box) \(self.mass))"
        } else {
            return "(notWorking \(self.name) \(self.box) \(self.mass))"
        }
    }
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
    var box: Box 

    func shouldKeep(_ item: T) -> Bool {
        return item.box == box
    }
}

struct MassFilter<T: Weighable>: DeviceFilterPredicate {
    var mass: Mass

    func shouldKeep(_ item: T) -> Bool {
        return item.mass == mass
    }
}

// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
    _ elements: [T], 
    by filter: F
) -> [T] where T == F.Device {
    return elements.filter { filter.shouldKeep($0) }
}


// Let's test the function out!
let servers = [
    Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
    Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
    Server(isWorking: true, name: "serverC", box: .large, mass: .light),
    Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
    Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]

let products = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))

print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)]
[(notWorking serverB medium medium)]
[(a small light)]
[(b large medium)]

Теперь мы можем фильтровать массив не только по любому свойству определенной struct , но и по любой структуре, имеющей это свойство!

Советы по хорошему дизайну API

Этот раздел был взят из доклада WWDC 2019: Modern Swift API Design .

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

  • Начните с конкретных случаев использования:
    • Сначала изучите вариант использования с конкретными типами и поймите, какой код вы хотите поделиться и какой код повторяется. Затем учтите этот общий код с помощью дженериков. Это может означать создание новых протоколов. Откройте для себя потребность в универсальном коде.
  • Рассмотрите возможность создания новых протоколов из существующих протоколов, определенных в стандартной библиотеке. Хороший пример этого можно найти в следующей документации Apple .
  • Вместо универсального протокола рассмотрите возможность определения универсального типа.

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

Допустим, мы хотим определить протокол GeometricVector для чисел с плавающей запятой, который будет использоваться в каком-то приложении по геометрии, которое мы создаем, которое определяет 3 важные векторные операции:

protocol GeometricVector {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Допустим, мы хотим сохранить размеры вектора, в чем нам может помочь протокол SIMD , поэтому мы сделаем наш новый тип уточненным протоколом SIMD . Векторы SIMD можно рассматривать как векторы фиксированного размера, которые очень быстры при использовании их для выполнения векторных операций:

protocol GeometricVector: SIMD {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Теперь давайте определим реализации по умолчанию вышеперечисленных операций:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

А затем нам нужно добавить соответствие каждому из типов, к которым мы хотим добавить эти способности:

extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }

Этот трехэтапный процесс определения протокола, присвоения ему реализации по умолчанию и последующего добавления соответствия нескольким типам довольно повторяется.

Был ли протокол необходим?

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

Определение его в расширении SIMD

Если мы напишем три оператора в расширении протокола SIMD , это может решить проблему более кратко:

extension SIMD where Scalar: FloatingPoint {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

Используя меньше строк кода, мы добавили все реализации по умолчанию ко всем типам SIMD .

Иногда у вас может возникнуть соблазн создать такую ​​иерархию типов, но помните, что это не всегда необходимо. Это также означает, что двоичный размер вашей скомпилированной программы будет меньше, и ваш код будет компилироваться быстрее.

Однако этот подход расширения отлично подходит, когда у вас есть несколько методов, которые вы хотите добавить. Однако при разработке более крупного API возникает проблема масштабируемости.

Это? Имеет?

Ранее мы говорили, что GeometricVector усовершенствует SIMD . Но является ли это отношением «есть»? Проблема в том, что SIMD определяет операции, которые позволяют нам добавлять к вектору скаляр 1, но определять такую ​​операцию в контексте геометрии не имеет смысла.

Так что, возможно, отношение has-a было бы лучше, если бы обернуть SIMD в новый общий тип, который может обрабатывать любое число с плавающей запятой:

// NOTE: `Storage` is the underlying type that is storing the values, 
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
    typealias Scalar = Storage.Scalar
    var value: Storage
    init(_ value: Storage) { self.value = value }
}

Тогда мы можем быть осторожны и определять только те операции, которые имеют смысл только в контексте геометрии:

extension GeometricVector {
    static func + (a: Self, b: Self) -> Self {
        Self(a.value + b.value)
    }

    static func - (a: Self, b: Self) -> Self {
        Self(a.value - b.value)
    }
    static func * (a: Self, b: Scalar) -> Self {
        Self(a.value * b)
    }
}

И мы по-прежнему можем использовать общие расширения, чтобы получить три предыдущих оператора, которые мы хотели реализовать, которые выглядят почти так же, как и раньше:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a.value * b.value).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

В целом нам удалось уточнить поведение наших трех операций до типа, просто используя структуру. В случае с протоколами мы столкнулись с проблемой написания повторяющихся соответствий для всех векторов SIMD , а также не смогли запретить доступность определенных операторов, таких как Scalar + Vector (чего в данном случае мы не хотели). Таким образом, помните, что протоколы не являются универсальным решением. Но иногда более традиционные решения могут оказаться более эффективными.

Дополнительные ресурсы по протокольно-ориентированному программированию

Вот дополнительные ресурсы по обсуждаемым темам:

  • WWDC 2015: Протокольно-ориентированное программирование на Swift : это было представлено с использованием Swift 2, поэтому с тех пор многое изменилось (например, названия протоколов, которые они использовали в презентации), но это по-прежнему хороший ресурс для теории и ее использования. .
  • Знакомство с протокольно-ориентированным программированием в Swift 3 : это было написано в Swift 3, поэтому для успешной компиляции может потребоваться изменить часть кода, но это еще один отличный ресурс.
  • WWDC 2019: Современный дизайн API Swift : рассматриваются различия между типами значений и ссылочными типами, пример использования, когда протоколы могут оказаться худшим выбором в дизайне API (так же, как в разделе «Советы по хорошему проектированию API» выше), ключ поиск членов пути и оболочки свойств.
  • Дженерики : собственная документация Swift для Swift 5, посвященная дженерикам.