此页面由 Cloud Translation API 翻译。
Switch to English

面向协议的编程与开发仿制药

在TensorFlow.org上查看 在GitHub上查看源代码

本教程将介绍面向协议的编程,以及在日常示例中如何将它们与泛型一起使用的不同示例。

通讯协定

继承是一种以编程语言组织代码的有效方法,它使您可以在程序的多个组件之间共享代码。

在Swift中,有多种表达继承的方法。您可能已经熟悉其他语言中的一种方式:类继承。但是,Swift有另一种方式:协议。

在本教程中,我们将探讨协议-子类的一种替代方法,它使您可以通过不同的权衡来实现相似的目标。在Swift中,协议包含多个抽象成员。类,结构和枚举可以符合多种协议,并且可以追溯建立符合关系。所有这些使某些使用子类化在Swift中不易表达的设计成为可能。我们将逐步介绍支持协议使用(扩展和协议约束)以及协议限制的惯用法。

Swift s的值类型!

除了具有引用语义的类之外,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 }
}
 

在一个面向对象的世界(没有多重继承)中,您可能已经创建了ElectricGas抽象类,然后使用类继承使两者都继承自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)
 

这指定了同时符合CarElectric协议的新结构TeslaModelS

现在让我们定义一个汽油动力汽车:

 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。因此,我们刚刚能够用唯一的默认行为来装饰类,结构和枚举。

协议漫画

感谢Ray Wenderlich的漫画!

但是,需要注意的一件事是以下内容。在我们的第一个实现中,我们将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

但是,如果我们在A上使foo()必需,则会得到“ 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协议中定义的,但是由于CollectionSequence继承,因此它继承了与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协议的扩展中编写3个运算符,则可以更简洁地解决问题:

 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的运算,但是在几何环境中定义这样的运算没有意义。

因此,通过将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)
    }
}
 

而且,我们仍然可以使用通用扩展名来获取我们想要实现的之前的3个运算符,它们看起来几乎与以前完全相同:

 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:现代Swift API设计 :遍历值和引用类型之间的差异,一个用例,证明协议可以证明是API设计中的较差选择(与上面的“有关API设计的提示”部分相同),关键路径成员查找和属性包装器。
  • 泛型 :Swift自己的Swift 5文档,涉及泛型。