swift 函数式编程tips (二)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/PeipeiQ/article/details/81122001

protocol(协议)专题二

demo链接–>https://github.com/PeipeiQ/MySwift
我的个人博客->http://www.peipeiq.cn
最近在公司用swift做开发,也开始关注一些swift的语言风格,所以接下来的博客以swift语言为主。oc或者swift有什么问题可以一起交流。

协议本质

OC和swift中都可以把协议作为类型来使用,比如OC中最常用的

@property(nonatomic,weak) id<ViewControllerDelegate> delegate;

在swift中,常用的是

 weak var delegate: ViewControllerDelegate?

虽然这样做法是等价的,但是OC和swift协议类型的内部原理确实区别很大的。当我们通过协议类型创建一个变量的时候,这个变量会被包装到一个叫做存
在容器的盒子中。
这里引用自《swift进阶》:

对于普通的协议 (也就是没有被约束为只能由 class 实 现的协议),会使用不透明存在容器 (opaque existential container)。不透明存在容器中含有一 个存储值的缓冲区 (大小为三个指针,也就是 24 字节);一些元数据 (一个指针,8 字节);以及 若干个目击表 (0 个或者多个指针,每个 8 字节)。如果值无法放在缓冲区里,那么它将被存储到 堆上,缓冲区里将变为存储引用,它将指向值在堆上的地址。元数据里包含关于类型的信息 (比 如是否能够按条件进行类型转换等)。关于目击表,我们接下来会⻢上对它进行讨论。

目击表是让动态派发成为可能的关键。它为一个特定的类型将协议的实现进行编码:对于协议中的每个方法,表中会包含一个指向特定类型中的实现的入口。有时候这被称为 vtable。
这里举一个这样的例子:

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

然后,在协议的扩展中添加一个方法

extension Drawing {
        mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
            let diameter = radius * 2
            let origin = CGPoint(x: center.x - radius, y: center.y - radius) let size = CGSize(width: diameter, height: diameter)
            let rect = CGRect(origin: origin, size: size)
            addEllipse(rect: rect, fill: fill)
        }
    }

接着,我们在一个结构体的扩展中重写协议中的这个方法。(当然,提前让这个结构体遵守这个协议)

extension SVG {
        mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
            var attributes: [String:String] = [ "cx": "\(center.x)",
                "cy": "\(center.y)",
                "r": "\(radius)",
            ]
            attributes["fill"] = String(hexColor: fill)
            append(node: XMLNode(tag: "circle", attributes: attributes))
        }
    }

接下来实例化这个结构体,其中一个实例遵守协议,另外一个不遵守。然后分别调用addCircle方法,看看会发生什么事。

var sample = SVG()
sample.addCircle(center: .zero, radius: 20, fill: .red)
var otherSample: Drawing = SVG()
otherSample.addCircle(center: .zero, radius: 20, fill: .red)

两次的输出结果:

//第一次结果
<circle cy="0.0" fill="#010000" r="20.0" cx="0.0"/> </svg>


//第二次结果
<ellipse cy="-20.0" fill="#010000" ry="40.0" rx="40.0" cx="-20.0"/>

第二次返回的结果可知,它返回的是 ellipse 元素,而不是我们所期望的 circle。也就是说,它使用了协议扩展中的 addCircle 方法,而没有用 SVG 扩展中的。当我们将 otherSample 定义为 Drawing 类型的变 量时,编译器会自动将 SVG 值封装到一个代表协议的类型中,这个封装被称作存在容器 (existential container)。

知道了目击表, addCircle 的奇怪行为就很容易解释得通了。因为 addCircle 不是协议定义的一部分 (或者说,它不是协议所要求实现的内容),所以它也不在目击表中。因 此,编译器除了静态地调用协议的默认实现以外,别无选择。一旦我们将 addCircle 添加为协 议必须实现的方法,它就将被添加到目击表中,于是我们就可以通过动态派发对其进行调用了。

不透明存在容器的尺寸取决于目击表个数的多少,每个协议会对应一个目击表。举例来说, Any 是空协议的类型别名,所以它完全没有目击表:

typealias Any = protocol<>
MemoryLayout<Any>.size // 32

如果我们合并多个协议,每多加一个协议,就会多 8 字节的数据块。所以合并四个协议将增加 32 字节:

protocol Prot { }
protocol Prot2 { }
protocol Prot3 { }
protocol Prot4 { }
typealias P = Prot & Prot2 & Prot3 & Prot4
MemoryLayout<P>.size // 64

对于只适用于类的协议 (也就是带有 SomeProtocol: class 或者 @objc 声明的协议),会有一个 叫做类存在容器的特殊存在容器,这个容器的尺寸只有两个字⻓ (以及每个额外的目击表增加 一个字⻓),一个用来存储元数据,另一个 (而不像普通存在容器中的三个) 用来存储指向这个类 的一个引用:

protocol ClassOnly: AnyObject {} 
MemoryLayout<ClassOnly>.size // 16

从 Objective-C 导入 Swift 的那些协议不需要额外的元数据。所以那些类型是 Objective-C 协 议的变量不需要封装在存在容器中;它们在类型中只包含一个指向它们的类的指针:

MemoryLayout<NSObjectProtocol>.size // 8 
MemoryLayout<NSObjectProtocol>.size // 8

性能问题?

存在容器为代码调用添加了一层非直接层,所以相对于泛型参数,一般来说都会造成性能降低 (假设编译器能够对泛型代码进行特化处理)。除了可能更慢的方法派发以外,存在容器还扮演了 阻止编译器优化的壁垒⻆色。大多数时候,担忧这里的性能其实是过早优化。但是,如果你想 要获取最大化的性能的时候,使用泛型参数确实要比使用协议类型高效得多。通过使用泛型参 数,你可以避免隐式的泛型封装。
如果你尝试将一个 [String] (或者其他任何类型) 传递给一个接受 [Any] (或者其他任意接受协议 类型,而非具体类型的数组) 的函数时,编译器将会插入代码对数组进行映射,将每个值都包装 起来。这将使方法调用本身成为一个 O(n) 的操作 (其中 n 是数组中的元素个数),这还不包含 函数体的复杂度。同样的,大多数情况下这不会导致问题,但是如果你需要写高性能的代码, 你可能需要将你的函数写为泛型参数的形式,而不是使用协议类型

。。。to be continue

展开阅读全文

没有更多推荐了,返回首页