首页 Swift中的不透明类型
文章
取消

Swift中的不透明类型

在Swift编程语言中,不透明类型(Opaque Types)作为一种抽象类型可以隐藏底层实现细节。这篇博客将深入介绍Swift中的不透明类型,并提供一些有用的示例和最佳实践。

什么是不透明类型?

一个类型被认为是不透明类型,当它能够隐藏其底层实现细节并仅公开必要的接口。此外,不透明类型也可用于封装相关类型之间的关系,从而使代码更加模块化和易于管理。

Swift使用some关键字来定义不透明类型,它表示该类型是某个符合特定协议的关联类型的任意实现,但具体的实现细节是未知的。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protocol Vehicle {
    func start()
}

struct Car: Vehicle {
    func start() {
        print("Starting car engine")
    }
}

struct Truck: Vehicle {
    func start() {
        print("Starting truck engine")
    }
}

func startEngine(vehicle: some Vehicle) {
    vehicle.start()
}

在上面的示例中,我们定义了一个Vehicle协议,以及两个符合该协议的结构体类型:CarTruck。然后,我们定义了一个接受不透明类型的函数startEngine(),该类型必须符合Vehicle协议。通过使用some关键字,我们可以在不暴露具体实现的情况下,将CarTruck实例传递给startEngine()函数进行处理。

不透明类型的优势

使用不透明类型有几个优点。首先,它可以帮助我们隐藏底层实现细节,从而提高代码的安全性和稳定性。例如,在我们的示例中,startEngine()函数不需要知道车辆类型的内部细节,它只需要知道每个类型都符合Vehicle协议即可。这使得代码更具灵活性,因为我们可以随时添加新的符合Vehicle协议的类型,而无需更改startEngine()函数的实现。

另外一个好处是,不透明类型可以大幅简化代码,使其更加易于理解和维护。例如,在我们的示例中,如果我们没有使用不透明类型,那么我们可能需要为每种类型分别定义一个函数。但是通过使用不透明类型,我们可以将这些实现放在同一个函数中,从而避免了重复的代码和冗余的逻辑。

如何使用不透明类型

使用不透明类型的最佳实践是将其用于封装相关类型之间的关系,从而使代码更加模块化和易于管理。例如,在我们的示例中,Vehicle协议可以用于封装汽车类和卡车类之间的共同行为,而不需要暴露它们的实现细节。这种抽象化使得代码更加易于理解,并且使得我们可以轻松地替换或添加新的类型,而无需修改现有代码。

除了封装相关类型之间的关系,不透明类型还可以用于创建通用API。例如,假设我们正在开发一个UI组件库,并且需要一个通用的方法来显示内容。我们可以使用不透明类型来定义此函数,并且可以在该函数中使用some View类型来表示任意视图。

1
2
3
func showContent(content: some View) {
    // display the content
}

在这个例子中,我们使用不透明类型作为参数类型,该类型表示任何符合View协议的视图,并命名为content。这使得我们可以在该函数中使用任何符合View协议的视图类型,例如文本、按钮等。

不透明类型与泛型

不透明类型和泛型有些相似,但它们之间也有一些区别。首先,不透明类型只能用于返回类型或参数类型,而泛型可以用于函数的任意位置。其次,不透明类型可以用于封装多个相关类型之间的关系,而泛型则通常用于处理单个类型。

以下是一个使用泛型的示例:

1
2
3
4
5
6
7
8
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

在上面的示例中,我们定义了一个泛型函数findIndex(),它接受一个要查找的值和一个数组,并返回该值在数组中的索引。注意,我们使用了泛型类型<T>Equatable协议来表示我们要查找的类型必须符合Equatable协议。

与此不同,以下是一个使用不透明类型的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol Vehicle {
    func start()
}

struct Car: Vehicle {
    func start() {
        print("Starting car engine")
    }
}

func startEngine(vehicle: some Vehicle) {
    vehicle.start()
}

在这个示例中,我们定义了一个Vehicle协议和一个符合该协议的结构体类型Car。然后,我们定义了一个函数startEngine(),它接受一个不透明类型作为参数,并调用该类型的start()方法。

需要注意的是,虽然这两个示例看起来很相似,但它们的目的和应用场景是不同的。泛型通常用于处理单个类型,例如一个数组中的元素类型,而不透明类型通常用于封装多个相关类型之间的关系,例如封装汽车和卡车类型之间的共同行为。

关联类型

有时候我们可能需要在协议中使用关联类型,比如下面这个例子:

1
2
3
4
5
protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

在这个协议中,我们定义了一个关联类型Item,用于表示容器中的元素类型。然后我们还定义了两个属性和一个下标,用于访问容器中的元素。

我们可以通过遵循Container协议来实现一个不透明类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct IntStack: Container {
    typealias Item = Int
    
    var items = [Int]()
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

func makeContainer() -> some Container {
    return IntStack(items: [1, 2, 3])
}

let container = makeContainer()
print(container.count) // 输出 "3"

在上面的例子中,我们实现了一个IntStack结构体,它遵循了Container协议,并且将关联类型Item指定为Int。然后我们使用makeContainer()函数返回了一个不透明类型,这个不透明类型也遵循了Container协议,我们可以像使用任何实现了该协议的类型一样使用它。

多个不透明类型

我们也可以在同一个函数中返回多个不透明类型。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func randomBool() -> Bool {
    return arc4random_uniform(2) == 0
}

func foo() -> (some Equatable, some View) {
    if randomBool() {
        return ("Hello", Text("World"))
    } else {
        return (42, Button("Click me!"))
    }
}

let (a, b) = foo()
print(a == "Hello") // 输出 "true"
print(b) // 输出 "Text("World")" 或者 "Button(_:)"

在上面的例子中,foo()函数会随机返回一个字符串和一个文本视图,或者一个整数和一个按钮视图。由于我们并不知道具体的类型,所以使用some关键字来定义不透明类型。最终,我们可以像使用任何其他类型一样对它们进行操作。

结构体和枚举中的不透明类型

除了协议和类,我们也可以在结构体和枚举中使用不透明类型。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
enum Shape {
    case rectangle(some View and some Shape)
    case circle(some View and some Shape)
}

struct MyView: View {
    var body: some View {
        return Shape.rectangle(Color.red, RoundedRectangle(cornerRadius: 10))
    }
}

在上面的代码中,我们定义了一个Shape枚举,在其中包含了矩形和圆形两种形状。每个成员都包含了一个不透明类型的视图和一个不透明类型的形状。

然后我们定义了一个名为MyView的结构体,它遵循了View协议,并且返回了一个矩形形状和红色颜色的视图。在这里,我们使用了some关键字来定义不透明类型的视图和形状。

需要注意的是,在结构体或枚举中使用不透明类型时,我们必须显式地将其指定为some类型。此外,我们也不能将不透明类型作为结构体或枚举中的属性类型。

限制

尽管不透明类型非常灵活,但是它们也有一些限制。具体来说,我们不能将不透明类型用作函数参数或返回值类型的泛型参数,例如:

1
2
3
4
5
6
7
8
9
// 无法编译通过
func myFunction<T>(arg: some T) { // Error: Cannot use an opaque type as a generic parameter
    // ...
}

// 无法编译通过
func myFunction() -> some Equatable { // Error: Opaque return types must have a fixed underlying type
    // ...
}

此外,我们还需要注意以下几点:

  • 不透明类型必须显式地指定遵循的协议或继承的类。
  • 如果我们需要在函数中使用多个不透明类型,它们的类型必须相同。
  • 不透明类型只能用于变量、常量或函数返回值,它们不能被直接用作结构体或类中的属性。
本文由作者按照 CC BY 4.0 进行授权

Swift中的泛型

Swift中的自动引用计数