美文网首页
Swift进阶十二:协议

Swift进阶十二:协议

作者: Trigger_o | 来源:发表于2022-06-14 16:15 被阅读0次

    一:对比Swift协议与其他语言

    泛型可以帮助我们写出动态的程序。协议可以与函数和泛型协同工作,让我们代码的动态特性更加强大.

    Swift 协议既可以被用作代理,也可以让你对接口进行抽象,比如Sequence,和OC协议的最大不同在于我们可以让结构体和枚举类型满足协议。除此之外,还可以有关联类型,可以通过协议扩展的方式为协议添加方法实现等等.

    在面向对象编程中,子类是在多个类之间共享代码的有效方式。一个子类将从它的父类继承所有的方法,然后选择重写其中的某些方法.
    在 Swift 中,代码共享也可以通过协议和协议扩展来实现。Sequence 协议和它的扩展在结构体和枚举这样的值类型中依然可用,而这些值类型是不支持子类继承的.

    不再依赖于子类让类型系统更加灵活。在 Swift (以及其他大多数面向对象的语言) 中,一个类只能有一个父类。当我们创建一个类时,我们必须同时选择父类,而且我们只能选择一个父类,而C++这种可以多重继承的语言,又会有新的问题,相比多继承,实现多个协议并没有那些问题.

    协议扩展是一种可以在不共享基类的前提下共享代码的方法。协议定义了一组最小可行的方法集合,以供类型进行实现。而类型通过扩展的方式在这些最小方法上实现更多更复杂的特性.

    要实现一个对任意序列进行排序的泛型算法,你需要两件事情。首先,你需要知道如何对要排序的元素进行迭代。其次,你需要能够比较这些元素的大小,就这么多。没有必要知道元素是如何被存储的,也没有必要规定这些元素到底是什么只要你在类型系统中提供了前面提到的那两个约束,我们就能实现 sort 函数.

    extension Sequence where Element: Comparable { 
          func sorted() -> [Self.Element] 
    }
    

    通过父类来添加共享特性就没那么灵活了,在开发过程进行到一半的时候再决定为很多不同的类添加一个共同基类往往是很困难的,这需要大量的重构。而且如果你不是这些子类的拥有者的话,就更做不到了.
    子类必须知道哪些方法是它们能够重写而不会破坏父类行为的。比如,当一个方法被重写时,子类可能会需要在合适的时机调用父类的方法,这个时机可能是方法开头,也可能是中间某个地方,又或者是在方法最后。通常这个调用时机是不可预估和指定的。另外,如果重写了错误的方法,子类还可能破坏父类的行为,却不会收到任何来自编译器的警告.

    通过协议进行代码共享相比与通过继承的共享,有这几个优势:
    1.我们不需要被强制使用某个父类。
    2.我们可以让已经存在的类型满足协议 (比如我们让 CGContext 满足了 Drawing)。子类就没那么灵活了,如果 CGContext 是一个类的话,我们无法以追溯的方式去变更它的父类。
    3.协议既可以用于类,也可以用于结构体,而父类就无法和结构体一起使用了。
    4.当处理协议时,我们无需担心方法重写或者在正确的时间调用 super 这样的问题。

    二:面向协议编程

    1.追溯建模

    协议的最强大的特性之一就是我们可以以追溯的方式来修改任意类型,让它们满足协议.这也是最常用的特性.

    protocol Drawing { 
          mutating func addEllipse(rect: CGRect, fill: UIColor)
          mutating func addRectangle(rect: CGRect, fill: UIColor) 
    }
    
    extension CGRect: Drawing {
        func addEllipse(rect: CGRect, fill: UIColor) {
           print("addEllipse CGRect")
        }
        
        func addRectangle(rect: CGRect, fill fillColor: UIColor) {
            print("addRectangle CGRect")
        }
    }
    
    

    2.协议扩展

    作为协议的作者,当你想在扩展中添加一个协议方法,你有两种方法。首先,你可以只在扩展中进行添加。或者,你还可以在协议定义本身中添加这个方法的声明,让它成为协议要求的方法。协议要求的方法是动态派发的,而仅定义在扩展中的方法是静态派发的。

    添加在扩展中的方法

    extension Drawing {
        mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
            print("addCircle Drawing")
        }
    }
    

    另外也可以重写这个方法,编译器将选择 addCircle 的最具体的版本,也就是定义在 CGRect 扩展上的版本

    func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
            print("addCircle CGRect")
    }
    var rect = CGRect.zero
    rect.addCircle(center: .zero, radius: 0, fill: .clear)
    // addCircle CGRect
    

    如果把声明的类型改为Drawing,编译器会使用了协议扩展中的addCircle 方法,而没有用 CGRect 扩展中的

    var rect:Drawing = CGRect.zero
    rect.addCircle(center: .zero, radius: 0, fill: .clear)
    //addCircle Drawing
    

    当我们将 rect 定义为 Drawing 类型的变量时,编译器会自动将 CGRect 值封装到一个代表协议的类型中,这个封装被称作存在容器.
    对存在容器调用 addCircle 时,方法是静态派发的,也就是说,它总是会使用 Drawing的扩展。如果它是动态派发,那么它肯定需要将方法的接收者 CGRect 类型考虑在内.

    如果需要动态派发,就要把方法定义到协议本体中

    protocol Drawing {
        mutating func addEllipse(rect: CGRect, fill: UIColor)
        mutating func addRectangle(rect: CGRect, fill: UIColor)
        mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor)
    }
    
    extension Drawing {
        mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
            print("addCircle Drawing")
        }
    }
    

    并且不影响在扩展中添加一个默认实现,具体的类型还是可以自由地重写 addCircle。因为现在它是协议定义的一部分了,它将被动态派发。在运行时,根据方法接收者的动态类型的不同,存在容器将会在自定义实现存在时对其进行调用。如果自定义实现不存在,那么它将使用协议扩展中的默认实现。addCircle 方法变为了协议的一个自定义入口.标准库大量使用这种方法,比如Sequence定义的方法很多但基本都有默认实现.

    三:特殊的协议类型

    带有关联类型的协议,以及协议定义中在任何地方使用了 Self 的协议,这两种协议和普通的协议是不同的,这样的协议不能被当作独立的类型来使用.

    1.带有关联类型的协议
    IteratorProtocol包含一个关联类型 Element和一个返回该类型可选值的 next() 函数

    public protocol IteratorProtocol { 
          associatedtype Element 
          public mutating func next() -> Element? 
    }
    

    假如我们写出 var a:IteratorProtocol
    编译器就会报错:
    "Protocol 'IteratorProtocol' can only be used as a generic constraint because it has Self or associated type requirements"
    (IteratorProtocol’ 协议含有 Self 或者关联类型,因此它只能被当作泛型约束使用)

    这就是说,将 IteratorProtocol 是一个不完整的类型。我们必须为它指明关联类型,否则单是关联类型的协议是没有意义的.
    这样使用是可以的

    func nextInt<I: IteratorProtocol>(iterator: inout I) -> Int? where I.Element == Int { 
          return iterator.next() 
    }
    

    这是可行方式,但是却有一个缺点,存储的迭代器的指定类型通过泛型参数 “泄漏” 出来了。在现有的类型系统中,我们无法表达 “元素类型是 Int 的任意迭代器” 这样一个概念。如果你想把多个 IteratorStore 放到一个数组里,这个限制就将带来问题。数组里的所有元素都必须有相同的类型,这也包括任何的泛型参数.

    不过我们可以绕过这种限制,这种将 (迭代器这样的) 指定类型移除的过程,就被称为类型抹消.

    实现一个封装类。我们不直接存储迭代器,而是让封装类存储迭代器的 next函数。要做到这一点,我们必须首先将 iterator 参数复制到一个本地的 var 变量中,这样我们就可以调用它的 mutating 的 next 方法了。接下来我们将 next() 的调用封装到闭包表达式中,然后将这个闭包赋值给属性。我们使用类来表征 IntIterator 具有引用语义.

    class IntIterator { 
          var nextImpl: () -> Int?
          init<I: IteratorProtocol>(_ iterator: I) where I.Element == Int { 
                var iteratorCopy = iterator 
                self.nextImpl = { iteratorCopy.next() } 
          }
    }
    

    现在,在 IntIterator 中,迭代器的具体类型 (比如 ConstantIterator) 只在创建实例的时候被指定。在那之后,这个具体类型被隐藏起来,并被闭包捕获。我们可以使用任意类型且元素为整数的迭代器,来创建 IntIterator 实例。

    var iter = IntIterator(ConstantIterator()) 
    iter = IntIterator([1,2,3].makeIterator())
    

    2.带有Self的协议
    最简单的带有 Self 的协议是 Equatable。它有一个 (运算符形式的) 方法,用来比较两个元素

    protocol Equatable { 
          static func ==(lhs: Self, rhs: Self) -> Bool 
    }
    

    我们不能简单地用 Equatable 来作为类型进行变量声明,这很好理解,任何类型都可以实现Equatable,但是两种类型显然不能分别写在"=="的两边.

    相关文章

      网友评论

          本文标题:Swift进阶十二:协议

          本文链接:https://www.haomeiwen.com/subject/arkzmrtx.html