swift 协议类型作为参数_swift 类型作为参数 - CSDN
  • Swift 2.2 不允许在协议声明时提供默认参数。如果你想使用协议抽象出 App 中的日志代码,就会面临一个问题。因为默认参数通常用来将源代码位置传递给日志函数。不过,你可以在协议扩展中使用默认...
        

    作者:Natasha The Robot,原文链接,原文日期:2016-05-01
    译者:Channe;校对:walkingway;定稿:CMB

    Swift 2.2 不允许在协议声明时提供默认参数。如果你想使用协议抽象出 App 中的日志代码,就会面临一个问题。因为默认参数通常用来将源代码位置传递给日志函数。不过,你可以在协议扩展中使用默认参数,这是一个变通方案。

    一个典型的日志消息应该包括日志事件的源代码位置(文件名、行号和可能的函数名)。Swift 为此提供了 #file#line#column#function 调试标识。在编译时,解析器将这些占位符展开为字符串或用来描述当前源代码位置的整数字面量。如果我们在每次调用日志函数时都包含这些参数,那重复的次数太多,所以它们通常都是作为默认参数传递。这里之所以可行是因为编译器足够聪明,能够在评估默认参数列表时将调试标识扩展到函数调用处。标准库中的 assert 函数就是一个例子,它这样声明:

    
    func assert(
        @autoclosure condition: () -> Bool,
        @autoclosure _ message: () -> String = default,
        file: StaticString = #file,
        line: UInt = #line)
    

    第三个和第四个参数默认扩展为调用者源代码的位置。(如果你对 @autoclosure 属性有疑问,它把一个表达式封装为一个闭包,有效地将表达式的执行从调用处延迟到函数体执行时,即闭包表达式在明确使用时才会执行。assert 只在调试构建时使用它来执行 condition 参数的计算(可能代价高昂或者有副作用),同时只在断言失败时才计算 message 参数。)

    一个简单、全局的日志函数

    你可以使用同样的方法来写一个日志函数,该函数需要一个日志消息和一个日志级别作为参数。它的接口和实现类似于:

    
    enum LogLevel: Int {
        case verbose = 1
        case debug = 2
        case info = 3
        case warning = 4
        case error = 5
    }
    
    func log(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString = #file,
        line: Int = #line,
        function: StaticString = #function)
    {
        // 使用 `print` 打印日志
        // 此时不用考虑 `logLevel`
        print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
    }
    

    你可能主张使用另一种方法,而不是像这里将 message 参数声明为 @autoclosure。这个属性并没有提供多少好处,因为 message 参数无论什么情况都会计算。既然如此,我们来修改一下。

    具体类型

    为了代替全局的日志函数,我们创建一种叫做 PrintLogger 的类型,它用最小日志级别初始化,只会记录最小日志级别的事件。LogLevel 因此需要 Comparable 协议,这是为什么我之前把它声明为 Int 型来存储原始数据的原因:

    
    extension LogLevel: Comparable {}
    
    func <(lhs: LogLevel, rhs: LogLevel) -> Bool {
        return lhs.rawValue < rhs.rawValue
    }
    
    struct PrintLogger {
        let minimumLogLevel: LogLevel
    
        func log(
            logLevel: LogLevel,
            @autoclosure _ message: () -> String,
            file: StaticString = #file,
            line: Int = #line,
            function: StaticString = #function)
        {
            if logLevel >= minimumLogLevel {
                print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
            }
        }
    }
    

    你将会这样使用 PrintLogger

    
    let logger = PrintLogger(
        minimumLogLevel: .warning)
    logger.log(.error, "This is an error log")
        // 获取日志
    logger.log(.debug, "This is a debug log")
        // 啥也没做
    

    带默认参数的协议

    下一步,我将会创建一个 Logger 协议作为 PrintLogger 的抽象。它将允许我今后使用更高级的实现替换简单的 print 语句,比如记录日志到文件或者发送日志给服务器。但是,我在这里碰了壁,因为 Swift 不允许在协议声明时提供默认参数。下面的代码无法通过编译:

    
    protocol Logger {
        func log(
            logLevel: LogLevel,
            @autoclosure _ message: () -> String,
            file: StaticString = #file,
            line: Int = #line,
            function: StaticString = #function)
        // 错误: 协议方法中不允许默认参数
    }
    

    因此,我不得不删掉默认参数,使协议编译能够通过。这似乎并不是一个问题。PrintLogger 可以使用带有空扩展的协议,它目前的实现基本上能满足要求。通过使用一个 logger: PrintLogger 类型的变量和之前的用法没有什么区别。

    如果你尝试使用一个 logger2: Logger 协议类型的变量,问题马上就来了,因为你调用代码时是猜不到具体的实现的:

    
    let logger2: Logger = PrintLogger(minimumLogLevel: .warning)
    logger2.log(.error, "An error occurred")
        // 错误:调用时缺少参数
    logger2.log(.error, "An error occurred", file: #file, line: #line, function: #function)
        // 可用但是 ?
    

    logger2 只知道这个日志函数有五个必须的参数,所以你不得不每次都全部写上它们。讨厌!

    把默认参数移到协议扩展里

    解决方法是声明两个版本的日志函数:一,在协议声明时没有默认参数,我命名这个方法为 writeLogEntry。二,在 Logger 的协议扩展里包含默认参数(这是允许的),我保持这个方法名就为 log,因为该方法会是这个协议的公开接口。

    现在,log 的实现只有一行代码:调用 writeLogEntry,传入所有参数,而调用者通过默认参数传入了源代码位置。writeLogEntry 从另一方面来说是协议必须实现的适配器方法,用来执行实际的日志操作。这里是完整的协议代码:

    
    protocol Logger {
        /// 打印一条日志
        /// 类型必须遵循 Logger 协议的必选参数
        /// - 注意:Logger 的调用者永远不应该调用此方法
         /// 总是调用 log(_:,_:) 方法
        func writeLogEntry(
            logLevel: LogLevel,
            @autoclosure _ message: () -> String,
            file: StaticString,
            line: Int,
            function: StaticString)
    }
    
    extension Logger {
        /// Logger 协议的公开 API
        /// 只是调用 writeLogEntry(_:,_:,file:,line:,function:) 方法
        func log(
            logLevel: LogLevel,
            @autoclosure _ message: () -> String,
            file: StaticString = #file,
            line: Int = #line,
            function: StaticString = #function)
        {
            writeLogEntry(logLevel, message,
                file: file, line: line,
                function: function)
        }
    }
    

    按照 session 408 的说法,writeLogEntry 是一个协议要求和协议的用户自定义点,但 log 并不是。这就是我们想要的。log 方法的唯一任务就是立刻转发给 writeLogEntrywriteLogEntry 包含了实际的逻辑。实现 Logger 协议时就没有理由重写log方法了。

    下面是采用协议后的完整 PrintLogger 类型:

    
    struct PrintLogger {
        let minimumLogLevel: LogLevel
    }
    
    extension PrintLogger: Logger {
        func writeLogEntry(
            logLevel: LogLevel,
            @autoclosure _ message: () -> String,
            file: StaticString,
            line: Int,
            function: StaticString)
        {
            if logLevel >= minimumLogLevel {
                print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
            }
        }
    }
    

    现在你可以像期望中那样使用协议了:

    
    let logger3: Logger = PrintLogger(
        minimumLogLevel: .verbose)
    logger3.log(.error, "An error occurred") // 撒花?
    

    调用者的 API 可见度

    这个方法有一个弊端,不能简便清晰的通过访问控制给使用者指出协议中的 logwriteLogEntry 的作用。理想情况下,调用者使用协议时不会看到 writeLogEntry 方法,然而部署协议的对象可能同时看到 logwriteLogEntry 。如果你不想让调用者创建自己的 Logger 类型,只能使用 publicinternalprivate。当然,通过文档说明情况也是一个选择。

    本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

    展开全文
  • 来自Leo的原创博客,转载请著名出处我的StackOverflow我的Github https://github.com/LeoMobileDeveloper注意:本文的代码是用Swift... At the heart of Swift’s design are two incredibly powerful ideas: protocol-

    来自Leo的原创博客,转载请著名出处

    我的StackOverflow

    profile for Leo on Stack Exchange, a network of free, community-driven Q&A sites

    我的Github
    https://github.com/LeoMobileDeveloper

    注意:本文的代码是用Swift 2.2写的。


    前言

    引自Apple Swift团队的一句话

    At the heart of Swift’s design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming.

    通过值类型和面相协议协议来编写你的App,你会发现原来Swift对比Objective C来说,是如此的Powerful

    在阅读本文之前,建议有时间的同学去看下WWDC 2015和WWDC 2016的三个视频,相信会很有收获的。


    值类型

    首先,我们来看看OC中常见的需要Copy的类型,比如NSString,NSArray,NSDictionary…
    通常,你需要将一个NSStrig声明为Copy,so,why?
    如果我们将一个NSString声明为strong

    @property (strong,nonatomic)NSString * str;

    然后,这么调用

     NSMutableString * mutableStr = [@"Leo" mutableCopy];
     self.str = mutableStr;
     [mutableStr appendString:@" MobileDeveloper"];
     NSLog(@"%@",self.str); //Leo MobileDeveloper

    所以,问题就在这了,本身NSString是一个不可变对象,但是我的str潜在的被修改了都不知道。所谓为了更安全,很多类型都采用了Copy来进行操作。

    Swift中,Class是引用类型,Struct和Enum是值类型。引用类型在传递的时候,具有implict sharing. 也就是,默认是同一个内存对象的引用。而值类型在传递的时候,都是copy。
    比如

    class Person{
        var name:String
        var age:UInt
        init(name:String,age:UInt){
            self.name = name
            self.age = age
        }
    }
    

    然后,当你进行传递,然后不小心在某一个地方修改了一个属性,你会发现原始的对象被修改了(即使我将Person声明为let)

    let jack = Person(name: "Jack", age: 23)
    let temp = jack
    temp.age = 24
    print(jack.age) //24

    这样做导致的结果就是:

    • 你的实例在多个地方都可能被修改,导致了很多你注意不到的代码耦合
    • 你的实例依赖于外部状态,所以,单元测试变的困难。甚至,你的代码通过了单元测试,仍然会有Bug

    然后,我们再来看看值类型。

    struct Person{
        var name:String
        var age:UInt
    }
    
    let jack = Person(name: "Jack", age: 23)
    
    var temp = jack
    temp.age = 24
    print(jack.age) //23

    值类型的最大优势是:不可变状态,拿到值类型你不需要考虑别处会修改它

    如果你懂一点函数式编程语言,比如Haskell,你一定会了解不可变状态带来的代码可读性,可维护性,可测试行的好处。

    值类型用在哪些地方?

    如果你在网上搜索,那么一定会有很多博客,很多问答告诉你:“值类型用在简单的Model上”

    网上别人的观点一定是正确的吗?

    当然不是。

    值类型不仅仅能用在Model上,从MVVM的角度,他也能用在View,Controller,和viewModel上。并且,带来很多好处。WWDC 2016的这个视频Protocol and Value Oriented Programming in UIKit Apps介绍了值类型用在View的Layout以及Controller的Undo。这里我就不拾人牙慧,再写一遍了。

    引自Swift官方的描述,以下场景你可以优先考虑值类型

    • 当这个数据结构主要是为了封装一系列相关的值
    • 当进行传递时候,应该传递的是Copy,而不是引用
    • 这个数据结构本身存储的类型也是值类型
    • 这个数据结构不需要继承

    Equatable

    通常值类型,你都要让其遵循Equatable来保证可以用符号==来比较。
    比如

    struct Point:Equatable{
        var x,y:CGFloat
    }
    func == (lhs:Point,rhs:Point)->Bool{
        return lhs.x == rhs.x && lhs.y == rhs.y
    }

    值类型性能

    看到这,有经验的你一定在想,值类型每次都Copy,不会有性能问题吗?
    绝大部份时候,没有性能问题。
    并且相对于Class,Struct往往性能更好,因为:

    • 通常Struct在栈上分配内存,Class在堆上分配内存。在堆上分配内存的性能是要低于栈上的,因为堆上不得不做很多锁之类的操作来保证多线程的时候不会有问题。
    • Struct无需引用计数,而Classs需要。引用计数会造成隐式的额外开销
    • Struct不能继承,所有在方法执行的时候,是static dispatch.而Class是dynacmic dispatch。意味着Class需要通过virtual table来动态的找到执行的方法。

    Tips:如果你希望深入了解Swift的性能问题,建议看看WWDC 2016的这个视频 Understanding Swift Performance


    协议的定义

    A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. (协议定义了一个功能需要的方法,属性以及其它必要条件的蓝图)

    也就是说,协议是一个功能抽象的体现,协议本身只提供一套规则。

    Swift协议的优秀特性

    协议是一种类型

    Swift协议是一种类型。也就意味着你可以

    • 把它作为函数的参数或者返回值
    • 声明一个常量,变量或者属性
    • 作为容器类型(Array/Dictionary)的Element
      比如
    protocol Drawable{
        func draw()
    }
    
    struct Circle:Drawable{
        var center:CGPoint
        var radius:CGFloat
        func draw(){
            print("Draw cirlce at \(center) with radius \(radius)")
        }
    }
    struct Line:Drawable{
        var from,to:CGPoint
        func draw(){
            print("Draw line from \(from) to radius \(to)")
        }
    }
    
    struct Chart:Drawable{
        var items:[Drawable]
        func draw() {
            items.forEach{
                $0.draw()
            }
        }
    }
    let circle = Circle(center: CGPointMake(10, 10), radius: 5)
    let line = Line(from: CGPointMake(10, 10), to:CGPointMake(15, 10))
    let chart = Chart(items: [circle,line])
    chart.draw()
    //Draw cirlce at (10.0, 10.0) with radius 5.0
    //Draw line from (10.0, 10.0) to radius (15.0, 10.0)

    因为协议本身是一种类型,你所需要关注的就是抽象的本身,比如上文的代码里我只需要关注Drawable(可绘制),至于具体是怎么实现的,我并不关心。

    可继承

    protocol Drawable{
        func draw()
    }
    protocol AntoherProtocol:Drawable{
        func anotherFunc()
    }

    协议本身也是可扩展的

    通过扩展,来提供协议的默认实现

    比如,利用extension来扩展协议Drawable

    extension Drawable{
        func printSelf(){
            print("This is a drawable protocol instance")
        }
    }
    //然后,我们就可以这么调用了
    let circle = Circle(center: CGPointMake(10, 10), radius: 5)
    circle.printSelf()

    让现有Class/Struct枚举遵循协议

    可以通过Extension的方式,来让一个现有类实现某一个协议。
    比如,以下是最简单的Swift Model,Person和Room。

    struct Person{
        var name:String
        var age:Int
        var city:String
        var district:String
    }
    struct Room{
        var city:String
        var district:String
    }

    然后,突然有一天,我们有个需求是需要Log出Person和Room的地址信息。这时候怎么做?

    方式一,通过修改Person和Room的实现。这样其实就违背了“对扩展开放,对修改封闭的原则”。

    方式二,通过Swift协议扩展

    //定义新的需求的协议
    protocol AddressPrintAble{
        var addressDescription:String {get}
    }
    //通过扩展,让Person和Room实现协议
    extension Person:AddressPrintAble{
        var addressDescription:String{
            return "\(city) \(district)"
        }
    }
    extension Room:AddressPrintAble{
        var addressDescription:String{
            return "\(city) \(district)"
        }
    }
    

    然后,你就可以把Person,Room结构体的实例,当作AddressPrintAble(新的可打印地址)类型来使用了。因为协议是一种类型,你可以把他们存到一个数组里。

    let leo:AddressPrintAble = Person(name: "Leo", age: 24, city: "Shanghai", district: "XuHui")
    let room:AddressPrintAble = Room(name: "XinSi", city: "Shanghai", district: "Xuhui")
    
    var array = [AddressPrintAble]()
    array.append(leo)
    array.append(room)
    
    array.forEach { (address) in
        print(address.addressDescription)
    }

    通过协议来扩展Cocoa类是一个很常见也是很有用的一个技巧

    条件扩展

    可以在扩展协议的时候,用where语句进行约束,只对满足制定条件的约束协议进行扩展。比如,我们在初始化一个NSObject子类的时候,我们希望有一些初始化配置,如果能够让NSObject提供一个操作符来让我们进行初始化配置就好了
    于是,我们可以这么扩展

    public protocol SetUp {}
    extension SetUp where Self: AnyObject {
        public func SetUp(@noescape closure: Self -> Void) -> Self {
            closure(self)
            return self
        }
    }
    extension NSObject: SetUp {}

    然后,你就可以这么调用了

    let textfield = UITextField().SetUp {
         $0.frame = CGRectMake(0, 0,200, 30)
         $0.textAlignment = .Center
         $0.font = UIFont.systemFontOfSize(14)
         $0.center = view.center
         $0.placeholder = "Input some text"
         $0.borderStyle = UITextBorderStyle.RoundedRect
     }

    其它

    仅能用在Class上的协议

    protocol myProtocol:class 

    协议组合Protocol Composition

    同时遵循两个协议

    protocol Named {
        var name: String { get }
    }
    protocol Aged {
        var age: Int { get }
    }
    func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
        print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
    }

    Self

    协议方法中的Self,表示实际的遵循协议的类型。

    protocol Orderable{
        func compare(other:Self)->Bool
    }

    然后

    protocol Orderable{
        func compare(other:Self)->Bool
    }
    extension CGPoint:Orderable{
        func compare(other: CGPoint) -> Bool { //注意,这里的Other是CGPoint 类型了
            return self.x > other.x && self.y > other.y
        }
    }

    associatedtype

    当你的协议中,有不确定类型的对象,你可以通过associatedtype来声明
    比如

    protocol Listable{
        associatedtype Content
        var dataList:[Content]{get}
    }
    extension CGPoint:Listable{
        typealias Content = CGFloat
        var dataList:[Content]{
            get{
                return [x,y]
            }
        }
    }

    总结

    协议会让你的代码变的前所未有的灵活,比如我在写PullToRefreshKit的时候,就用协议增强了其可扩展性。
    举个例子,对于下拉刷新Header。
    定义了Protocol RefreshableHeader来表示任意可刷新的View的抽象。

    public protocol RefreshableHeader:class{
        func distanceToRefresh()->CGFloat
        func percentUpdateWhenNotRefreshing(percent:CGFloat)
        func releaseWithRefreshingState()
        func didBeginEndRefershingAnimation(result:RefreshResult)
        func didCompleteEndRefershingAnimation(result:RefreshResult)  
    }

    然后,利用协议和范型,任何遵循这个协议的UIView子类,都可以作为我的Refresh Header

    public func setUpHeaderRefresh<T:UIView where T:RefreshableHeader>(header:T,action:()->())->T{
        //...
    }

    本来想把《面相协议编程》和《面相值类型编程》放到这一篇里来写的,写到这里发现篇幅有点过长了。这篇文章就当做是基础复习吧。后面再写一篇

    展开全文
  • swift 协议

    2018-12-18 10:17:59
    2.22 协议 本页包含内容: 协议语法 ... 协议类型的集合 协议的继承 类类型专属协议 协议合成 检查协议一致性 可选的协议要求 协议扩展 协议 定义了一个蓝图,规定了用来实现某一特定任务或者...

    2.22 协议

    本页包含内容:

    • 协议语法
    • 属性要求
    • 方法要求(Method Requirements)
    • Mutating 方法要求
    • 构造器要求
    • 协议作为类型
    • 委托(代理)模式
    • 通过扩展添加协议一致性
    • 通过扩展遵循协议
    • 协议类型的集合
    • 协议的继承
    • 类类型专属协议
    • 协议合成
    • 检查协议一致性
    • 可选的协议要求
    • 协议扩展

    协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

    除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。

    协议语法

    协议的定义方式与类、结构体和枚举的定义非常相似:

    protocol SomeProtocol {
        // 这里是协议的定义部分
    }
    

    要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:

    struct SomeStructure: FirstProtocol, AnotherProtocol {
        // 这里是结构体的定义部分
    }
    

    拥有父类的类在遵循协议时,应该将父类名放在协议名之前,以逗号分隔:

    class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
        // 这里是类的定义部分
    }
    

    属性要求

    协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储型属性还是计算型属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。

    如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

    协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:

    protocol SomeProtocol {
        var mustBeSettable: Int { get set }
        var doesNotNeedToBeSettable: Int { get }
    }
    

    在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:

    protocol AnotherProtocol {
        static var someTypeProperty: Int { get set }
    }
    

    如下所示,这是一个只含有一个实例属性要求的协议:

    protocol FullyNamed {
        var fullName: String { get }
    }
    

    FullyNamed 协议除了要求遵循协议的类型提供 fullName 属性外,并没有其他特别的要求。这个协议表示,任何遵循 FullyNamed 的类型,都必须有一个可读的 String 类型的实例属性 fullName

    下面是一个遵循 FullyNamed 协议的简单结构体:

    struct Person: FullyNamed {
        var fullName: String
    }
    let john = Person(fullName: "John Appleseed")
    // john.fullName 为 "John Appleseed"
    

    这个例子中定义了一个叫做 Person 的结构体,用来表示一个具有名字的人。从第一行代码可以看出,它遵循了 FullyNamed 协议。

    Person 结构体的每一个实例都有一个 String 类型的存储型属性 fullName。这正好满足了 FullyNamed协议的要求,也就意味着 Person 结构体正确地符合了协议。(如果协议要求未被完全满足,在编译时会报错。)

    下面是一个更为复杂的类,它适配并遵循了 FullyNamed 协议:

    class Starship: FullyNamed {
        var prefix: String?
        var name: String
        init(name: String, prefix: String? = nil) {
            self.name = name
            self.prefix = prefix
        }
        var fullName: String {
            return (prefix != nil ? prefix! + " " : "") + name
        }
    }
    var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
    // ncc1701.fullName 是 "USS Enterprise"
    

    Starship 类把 fullName 属性实现为只读的计算型属性。每一个 Starship 类的实例都有一个名为 name 的非可选属性和一个名为 prefix 的可选属性。 当 prefix 存在时,计算型属性 fullName 会将 prefix 插入到 name 之前,从而为星际飞船构建一个全名。

    方法要求

    协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法的参数提供默认值。

    正如属性要求中所述,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字作为前缀:

    protocol SomeProtocol {
        static func someTypeMethod()
    }
    

    下面的例子定义了一个只含有一个实例方法的协议:

    protocol RandomNumberGenerator {
        func random() -> Double
    }
    

    RandomNumberGenerator 协议要求遵循协议的类型必须拥有一个名为 random, 返回值类型为 Double 的实例方法。尽管这里并未指明,但是我们假设返回值在 [0.0,1.0) 区间内。

    RandomNumberGenerator 协议并不关心每一个随机数是怎样生成的,它只要求必须提供一个随机数生成器。

    如下所示,下边是一个遵循并符合 RandomNumberGenerator 协议的类。该类实现了一个叫做 线性同余生成器(linear congruential generator) 的伪随机数算法。

    class LinearCongruentialGenerator: RandomNumberGenerator {
        var lastRandom = 42.0
        let m = 139968.0
        let a = 3877.0
        let c = 29573.0
        func random() -> Double {
            lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
            return lastRandom / m
        }
    }
    let generator = LinearCongruentialGenerator()
    print("Here's a random number: \(generator.random())")
    // 打印 “Here's a random number: 0.37464991998171”
    print("And another one: \(generator.random())")
    // 打印 “And another one: 0.729023776863283”
    

    Mutating 方法要求

    有时需要在方法中改变方法所属的实例。例如,在值类型(即结构体和枚举)的实例方法中,将 mutating关键字作为方法的前缀,写在 func 关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。这一过程在在实例方法中修改值类型章节中有详细描述。

    如果你在协议中定义了一个实例方法,该方法会改变遵循该协议的类型的实例,那么在定义协议时需要在方法前加 mutating 关键字。这使得结构体和枚举能够遵循此协议并满足此方法要求。

    注意
    实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。

    如下所示,Togglable 协议只要求实现一个名为 toggle 的实例方法。根据名称的暗示,toggle() 方法将改变实例属性,从而切换遵循该协议类型的实例的状态。

    toggle() 方法在定义的时候,使用 mutating 关键字标记,这表明当它被调用时,该方法将会改变遵循协议的类型的实例:

    protocol Togglable {
        mutating func toggle()
    }
    

    当使用枚举或结构体来实现 Togglable 协议时,需要提供一个带有 mutating 前缀的 toggle() 方法。

    下面定义了一个名为 OnOffSwitch 的枚举。这个枚举在两种状态之间进行切换,用枚举成员 On 和 Off表示。枚举的 toggle() 方法被标记为 mutating,以满足 Togglable 协议的要求:

    enum OnOffSwitch: Togglable {
        case off, on
        mutating func toggle() {
            switch self {
            case .off:
                self = .on
            case .on:
                self = .off
            }
        }
    }
    var lightSwitch = OnOffSwitch.off
    lightSwitch.toggle()
    // lightSwitch 现在的值为 .On
    

    构造器要求

    协议可以要求遵循协议的类型实现指定的构造器。你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:

    protocol SomeProtocol {
        init(someParameter: Int)
    }
    

    构造器要求在类中的实现

    你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required 修饰符:

    class SomeClass: SomeProtocol {
        required init(someParameter: Int) {
            // 这里是构造器的实现部分
        }
    }
    

    使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议。

    关于 required 构造器的更多内容,请参考必要构造器。

    注意
    如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。关于 final 修饰符的更多内容,请参见防止重写。

    如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required 和 override 修饰符:

    protocol SomeProtocol {
        init()
    }
    
    class SomeSuperClass {
        init() {
            // 这里是构造器的实现部分
        }
    }
    
    class SomeSubClass: SomeSuperClass, SomeProtocol {
        // 因为遵循协议,需要加上 required
        // 因为继承自父类,需要加上 override
        required override init() {
            // 这里是构造器的实现部分
        }
    }
    

    可失败构造器要求

    协议还可以为遵循协议的类型定义可失败构造器要求,详见可失败构造器。

    遵循协议的类型可以通过可失败构造器(init?)或非可失败构造器(init)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init)或隐式解包可失败构造器(init!)来满足。

    协议作为类型

    尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用。

    协议可以像其他普通类型一样使用,使用场景如下:

    • 作为函数、方法或构造器中的参数类型或返回值类型
    • 作为常量、变量或属性的类型
    • 作为数组、字典或其他容器中的元素类型

    注意
    协议是一种类型,因此协议类型的名称应与其他类型(例如 IntDoubleString)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamed 和 RandomNumberGenerator)。

    下面是将协议作为类型使用的例子:

    class Dice {
        let sides: Int
        let generator: RandomNumberGenerator
        init(sides: Int, generator: RandomNumberGenerator) {
            self.sides = sides
            self.generator = generator
        }
        func roll() -> Int {
            return Int(generator.random() * Double(sides)) + 1
        }
    }
    

    例子中定义了一个 Dice 类,用来代表桌游中拥有 N 个面的骰子。Dice 的实例含有 sides 和 generator 两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器,从而生成随机点数。

    generator 属性的类型为 RandomNumberGenerator,因此任何遵循了 RandomNumberGenerator 协议的类型的实例都可以赋值给 generator,除此之外并无其他要求。

    Dice 类还有一个构造器,用来设置初始状态。构造器有一个名为 generator,类型为 RandomNumberGenerator 的形参。在调用构造方法创建 Dice 的实例时,可以传入任何遵循 RandomNumberGenerator 协议的实例给 generator

    Dice 类提供了一个名为 roll 的实例方法,用来模拟骰子的面值。它先调用 generator 的 random()方法来生成一个 [0.0,1.0) 区间内的随机数,然后使用这个随机数生成正确的骰子面值。因为 generator遵循了 RandomNumberGenerator 协议,可以确保它有个 random() 方法可供调用。

    下面的例子展示了如何使用 LinearCongruentialGenerator 的实例作为随机数生成器来创建一个六面骰子:

    var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
    for _ in 1...5 {
        print("Random dice roll is \(d6.roll())")
    }
    // Random dice roll is 3
    // Random dice roll is 5
    // Random dice roll is 4
    // Random dice roll is 5
    // Random dice roll is 4
    

    委托(代理)模式

    委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。

    下面的例子定义了两个基于骰子游戏的协议:

    protocol DiceGame {
        var dice: Dice { get }
        func play()
    }
    protocol DiceGameDelegate {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
        func gameDidEnd(_ game: DiceGame)
    }
    

    DiceGame 协议可以被任意涉及骰子的游戏遵循。DiceGameDelegate 协议可以被任意类型遵循,用来追踪 DiceGame 的游戏过程。

    如下所示,SnakesAndLadders 是 控制流 章节引入的蛇梯棋游戏的新版本。新版本使用 Dice 实例作为骰子,并且实现了 DiceGame 和 DiceGameDelegate 协议,后者用来记录游戏的过程:

    class SnakesAndLadders: DiceGame {
        let finalSquare = 25
        let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
        var square = 0
        var board: [Int]
        init() {
            board = [Int](count: finalSquare + 1, repeatedValue: 0)
            board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
            board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
        }
         var delegate: DiceGameDelegate?
         func play() {
             square = 0
             delegate?.gameDidStart(self)
             gameLoop: while square != finalSquare {
                 let diceRoll = dice.roll()
                 delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
                 switch square + diceRoll {
                 case finalSquare:
                     break gameLoop
                 case let newSquare where newSquare > finalSquare:
                     continue gameLoop
                 default:
                     square += diceRoll
                     square += board[square]
                 }
             }
             delegate?.gameDidEnd(self)
         }
    }
    

    关于这个蛇梯棋游戏的详细描述请参阅 控制流 章节中的 Break 部分。

    这个版本的游戏封装到了 SnakesAndLadders 类中,该类遵循了 DiceGame 协议,并且提供了相应的可读的 dice 属性和 play() 方法。( dice 属性在构造之后就不再改变,且协议只要求 dice 为可读的,因此将 dice 声明为常量属性。)

    游戏使用 SnakesAndLadders 类的 init() 构造器来初始化游戏。所有的游戏逻辑被转移到了协议中的 play() 方法,play() 方法使用协议要求的 dice 属性提供骰子摇出的值。

    注意,delegate 并不是游戏的必备条件,因此 delegate 被定义为 DiceGameDelegate 类型的可选属性。因为 delegate 是可选值,因此会被自动赋予初始值 nil。随后,可以在游戏中为 delegate 设置适当的值。

    DicegameDelegate 协议提供了三个方法用来追踪游戏过程。这三个方法被放置于游戏的逻辑中,即 play() 方法内。分别在游戏开始时,新一轮开始时,以及游戏结束时被调用。

    因为 delegate 是一个 DiceGameDelegate 类型的可选属性,因此在 play() 方法中通过可选链式调用来调用它的方法。若 delegate 属性为 nil,则调用方法会优雅地失败,并不会产生错误。若 delegate 不为 nil,则方法能够被调用,并传递 SnakesAndLadders 实例作为参数。

    如下示例定义了 DiceGameTracker 类,它遵循了 DiceGameDelegate 协议:

    class DiceGameTracker: DiceGameDelegate {
        var numberOfTurns = 0
        func gameDidStart(_ game: DiceGame) {
            numberOfTurns = 0
            if game is SnakesAndLadders {
                print("Started a new game of Snakes and Ladders")
            }
            print("The game is using a \(game.dice.sides)-sided dice")
        }
        func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
            numberOfTurns += 1
            print("Rolled a \(diceRoll)")
        }
        func gameDidEnd(_ game: DiceGame) {
            print("The game lasted for \(numberOfTurns) turns")
        }
    }
    

    DiceGameTracker 实现了 DiceGameDelegate 协议要求的三个方法,用来记录游戏已经进行的轮数。当游戏开始时,numberOfTurns 属性被赋值为 0,然后在每新一轮中递增,游戏结束后,打印游戏的总轮数。

    gameDidStart(_:) 方法从 game 参数获取游戏信息并打印。game 参数是 DiceGame 类型而不是 SnakeAndLadders 类型,所以在gameDidStart(_:) 方法中只能访问 DiceGame 协议中的内容。当然了,SnakeAndLadders 的方法也可以在类型转换之后调用。在上例代码中,通过 is 操作符检查 game 是否为 SnakesAndLadders 类型的实例,如果是,则打印出相应的消息。

    无论当前进行的是何种游戏,由于 game 符合 DiceGame 协议,可以确保 game 含有 dice 属性。因此在 gameDidStart(_:) 方法中可以通过传入的 game 参数来访问 dice 属性,进而打印出 dice 的 sides属性的值。

    DiceGameTracker 的运行情况如下所示:

    let tracker = DiceGameTracker()
    let game = SnakesAndLadders()
    game.delegate = tracker
    game.play()
    // Started a new game of Snakes and Ladders
    // The game is using a 6-sided dice
    // Rolled a 3
    // Rolled a 5
    // Rolled a 4
    // Rolled a 5
    // The game lasted for 4 turns
    

    通过扩展添加协议一致性

    即便无法修改源代码,依然可以通过扩展令已有类型遵循并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。详情请在扩展章节中查看。

    注意
    通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。

    例如下面这个 TextRepresentable 协议,任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述:

    protocol TextRepresentable {
        var textualDescription: String { get }
    }
    

    可以通过扩展,令先前提到的 Dice 类遵循并符合 TextRepresentable 协议:

    extension Dice: TextRepresentable {
        var textualDescription: String {
            return "A \(sides)-sided dice"
        }
    }
    

    通过扩展遵循并符合协议,和在原始定义中遵循并符合协议的效果完全相同。协议名称写在类型名之后,以冒号隔开,然后在扩展的大括号内实现协议要求的内容。

    现在所有 Dice 的实例都可以看做 TextRepresentable 类型:

    let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
    print(d12.textualDescription)
    // 打印 “A 12-sided dice”
    

    同样,SnakesAndLadders 类也可以通过扩展遵循并符合 TextRepresentable 协议:

    extension SnakesAndLadders: TextRepresentable {
        var textualDescription: String {
            return "A game of Snakes and Ladders with \(finalSquare) squares"
        }
    }
    print(game.textualDescription)
    // 打印 “A game of Snakes and Ladders with 25 squares”
    

    通过扩展遵循协议

    当一个类型已经符合了某个协议中的所有要求,却还没有声明遵循该协议时,可以通过空扩展体的扩展来遵循该协议:

    struct Hamster {
        var name: String
           var textualDescription: String {
            return "A hamster named \(name)"
        }
    }
    extension Hamster: TextRepresentable {}
    

    从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:

    let simonTheHamster = Hamster(name: "Simon")
    let somethingTextRepresentable: TextRepresentable = simonTheHamster
    print(somethingTextRepresentable.textualDescription)
    // 打印 “A hamster named Simon”
    

    注意
    即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。

    协议类型的集合

    协议类型可以在数组或者字典这样的集合中使用,在协议类型提到了这样的用法。下面的例子创建了一个元素类型为 TextRepresentable 的数组:

    let things: [TextRepresentable] = [game, d12, simonTheHamster]
    

    如下所示,可以遍历 things 数组,并打印每个元素的文本表示:

    for thing in things {
        print(thing.textualDescription)
    }
    // A game of Snakes and Ladders with 25 squares
    // A 12-sided dice
    // A hamster named Simon
    

    thing 是 TextRepresentable 类型而不是 DiceDiceGameHamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thing 是 TextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription

    协议的继承

    协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

    protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
        // 这里是协议的定义部分
    }
    

    如下所示,PrettyTextRepresentable 协议继承了 TextRepresentable 协议:

    protocol PrettyTextRepresentable: TextRepresentable {
        var prettyTextualDescription: String { get }
    }
    

    例子中定义了一个新的协议 PrettyTextRepresentable,它继承自 TextRepresentable 协议。任何遵循 PrettyTextRepresentable 协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。在这个例子中,PrettyTextRepresentable 协议额外要求遵循协议的类型提供一个返回值为 String 类型的 prettyTextualDescription 属性。

    如下所示,扩展 SnakesAndLadders,使其遵循并符合 PrettyTextRepresentable 协议:

    extension SnakesAndLadders: PrettyTextRepresentable {
        var prettyTextualDescription: String {
            var output = textualDescription + ":\n"
            for index in 1...finalSquare {
                switch board[index] {
                case let ladder where ladder > 0:
                    output += "▲ "
                case let snake where snake < 0:
                    output += "▼ "
                default:
                    output += "○ "
                }
            }
            return output
        }
    }
    

    上述扩展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 协议,并提供了协议要求的 prettyTextualDescription 属性。每个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问 textualDescription 属性。然后,拼接上了冒号和换行符。接着,遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内容:

    • 当从数组中取出的元素的值大于 0 时,用  表示。
    • 当从数组中取出的元素的值小于 0 时,用  表示。
    • 当从数组中取出的元素的值等于 0 时,用  表示。

    任意 SankesAndLadders 的实例都可以使用 prettyTextualDescription 属性来打印一个漂亮的文本描述:

    print(game.prettyTextualDescription)
    // A game of Snakes and Ladders with 25 squares:
    // ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
    

    类类型专属协议

    你可以在协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前:

    protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
        // 这里是类类型专属协议的定义部分
    }
    

    在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型遵循。如果尝试让结构体或枚举类型遵循该协议,则会导致编译错误。

    注意
    当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。关于引用语义和值语义的更多内容,请查看结构体和枚举是值类型和类是引用类型。

    协议合成

    有时候需要同时遵循多个协议,你可以将多个协议采用 SomeProtocol & AnotherProtocol 这样的格式进行组合,称为 协议合成(protocol composition)。你可以罗列任意多个你想要遵循的协议,以与符号(&)分隔。

    下面的例子中,将 Named 和 Aged 两个协议按照上述语法组合成一个协议,作为函数参数的类型:

    protocol Named {
        var name: String { get }
    }
    protocol Aged {
        var age: Int { get }
    }
    struct Person: Named, Aged {
        var name: String
        var age: Int
    }
    func wishHappyBirthday(to celebrator: Named & Aged) {
        print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
    }
    let birthdayPerson = Person(name: "Malcolm", age: 21)
    wishHappyBirthday(to: birthdayPerson)
    // 打印 “Happy birthday Malcolm - you're 21!”
    

    Named 协议包含 String 类型的 name 属性。Aged 协议包含 Int 类型的 age 属性。Person 结构体遵循了这两个协议。

    wishHappyBirthday(to:) 函数的参数 celebrator 的类型为 Named & Aged。这意味着它不关心参数的具体类型,只要参数符合这两个协议即可。

    上面的例子创建了一个名为 birthdayPerson 的 Person 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。因为 Person 同时符合这两个协议,所以这个参数合法,函数将打印生日问候语。

    注意
    协议合成并不会生成新的、永久的协议类型,而是将多个协议中的要求合成到一个只在局部作用域有效的临时协议中。

    检查协议一致性

    你可以使用类型转换中描述的 is 和 as 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检查和转换到某个协议类型在语法上和类型的检查和转换完全相同:

    • is 用来检查实例是否符合某个协议,若符合则返回 true,否则返回 false
    • as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil
    • as! 将实例强制向下转换到某个协议类型,如果强转失败,会引发运行时错误。

    下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 area

    protocol HasArea {
        var area: Double { get }
    }
    

    如下所示,Circle 类和 Country 类都遵循了 HasArea 协议:

    class Circle: HasArea {
        let pi = 3.1415927
        var radius: Double
        var area: Double { return pi * radius * radius }
        init(radius: Double) { self.radius = radius }
    }
    class Country: HasArea {
        var area: Double
        init(area: Double) { self.area = area }
    }
    

    Circle 类把 area 属性实现为基于存储型属性 radius 的计算型属性。Country 类则把 area 属性实现为存储型属性。这两个类都正确地符合了 HasArea 协议。

    如下所示,Animal 是一个未遵循 HasArea 协议的类:

    class Animal {
        var legs: Int
        init(legs: Int) { self.legs = legs }
    }
    

    CircleCountryAnimal 并没有一个共同的基类,尽管如此,它们都是类,它们的实例都可以作为 AnyObject 类型的值,存储在同一个数组中:

    let objects: [AnyObject] = [
        Circle(radius: 2.0),
        Country(area: 243_610),
        Animal(legs: 4)
    ]
    

    objects 数组使用字面量初始化,数组包含一个 radius 为 2 的 Circle 的实例,一个保存了英国国土面积的 Country 实例和一个 legs 为 4 的 Animal 实例。

    如下所示,objects 数组可以被迭代,并对迭代出的每一个元素进行检查,看它是否符合 HasArea 协议:

    for object in objects {
        if let objectWithArea = object as? HasArea {
            print("Area is \(objectWithArea.area)")
        } else {
            print("Something that doesn't have an area")
        }
    }
    // Area is 12.5663708
    // Area is 243610.0
    // Something that doesn't have an area
    

    当迭代出的元素符合 HasArea 协议时,将 as? 操作符返回的可选值通过可选绑定,绑定到 objectWithArea 常量上。objectWithArea 是 HasArea 协议类型的实例,因此 area 属性可以被访问和打印。

    objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 CircleCountryAnimal 类型。然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。

    可选的协议要求

    协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上@objc属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

    使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

    协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。详细内容可在可选链式调用章节中查看。

    下面的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由 CounterDataSource 协议定义,包含两个可选要求:

    @objc protocol CounterDataSource {
        optional func incrementForCount(count: Int) -> Int
        optional var fixedIncrement: Int { get }
    }
    

    CounterDataSource 协议定义了一个可选方法 increment(forCount:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。

    注意
    严格来讲,CounterDataSource 协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。

    Counter 类含有 CounterDataSource? 类型的可选属性 dataSource,如下所示:

    class Counter {
        var count = 0
        var dataSource: CounterDataSource?
        func increment() {
            if let amount = dataSource?.incrementForCount?(count) {
                count += amount
            } else if let amount = dataSource?.fixedIncrement {
                count += amount
            }
        }
    }
    

    Counter 类使用变量属性 count 来存储当前值。该类还定义了一个 increment 方法,每次调用该方法的时候,将会增加 count 的值。

    increment() 方法首先试图使用 increment(forCount:) 方法来得到每次的增量。increment() 方法使用可选链式调用来尝试调用 increment(forCount:),并将当前的 count 值作为参数传入。

    这里使用了两层可选链式调用。首先,由于 dataSource 可能为 nil,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 increment(forCount:) 方法。其次,即使 dataSource存在,也无法保证其是否实现了 increment(forCount:) 方法,因为这个方法是可选的。因此,increment(forCount:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在 increment(forCount:) 方法后边也加上了 ?

    调用 increment(forCount:) 方法在上述两种情形下都有可能失败,所以返回值为 Int? 类型。虽然在 CounterDataSource 协议中,increment(forCount:) 的返回值类型是非可选 Int。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型。关于这一点的更多信息,请查阅连接多层可选链式调用

    在调用 increment(forCount:) 方法后,Int? 型的返回值通过可选绑定解包并赋值给常量 amount。如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。之后便将解包后的 amount 加到 count 上,增量操作完成。

    如果没有从 increment(forCount:) 方法获取到值,可能由于 dataSource 为 nil,或者它并没有实现 increment(forCount:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。fixedIncrement 是一个可选属性,因此属性值是一个 Int? 值,即使该属性在 CounterDataSource协议中的类型是非可选的 Int

    下面的例子展示了 CounterDataSource 的简单实现。ThreeSource 类遵循了 CounterDataSource 协议,它实现了可选属性 fixedIncrement,每次会返回 3

    class ThreeSource: NSObject, CounterDataSource {
        let fixedIncrement = 3
    }
    

    可以使用 ThreeSource 的实例作为 Counter 实例的数据源:

    var counter = Counter()
    counter.dataSource = ThreeSource()
    for _ in 1...4 {
        counter.increment()
        print(counter.count)
    }
    // 3
    // 6
    // 9
    // 12
    

    上述代码新建了一个 Counter 实例,并将它的数据源设置为一个 ThreeSource 的实例,然后调用 increment() 方法四次。和预期一样,每次调用都会将 count 的值增加 3.

    下面是一个更为复杂的数据源 TowardsZeroSource,它将使得最后的值变为 0

    @objc class TowardsZeroSource: NSObject, CounterDataSource {
        func increment(forCount count: Int) -> Int {
            if count == 0 {
                return 0
            } else if count < 0 {
                return 1
            } else {
                return -1
            }
        }
    }
    

    TowardsZeroSource 实现了 CounterDataSource 协议中的 increment(forCount:) 方法,以 count 参数为依据,计算出每次的增量。如果 count 已经为 0,此方法返回 0,以此表明之后不应再有增量操作发生。

    你可以使用 TowardsZeroSource 实例将 Counter 实例来从 -4 增加到 0。一旦增加到 0,数值便不会再有变动:

    counter.count = -4
    counter.dataSource = TowardsZeroSource()
    for _ in 1...5 {
        counter.increment()
        print(counter.count)
    }
    // -3
    // -2
    // -1
    // 0
    // 0
    

    协议扩展

    协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。

    例如,可以扩展 RandomNumberGenerator 协议来提供 randomBool() 方法。该方法使用协议中定义的 random() 方法来返回一个随机的 Bool 值:

    extension RandomNumberGenerator {
        func randomBool() -> Bool {
            return random() > 0.5
        }
    }
    

    通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现,无需任何额外修改:

    let generator = LinearCongruentialGenerator()
    print("Here's a random number: \(generator.random())")
    // 打印 “Here's a random number: 0.37464991998171”
    print("And here's a random Boolean: \(generator.randomBool())")
    // 打印 “And here's a random Boolean: true”
    

    提供默认实现

    可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。

    注意
    通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。

    例如,PrettyTextRepresentable 协议继承自 TextRepresentable 协议,可以为其提供一个默认的 prettyTextualDescription 属性,只是简单地返回 textualDescription 属性的值:

    extension PrettyTextRepresentable  {
        var prettyTextualDescription: String {
            return textualDescription
        }
    }
    

    为协议扩展添加限制条件

    在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述,正如Where 子句中所描述的。

    例如,你可以扩展 CollectionType 协议,但是只适用于集合中的元素遵循了 TextRepresentable 协议的情况:

    extension CollectionType where Generator.Element: TextRepresentable {
        var textualDescription: String {
            let itemsAsText = self.map { $0.textualDescription }
            return "[" + itemsAsText.joinWithSeparator(", ") + "]"
        }
    }
    

    textualDescription 属性返回整个集合的文本描述,它将集合中的每个元素的文本描述以逗号分隔的方式连接起来,包在一对方括号中。

    现在我们来看看先前的 Hamster 结构体,它符合 TextRepresentable 协议,同时这里还有个装有 Hamster 的实例的数组:

    let murrayTheHamster = Hamster(name: "Murray")
    let morganTheHamster = Hamster(name: "Morgan")
    let mauriceTheHamster = Hamster(name: "Maurice")
    let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]
    

    因为 Array 符合 CollectionType 协议,而数组中的元素又符合 TextRepresentable 协议,所以数组可以使用 textualDescription 属性得到数组内容的文本表示:

    print(hamsters.textualDescription)
    // 打印 “[A hamster named Murray, A hamster named Morgan, A hamster named Maurice]”
    

    注意
    如果多个协议扩展都为同一个协议要求提供了默认实现,而遵循协议的类型又同时满足这些协议扩展的限制条件,那么将会使用限制条件最多的那个协议扩展提供的默认实现。

    如果觉得对你有帮助,可以赏赐点什么,鼓励一下!

    微信支付:   支付宝:

     

    展开全文
  • Swift 5.0 ABI 达到稳定,Swift 5.1 达到 Module Stability,预示着 Swift 进入了成熟期。苹果也开始认真地吃自己的狗食了,我们看到这届大会上推出了几个用 Swift 写的 iOS 框架,占篇幅最大的,无疑是 SwiftUI。...

    今年 WWDC 最重要的关注点是什么?Swift!Swift 5.0 ABI 达到稳定,Swift 5.1 达到 Module Stability,预示着 Swift 进入了成熟期。苹果也开始认真地吃自己的狗食了,我们看到这届大会上推出了几个用 Swift 写的 iOS 框架,占篇幅最大的,无疑是 SwiftUI。为了这个框架写得6,苹果可以改语言,还不止一处。这次给大家介绍的是 Swift 5.1 在 协议上的改进:Opaque Result Type 不透明结果类型。这个特性增强了 Swift 泛型的能力,影响了 SwiftUI 的设计。

    本文我们先了解 Swift 5.1 的不透明返回类型的用途,然后再通过 Swift UI 上的应用,加深对它的理解。

    先来看一段代码,它展现了原来协议能力上的缺陷:

    protocol Shape {}

    struct Rectangle: Shape {}

    struct Union<A: Shape, B: Shape>: Shape {
    var a: Shape
    var b: Shape
    }

    struct Transformed<S: Shape>: Shape {
    var shape: S
    }

    protocol GameObject {
    associatedtype ShapeType: Shape
    var shape: ShapeType { get }
    }

    struct EightPointedStar: GameObject {
    var shape: Union<Rectangle, Transformed> {
    return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
    }

    缺陷有两方面:

    上述代码是可以编译通过的,但是 EightPointedStar 的 Shape 返回类型又臭又长,被暴露了出去;如果换成 Shape 则编译不通过,原因是 associatedtype ShapeType 要求必须指定具体的类型,而 Shape 不实现 Shape 本身。

    假如 Shape 协议中含有 Self 或者 associatedtype,无法作为函数的返回参数。这是 Swift 泛型系统长久以来的一个问题。

    而本文介绍的 Swift 5.1 Opaque Result Type 特性,解决了上述问题,它为协议作为返回类型提供以下能力:

    语法上隐藏具体类型,所以叫做不透明结果类型

    强类型:类型参数不丢失

    允许带有 Self 或者 associatedtype 的协议作为返回类型

    在 Swift 5.1 中,将返回类型改成 some + 协议名称的形式:
    struct EightPointedStar: GameObject {
    var shape: some Shape {
    return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
    }

    这类的泛型特性也被称作“反向泛型”,因为具体的类型参数是由“实现部分”指定并隐藏起来的,而一般的泛型是由“调用者”所指定的。

    上面这个例子中:语法上隐藏具体类型很明显,再举一个例子说明其它 2 个特性:

    func foo<T: Equatable>(x: T, y: T) -> some Equatable {
    let condition = x == y
    return condition ? 42 : 11
    }

    let x = foo(“apples”, “bananas”)
    let y = foo(“apples”, “oranges”)

    print(x == y) // 这里可以被调用是因为泛型系统保留了强类型

    这个例子显示了不透明结果类型的三个特性:既对外隐藏了具体的 Equatable 类型;又保留了强类型(使得 x == y)可以比较;还支持了 Equatable 这个带 Self 的泛型约束。

    不透明结果类型对于函数实现有一个增强的要求:函数实现必须返回同一个具体类型,以上述代码为例:不能返回 Equatable 或者是 不同类型的 Equatable 的实现。

    这里还有一个小问题:既然 x 和 y 可以直接比较,那么它们可否直接赋值给 var i: Int 呢?答案是对于静态类型系统是不可以的,它保留了 some Equatable 的具体类型隐藏功能,但是如果使用动态类型判断 as? Int,则可以转换成 Int

    1. 在SwiftUI 上的应用

    SwiftUI 中的视图类型的基本定义是一个协议 View

    public protocol View : _View {
    // body 属性的类型
    associatedtype Body : View

    // 唯一属性 body 的类型是另一个具体类型 View
    var body: Self.Body { get }
    

    }

    SwiftUI 最大特点的是声明式以及高度可组合,View 的唯一属性 body 是另一个满足 View 约束的具体 View 类型,我们在这里看到了组合以及递归两个特性。下面来看一个具体的 View 类型 ContentView:

    struct ContentView : View {
    var body: some View {
    VStack {
    Text(“Hello World”)
    Text(“Love & Peace”)
    }
    }
    }

    ContentView 使用了不透明结果类型的特性,对外隐藏了具体类型 VStack。此外,ContentView 的具体类型都是通过它的 body 属性递归定义的(取决于它所包含的具体 View):

    所有的递归定义都需要一个终止条件,于是就有了以下这些原生 View:Text、Color、Spacer、Image、Shape、Divider 等

    结语
    很高兴看到苹果终于开始提供 iOS 操作系统中的 Swift-Only 的 Framework,它对于 Swift 的推广和语言改进有进一步的促进作用。

    另外,我们也要注意到这个特性增加了 Swift ABI 的能力,需要最新的 runtime 才能运行。

    展开全文
  • Swift 协议(Protocols)

    2017-06-26 22:31:32
    类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。任意能够满足协议要求的类型被称为遵循(conform)这个协议协议定义,协议中的属性需求,协议中的方法需求 //土豪修别墅,看看如何...
  • [Swift]协议

    2014-12-06 15:23:14
     1) 其实就是Java中的接口,Swift的遵守协议就是Java中的实现接口,如果放在C++中就是纯虚类的概念,即协议就是一种高度抽象的抽象类,里面值规定了方法、属性等内容,但没有提供任何实现,所有遵守该协议的类、...
  • “我们如何在每天的开发过程中使用面向协议编程?Natasha 回答了这个问题,并专门针对...关注本篇在 App Builders CH 大会上的演讲,你将从面向对象编程转向面向协议编程,这样能使你的 Swift 编程更加清晰、更加易读!
  • 泛型可以将类型参数化,提高代码复用率,减少代码量 func swapValues<T>(_ a: inout T, _ b: inout T) { (a, b) = (b, a) } var i1 = 10 var i2 = 20 swapValues(&i1, &i2) print(i1, i2) // 20, ...
  • 什么是swift协议?Protocol Swift标准库中有50多个复杂不一的协议,几乎所有的实际类型都是妈祖若干协议的。protocol是Swift语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。这和我们熟知的面向对象...
  • Swift 协议

    2017-09-21 16:50:11
    协议规定了用来实现某一特定功能所必须的方法和属性。 任意能够满足协议要求的类型被称为遵循(conform...}要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号:分隔,作为类型定义的一部分。遵循多个协议
  • Swift 协议学习笔记

    2020-06-01 15:18:59
    7.协议类型的集合 8.协议继承协议 9.类类型专属协议 10.协议合成 11.检查协议的一致性 12.协议的可选性 13.协议扩展 一、协议的基本语法 下面是协议的一些基本语法: 1.定义一个协议 protocol SomeProtocol { //这
  • Swift Protocol 详解 - 协议&面向协议编程 之前一个帖子我总结了自己秋招面试经历,作为一个Swift开发者,有一个非常高频的问题就是:你觉得Swift相比于其他语言(或者OC来说)的特点和优势是什么?作为一个见识...
  • 文章标题谈到了面向协议编程(下文简称 POP),是因为前几天阅读了一篇讲 Swift 中 POP 的文章。本文会以此为出发点,聊聊相关的概念,比如接口、mixin、组合模式、多继承等,同时也会借助各种语言中的例子来阐述我的...
  • Swift协议

    2016-02-09 20:08:04
    面向协议(protocol-oriented)编程,苹果推荐!主要内容: 协议作为类型来使用 ...协议作为类型Swift编程语言中,协议可以被看作为一种类型,像其他任何类型那样使用。这意味着协议可以作为函数的参数
  • Swift 类型转换 Swift 语言类型转换可以判断实例的类型。也可以用于检测实例类型是否属于其父类或者子类的实例。 Swift类型转换使用 is 和 as 操作符实现,is 用于检测值的类型,as 用于转换类型类型转换也...
  • Swift协议

    2017-05-07 22:08:24
    前言如果你之前使用objective-c编写iOS程序肯定对协议都不陌生,在Swift中苹果将protocol这种语法发扬的更加深入和彻底。Swift中的protocol不仅能定义方法还能定义属性,配合extension扩展的使用还能提供一些方法的...
  •  任意能够满足协议要求的类型被称为协议的遵循者。  这种让规范和实现分离的方式正是协议的好处,一种松耦合的设计。 swift协议的作用,就相当于其他语言中的作用。  协议可以要求遵循者体统特
  • 简要协议是苹果给它自己设计的语言的一种类型Swift和OC都叫协议,它的功能更Java中的接口非常类似,其实就是Java中的接口功能,就是名称不一样而已,我们可以类比来学,但是有一点,Java中没有扩展,所以Java中的...
  • Swift 中定长的值类型都是保存在栈上的,操作时不会涉及堆上的内存。变长的值类型(字符串、集合类型是可变长度的值类型)会分配堆内存。 这相当于一个 “福利”,意味着你可以使用值类型更快速的完成一个方法的...
1 2 3 4 5 ... 20
收藏数 5,242
精华内容 2,096
关键字:

swift 协议类型作为参数