Swift底层进阶--013:协议

Protocol:所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”

  • Swift中协议被赋予更加强大、灵活的功能。相比Objective-C的协议,Swift的协议不仅可以被用做代理,也可以用作对接口的抽象,对代码的复用。
  • 协议规定了用来实现某一特定功能所必需的方法和属性。任意能够满足协议要求的类型被称为遵循(conform)协议。
  • 类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。
基本⽤法
语法格式
protocol MyProtocol {
   // 属性
   // 方法
}

classstructenum都可以遵循协议,如果要遵守多个协议,使⽤逗号分隔

struct LGTeacher: Protocol1, Protocol2 {
   // 属性
   // 方法
}

如果class中有superClass,⼀般放在遵循的协议之前

class LGTeacher: NSObject, MyProtocol{
   // 属性
   // 方法
}
协议中属性定义的要求
  • 属性不能设置默认值
  • 属性必须明确是可读或可读可写
  • 属性必须使用var修饰
协议中方法定义的要求
  • 方法不能有方法体
  • 方法参数不能设置默认值
遵循协议并实现协议中的属性、方法
protocol MyProtocol {
    var age: Int { get }
    var name: String { get set }
    
    func doSomething(age: Int) -> Int
    static func teach()
}

class LGTeacher: MyProtocol {
    let age: Int = 18
    var name: String = "Zang"
    
    func doSomething(age: Int = 18) -> Int {
        print("LGTeacher doSomething")
        return age
    }
    
    static func teach() {
        print("teach")
    }
}

LGTeacher遵循MyProtocol协议

  • 实现协议的属性,可以设置默认值。只读属性可以选择let修饰
  • 实现协议的方法,参数可以设置默认值
  • 协议中使用static修饰的类型方法,可以被类或结构体遵循。被类遵循,可以选择将static替换为class修饰

func使用static或者class修饰的区别与联系

  • static或者class关键字,都可用于指定类方法
  • class关键字指定的类方法可以被子类重写
  • static关键字指定的类方法不能被子类重写
required关键字

协议也可以定义初始化⽅法,当类实现初始化器的时候,必须使⽤required关键字

protocol MyProtocol {
    init(age: Int)
}

class LGTeacher: MyProtocol{
    var age: Int
    
    required init(age: Int) {
        self.age = age
    }
}

如果当前类被final修饰,可以忽略required关键字,因为被final修饰的类不能被继承

protocol MyProtocol: AnyObject {
    init(age: Int)
}

final class LGTeacher: MyProtocol{
    var age: Int

    init(age: Int) {
        self.age = age
    }
}

使用final修饰的类被继承,编译报错

编译报错

要求协议只能被类遵循

如果要求协议只能被类遵循,可以添加AnyObject

protocol MyProtocol: AnyObject {
    init(age: Int)
}

class LGTeacher: MyProtocol{
    var age: Int

    required init(age: Int) {
        self.age = age
    }
}

structenum遵循MyProtocol协议,编译报错

编译报错

将协议作为类型
  • 作为函数、⽅法或初始化程序中的参数类型或返回类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中项⽬的类型
案例1:

MyProtocol协议定义teach方法,在MyProtocol扩展和LGTeacher中都实现teach方法,查看案例1的打印结果:

protocol MyProtocol {
    func teach()
}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {
    func teach() {
        print("MyClass")
    }
}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyClass
//MyClass
  • t1声明MyProtocol类型,protocol中定义了teach⽅法,使用witness_method调用,通过PWT协议⽬击表获取对应的函数地址,打印结果MyClass
  • t2声明LGTeacher类型,使用class_method调用,通过V-table函数表查找函数,打印结果MyClass

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,LGTeacher遵循MyProtocol协议,并记录实现的方法

PWT

遵循协议后被实现的协议方法teach,内部使用class_method指令通过类的函数表查找函数

bb0

案例2:

如果MyProtocol是空协议,在MyProtocol扩展和LGTeacher中实现teach方法,查看案例2的打印结果:

protocol MyProtocol {}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {
    func teach() {
        print("MyClass")
    }
}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyProtocol
//MyClass
  • t1声明MyProtocol类型,protocol中未定义任何⽅法,直接使用extension静态调用。静态调用在编译链接之后方法地址已经确定,对于遵循协议的类来说⽆法重写,打印结果MyProtocol
  • t2声明LGTeacher类型,使用class_method调用,通过V-table函数表查找函数,打印结果MyClass

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,虽然LGTeacher遵循MyProtocol协议,但没有记录任何方法,同时也找不到上面定义的被实现的协议方法

PWT

案例3:

MyProtocol协议定义teach方法,仅MyProtocol扩展实现teach方法,查看案例3的打印结果:

protocol MyProtocol {
    func teach()
}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyProtocol
//MyProtocol

t1声明MyProtocol类型,protocol中定义了teach⽅法,使用witness_method调用,通过PWT协议⽬击表获取对应的函数地址,打印结果MyProtocol
t2声明LGTeacher类型,由于类中没有实现teach方法,直接使用extension静态调⽤,打印结果MyProtocol

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,LGTeacher遵循MyProtocol协议,并记录实现的方法

PWT

遵循协议后被实现的协议方法teach,内部使用extension静态调用

bb0

PWT
案例1

定义Circle类遵循Shape协议,声明Shape类型和Circle类型实例变量,打印各自的大小和步长

protocol Shape{
    var area: Double{ get }
}

class Circle: Shape {
    var radious: Double
    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

var shape: Shape = Circle.init(10.0)
print("shape of size:\(MemoryLayout.size(ofValue: shape))")
print("shape of stride:\(MemoryLayout.stride(ofValue: shape))")

var circle: Circle = Circle.init(10.0)
print("circle of size:\(MemoryLayout.size(ofValue: circle))")
print("circle of stride:\(MemoryLayout.stride(ofValue: circle))")

//输出以下内容:
//shape of size:40
//shape of stride:40
//circle of size:8
//circle of stride:8

shape声明为Shape类型,大小和步长占40字节circle声明为Circle类型,大小和步长只占8字节

使用lldb调式,能看到的内容有限,无法得出结论

lldb

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

通过官方文档,查看init_existential_addr的作用

init_existential_addr

Existential Container是编译器⽣成的⼀种特殊数据类型,⽤于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺⼨不同,使⽤Extential Container进⾏管理可以实现存储⼀致性

如果想进一步分析,明确init_existential_addr存储的到底是什么,需要将SIL代码再降⼀级,通过IR代码观察

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

IR

%T4main5ShapeP是一个结构体,有一个24 x i8的数组,占24字节。有一个%swift.type*的指针和一个i8**的二级指针

%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }

%4存储的是metadata

%4 = extractvalue %swift.metadata_response %3, 0

%5存储的是HeapObject,它调用了__allocating_init,传入了一个double类型和一个metadata

%5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)

图中第一句代码,获取结构体本身,拿到第一个元素,然后将%4也就是metadata,存储到结构体的第一个元素中

store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.shape : main.Shape", i32 0, i32 1), align 8

图中第二句代码,获取结构体本身,拿到第二个元素,然后将PWT协议目击表,存储到结构体的第二个元素中。代码中@"protocol witness table for main.Circle : main.Shape in main"就是PWT

store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.shape : main.Shape", i32 0, i32 2), align 8

图中第三句代码,实例对象main.shape是结构体首地址24 x i8数组类型,
使用bitcast将其强转为%T4main6CircleC二级指针类型,再将%5也就是HeapObject存储到强转后的指针中

store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.shape : main.Shape" to %T4main6CircleC**), align 8

最终可以分析出%T4main5ShapeP结构体的元素构成

type { HeapObject, metadata, PWT } 

使用Swift代码还原上述结论

定义HeapObject结构体

struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

定义ProtocolData结构体,value1value2value3三个属性,各自占8字节,连续内存空间存储,对应IR代码中的24 x i8数组。type对应metadatapwt对应PWT协议目击表

struct ProtocolData {
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    var type: UnsafeRawPointer
    var pwt: UnsafeRawPointer
}

将实例对象shape转换为ProtocolData结构

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x0000000100705910, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x000000010000c3b0, pwt: 0x0000000100008070)
  • value1存储HeapObject地址,value2value3未被使用
  • type存储的metadata,实际上是VWT(Value Witness Table)
  • pwt存储的PWT协议目击表

进入终端,使用nm命令查看pwt存储的0000000100008070地址:

nm /Users/zang/Library/Developer/Xcode/DerivedData/LGSwiftTest-ezhaqfxldtlovoammsyvcyhxjxoj/Build/Products/Debug/LGSwiftTest | grep 0000000100008070

输出结果:

0000000100008070 S _$s11LGSwiftTest6CircleCAA5ShapeAAWP

使用xcrun还原符号:

xcrun swift-demangle s11LGSwiftTest6CircleCAA5ShapeAAWP

输出结果:

$s11LGSwiftTest6CircleCAA5ShapeAAWP ---> protocol witness table for LGSwiftTest.Circle : LGSwiftTest.Shape in LGSwiftTest

这也解释了为什么上面看到的Shape类型实例对象会占40字节;其中value1value2value3三个属性共占24字节typepwt共占16字节

案例2

如果定义的是结构体,遵循Shape协议,存储的结构会有哪些变化?

protocol Shape{
    var area: Double{ get }
}

struct Rectangle: Shape {
    var width, height: Double

    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var shape: Shape = Rectangle.init(10.0, 20.0)

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

IR

图中%4%5发生了变化,直接将结构体的两个double属性取出来,存储到%T4main5ShapeP结构体首地址的24 x i8数组中

将结构体类型的实例对象shape转换为ProtocolData结构

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x00000001000040e0, pwt: 0x0000000100004070)

value1存储width属性,value2存储height属性,value3未被使用
type存储的metadata,实际上是VWTValue Witness Table
pwt存储的PWT协议目击表

案例3

如果结构体大小超过24字节,存储的结构会有哪些变化?

protocol Shape{
    var area: Double{ get }
}

struct Rectangle: Shape {
    var width, height: Double
    var width1: Double = 40.0
    var height1: Double = 50.0

    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var shape: Shape = Rectangle.init(10.0, 20.0)

案例2的基础上,将结构体增加width1height1两个属性,这时结构体的大小已超过24字节

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x0000000103304080, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100004108, pwt: 0x0000000100004098)

只有value1有值,value2value3未被使用
typepwt没有变化

通过lldb,查看value1存储的是什么

lldb

  • 结构体首地址24字节,存储的是遵循协议的结构体或类的值
  • 如果是值类型,大小在24字节以内,直接存储值。如果超过24字节,系统将在堆区分配内存空间,将值存储到堆区,然后将堆区的指针地址存储到前面8字节,此时value2value3未被使用
  • 如果是引用类型,直接存储HeapObject
案例4

声明两个协议类型实例对象,将它们存储到数组。循环打印shape.area属性时,它们可以正确调用到各自的实现方法

protocol Shape{
    var area: Double{ get }
}

class Circle: Shape {
    var radious: Double

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

struct Rectangle: Shape {
    var width, height: Double

    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]

for shape in shapes{
    print(shape.area)
}

//输出以下内容:
//314.0
//200.0

理解了前三个案例,对于案例4的打印结果应该不会意外;本质上协议容器里存放了PWT协议目击表和metadata,在协议目击表内依然使用class_method查表方式,通过metadata 找到对应的V-table,最终调用正确的实现方法

案例5

声明Shape类型circle,将circle赋值给circle1,这时打印circlecircle1value1地址,会是相同地址吗?如果修改circle1.radious属性,他们value1的地址又会发生怎样的变化?

protocol Shape{
    var radious: Double { get set }
    var area: Double { get }
}

struct Circle: Shape {
    var radious: Double
    var radious1: Double = 3
    var radious2: Double = 6
    var radious3: Double = 9

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

struct ValueData {
    var type: HeapObject
    var val1: Double
    var val2: Double
    var val3: Double
    var val4: Double
}

var circle: Shape = Circle.init(10.0)
var circle1 = circle

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

circle1.radious = 20.0
print("\n----------\n")

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

//输出一下结果:
//circle.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改后地址:0x000000010052ed30,值:20.0

circle1.radious修改前,circlecircle1value1地址是相同的。当circle1.radious发生改变后,circle1.value1的地址也发生了改变,这种现象称之为“写时复制”(copy-on-write

上述代码为什么会触发的“写时复制”?
  • Circle是结构体类型,也就是值类型,所以circlecircle1并不共享状态
  • circle1.radious发生改变,相当于修改副本,对circle没有任何影响
  • protocol结构体中24字节官方叫法是Value BufferValue Buffer用来存储当前的值,如果超过Value Buffer的最大存储容量,系统会开辟堆空间存储值,Value Buffer存储堆区指针地址
  • 修改堆空间里的值类型,在修改前会先检测引用计数,如果引用计数大于1,此时系统会开辟新的堆空间,把要修改的内容拷贝到新空间内,目的是提升性能
  • 如果是引用类型Class,则不会触发写时复制
案例6

案例5的结构体修改为Class,查看输出结果的变化

class Circle: Shape {
    var radious: Double

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

var circle: Shape = Circle.init(10.0)
var circle1 = circle

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

circle1.radious = 20.0
print("\n----------\n")

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

//输出一下结果:
//circle.value1 - 修改前地址:0x0000000103204a20,值:10.0
//circle1.value1 - 修改前地址:0x0000000103204a20,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000103204a20,值:20.0
//circle1.value1 - 修改后地址:0x0000000103204a20,值:20.0

Circle修改为Class类型,也就是引用类型,所以circlecircle1共享状态。当circle1.radious发生改变,circle也会随之改变

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • 协议的基本⽤法 协议的语法格式 我们熟悉的 class , struct , enum 都可以遵循协议,如果要遵守...
    Mjs阅读 523评论 0 0
  • 协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现...
    HotPotCat阅读 1,504评论 2 5
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile丽语阅读 3,830评论 0 6
  • C语⾔枚举 先来回顾⼀下C语⾔的枚举写法: ⽐如表示⼀周 7天,⽤C语⾔的枚举写法应该是这样的: 第⼀个枚举成员默...
    帅驼驼阅读 602评论 0 3
  • 闭包是可以在你的代码中被传递和引用的功能性独立代码块。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址...
    HotPotCat阅读 1,018评论 1 4