Swift 中的不透明类型、存在类型以及 some、any 关键字

Xcode 14 beta 3Swift 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")

编译器可以诊断出TextImage是不同的类型,因而抛出错误。假设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

这儿运用泛型束缚保证C1C2是调集类型,运用where分句保证二者的相关类型Element是可以判等的相同类型。功能虽已完成,但写起来十分繁琐,也不利于阅览。那么,该如何简化呢?

在简化之前,先来看看 Swift 5.7 新增的两个新特性:

  1. 运用范围更广的不透明类型

    此前,不透明类型只能用于回来值。现在,咱们还可以将其用于特点、下标以及函数参数。

  2. 首要相关类型

    协议支持多个相关类型,运用尖括号声明(类似泛型写法)的则是首要相关类型。

    如下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 {
  ...
}

c1c2可以是恣意调集类型,假如没有运用some符号,它便是下文说到的存在类型,编译器会提示运用any润饰。但这儿将其声明为不透明类型,根据以下两点:

  1. 旧函数在调用时就现已确定了入参的详细类型,这和any的表达的意思有悖。
  2. 此处的不透明类型并没有用作回来值,只是在函数被调用时的入参,其详细类型是固定的,没有必要运用any,这和旧函数表达的意图一致。

细心对比两个函数,可以发现:some PT 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的某个详细类型

在编译期间,f1p是存在类型(any P),它将p底层的详细类型包装在一个“容器”中。而在运转时,从容器中取出内容物才能得知p底层的详细类型。p的类型可被任何遵从协议P的某个详细类型进行替换,因而存在类型具有动态分发的特性。

比方下面的代码:

func f3() -> any P {
  Bool.random() ? CP1() : CP2()
}

f3的回来类型在编译期间是存在类型any P,但是在运转期间的详细类型是CP1CP2

f2中的p没有被“容器”包装,无需进行装箱、拆箱操作。由于泛型的束缚,当咱们调用该办法时,就现已确定了它的详细类型。无论是编译期还是运转时,它的类型都是详细的,这又称为静态分发。比方这样调用时:f2(CP1()),入参和回来值类型都就现已固化为CP1,在编译期和运转时都保持为该详细类型。

由于动态分发会带来一定的功能损耗,因而 Swift 引进了any关键字来向咱们警示存在类型的负面影响,咱们应该尽量防止运用它。

上面的示例代码不运用any关键字还能通过编译,但从 Swift 6 开端,当咱们运用存在类型时,编译器会强制要求运用any关键字符号,否则会报错。

在实际开发中,引荐优先运用泛型和some,尽可能地防止运用any,除非你真的需要一个动态的类型。


文中触及源码参阅:Source code。