Xcode 14 beta 3
Swift 5.7
不透明类型、some 关键字
some
关键字由 Swift 5.1 引进,它用来润饰某个协议,使之成为不透明类型。
不透明类型是躲藏类型信息的笼统类型,其底层的详细类型不可动态改动。
初度接触 SwiftUI 的读者会看到这样的代码:
var body: some View {
Text("Hello")
}
body
是不透明类型some View
,调用者只知其是一个遵从View
协议的笼统类型,却不知其底层的详细类型(Text
),由于不透明类型对调用者躲藏了类型信息。
这儿的”不可见“是对调用者而言的,而编译器具有”透视“视角,它可以在编译期获取到不透明类型底层的详细类型(Text
),并保证其底层类型是静态的。
假如在body
内这样写:
Bool.random() ? Text("Hello") : Image(systemName: "swift")
编译器可以诊断出Text
和Image
是不同的类型,因而抛出错误。假设body
内部可以动态地改动其底层的详细类型,这意味着更多的内存占用和复杂核算,这会导致程序的功能损耗。
根据以上特性,不透明类型十分适合在模块之间调用,它可以维护类型信息为私有状态而不被露出,而编译器可以拜访类型信息并作出优化工作。
不透明类型受完成者束缚,这和泛型受调用者束缚是相反的。因而,不透明类型又被称为反向泛型。比方下面的代码:
func build1<V: View>(_ v: V) -> V {
v
}
// v1 is Text
let v1 = build1(Text("Hello"))
func build2() -> some View {
Text("Hello")
}
// v2 is View
let v2 = build2()
调用build1
时就需要指定详细类型,此处入参为Text
类型,因而v1
的类型也是Text
。
build2
回来的详细类型由内部完成决定,这儿回来的是Text
类型。鉴于不透明类型对调用者躲藏了类型信息,因而v2
的类型在编译期是View
,在运转时是Text
。
更高雅的泛型
下面的代码用于比较两个调集,假如一切元素相同,回来 true。
func compare<C1: Collection, C2: Collection>(_ c1: C1, _ c2: C2) -> Bool
where C1.Element == C2.Element, C1.Element: Equatable {
if c1.count != c2.count { return false }
for i in 0..<c1.count {
let v1 = c1[c1.index(c1.startIndex, offsetBy: i)]
let v2 = c2[c2.index(c2.startIndex, offsetBy: i)]
if v1 != v2 {
return false
}
}
return true
}
let c1: [Int] = [1, 2, 3]
let c2: Set<Int> = [1, 2, 3]
let ans = compare(c1, c2) // true
这儿运用泛型束缚保证C1
和C2
是调集类型,运用where
分句保证二者的相关类型Element
是可以判等的相同类型。功能虽已完成,但写起来十分繁琐,也不利于阅览。那么,该如何简化呢?
在简化之前,先来看看 Swift 5.7 新增的两个新特性:
-
运用范围更广的不透明类型
此前,不透明类型只能用于回来值。现在,咱们还可以将其用于特点、下标以及函数参数。
-
首要相关类型
协议支持多个相关类型,运用尖括号声明(类似泛型写法)的则是首要相关类型。
如下
Collection
协议中的Element
,便是首要相关类型。借助这一特性,在运用具有相关类型的协议时,写法可以十分简练。比方上面的
where
分句,咱们可以简写成Collection<Equatable>
。public protocol Collection<Element> : Sequence { associatedtype Element associatedtype Iterator = IndexingIterator<Self> ... }
将以上两点结合起来,更高雅的写法如下:
func compare<E: Equatable>(_ c1: some Collection<E>, _ c2: some Collection<E>) -> Bool {
...
}
c1
和c2
可以是恣意调集类型,假如没有运用some
符号,它便是下文说到的存在类型,编译器会提示运用any
润饰。但这儿将其声明为不透明类型,根据以下两点:
- 旧函数在调用时就现已确定了入参的详细类型,这和
any
的表达的意思有悖。 - 此处的不透明类型并没有用作回来值,只是在函数被调用时的入参,其详细类型是固定的,没有必要运用
any
,这和旧函数表达的意图一致。
细心对比两个函数,可以发现:some P
和T where T: P
表达的意思其实是一样的。假如P
带有相关类型E
,那么T where T: P, T.E: V
可以简写为some P<V>
。
存在类型、any 关键字
any
关键字由 Swift 5.6 引进,它用来润饰存在类型:一个可以容纳恣意遵从某个协议的的详细类型的容器类型。
咱们结合下面的代码来了解这段笼统的描绘:
protocol P {}
struct CP1: P {}
struct CP2: P {}
func f1(_ p: any P) -> any P {
p
}
func f2<V: P>(_ p: V) -> V {
p
}
f1
中的p
及其回来值都是存在类型,只要是遵从协议P
的类型实例都是合法的。
f2
中的p
及其回来值都不是存在类型,而是遵从协议P
的某个详细类型。
在编译期间,f1
中p
是存在类型(any P
),它将p
底层的详细类型包装在一个“容器”中。而在运转时,从容器中取出内容物才能得知p
底层的详细类型。p
的类型可被任何遵从协议P
的某个详细类型进行替换,因而存在类型具有动态分发的特性。
比方下面的代码:
func f3() -> any P {
Bool.random() ? CP1() : CP2()
}
f3
的回来类型在编译期间是存在类型any P
,但是在运转期间的详细类型是CP1
或CP2
。
而f2
中的p
没有被“容器”包装,无需进行装箱、拆箱操作。由于泛型的束缚,当咱们调用该办法时,就现已确定了它的详细类型。无论是编译期还是运转时,它的类型都是详细的,这又称为静态分发。比方这样调用时:f2(CP1())
,入参和回来值类型都就现已固化为CP1
,在编译期和运转时都保持为该详细类型。
由于动态分发会带来一定的功能损耗,因而 Swift 引进了any
关键字来向咱们警示存在类型的负面影响,咱们应该尽量防止运用它。
上面的示例代码不运用any
关键字还能通过编译,但从 Swift 6 开端,当咱们运用存在类型时,编译器会强制要求运用any
关键字符号,否则会报错。
在实际开发中,引荐优先运用泛型和some
,尽可能地防止运用any
,除非你真的需要一个动态的类型。
文中触及源码参阅:Source code。