飞道的博客

Swift ——协议

411人阅读  评论(0)

1. 协议与继承

为两个类创建一个 debug 函数来打印当前类的基本信息。从继承的⻆度来说,我们可能会想到抽取一个公共的基类,当然大家都是动物,人也是动 物。从业务逻辑上来说,这么处理不太合理。可能最直观的办法是对于每一个类都写一个单独的 方法函数。

如果我们对当前代码中的每个类都需要 debug ,那上面这种方法显然是行不通的,于是我们有 了下面的代码

func debug(subject: Any){ print(.....)
}

当然看到这里可能大家也会觉得没有问题,如果我们要具体的描述当前类的具体信息,这个时候 我们还需要引入一个公共的基类,同时我们还需要有一个公共的属性 description 来让子类重 载,这无疑对我们的代码是很强的入侵。
所以这个时候我们通过一个协议来描述当前类的共同行为,并通过 extension 的方式来对我们 的类进行扩展,这样无疑是很棒的

extension LGTeacher : CustomStringConvertible {
var description : String { get { return "LGTeacher: \(age)\(name)" } }
}
extension Dog : CustomStringConvertible {
var description : String { get { return "Dog: \(age)\(name)" } }
}
func print(subject: CustomStringConvertible) { let string = data.description
//to do....
}

这里我们可以稍微的总结一下:

  • class 本质上定义了一个对象是什么
  • protocol 本质上定义了一个对象有哪些行为

2. 协议的基本语法

  • 协议要求一个属性必须明确是 get 或 get 和 set,且必须用var声明也就是变量
protocol MyProtocol{
    var age: Int{ get set }
    var name: String{ get }
}

  • 这里需要注意的一点是:并不是说当前声明 get 的属性一定是计算属性,协议里面声明get说明遵循协议的类要实现属性的get方法。
class LGTeacher: MyProtocol{
    var age: Int = 18
    var name: String
    init(_ name: String) {
        self.name = name   
    }
}
  • 协议中的异变方法,表示在该方法可以改变其所属的实例,以及该实例的所有属性(用于枚 举和结构体),在为类实现该方法的时候不需要写 mutating 关键字
protocol Togglable { 
mutating func toggle()
}
  • 类在实现协议中的初始化器,必须使用 required 关键字修饰初始化器的实现(类的初始化 器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器).如果class 添加final关键字,就不需要添加required了。
  • 类专用协议(通过添加 AnyObject 关键字到协议的继承列表,你就可以限制协议只能被类 类型采纳)
protocol MyProtocol: AnyObject{}
  • 可选协议:如果我们不想强制让遵循协议的类类型实现,可以使用 optional 作为前缀 放在协议的定义
protocol Incrementable{
optional func increment(by: Int)
}

3. 协议原理探究

那么对于协议的方法,会不会合成到类的Vtable当中进行调用呢?将下面的代码编译成sil文件。

protocol Incrementable{
    func increment(by:Int)
}

class LGTeacher: Incrementable {
    func increment(by: Int) {
        print(by)
    }
}

var t = LGTeacher()
t.increment(by: 20)

看到这里是class_method, class_method代表着用到了VTable调用方法。

往下看看到了increment在VTable里面。

那么如果把变量声明成协议的类型呢?

这里看到increment依然在Vtable里面。

但是调用方法确是用的witness_method。这里回去witness table(协议见证表)里面找到当前方法的实现。当类遵循了一个协议,实现了方法之后,编译器就会为这个类创建一个witness table,witness table里面就存放着实现方法的编码信息。

往下看到了witness table。

搜索一下,看到这里会去查找LGTeacher对于方法的实现


如果我们给协议的方法添加默认实现,这里还会调用LGTeacher中的实现。

但是,如果去掉了协议里面的声明,那么就会调用协议的默认实现。

  • 每个遵守了协议的类,都会有自己的PWT,遵守的协议函数越多,PWT中存储的函数地址就越 多
  • PWT的本质是一个指针数组,第一个元素存储TargetProtocolConformanceDescriptor,其 后面存储的是函数地址
  • PWT的数量与协议数量一致
  • Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协 议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管 理
  • 对于小容量的数据,直接存储在 Value Buffer(对于值类型来说,是24字节超过24字节就是大容量的数据)
  • 对于大容量的数据,通过堆区分配,存储堆空间的地址

4. 写时复制

那么如果struct的存储属性超过三个,也就是大容量的数据,就会通过对去分配,存储堆空间的地址,这个时候还会有值类型的特性吗?
这里看到circle的width还是没有改变的。

看到circle1,这里可以看到地址变了,并且width变成了80.

这里看到如果没有改变属性的值,那么指向的地址是一样的。

针对遵循协议的拥有比较大的数据的值类型,Swift采用了一种写时复制的技术,当在修改属性的值的时候,这里会去判断这个堆空间的引用计数,如果引用计数大于1,也就是有多个实例变量在引用这个堆空间的地址,就会触发写时复制,就会在堆空间重新复制一个新的存储空间,并把这个新的空间地址,传给要改变属性值的实例变量,用来保持修改后的实例值,这样做除了会保持值类型的特性外,还减少了内存分配的消耗,避免了创建了一些没有用的存储空间。


转载:https://blog.csdn.net/LinShunIos/article/details/125627035
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场