不透明类型和封装协议类型
Swift 提供了两种隐藏值类型细节的方法:不透明类型(Opaque Type)和封装协议类型(Boxed Protocol Type)。
在隔离模块和调用模块的代码上,隐藏类型信息是有用的,因为这样返回值的底层类型可以保持私有。
返回不透明类型的函数或方法隐藏了其返回值的类型信息。函数会将返回值类型描述为一个遵循某种协议的类型,而非一个更具体的类型。
不透明类型会保留类型的身份信息 —— 编译器可以访问该类型信息,但模块的调用端则无法访问。
封装协议类型可以存储遵循给定协议的任何类型的实例。
封装协议类型不保留类型的身份信息 —— 值的具体类型在运行时才会被知道,并且随着不同的值被存储其中,它的具体类型可能会发生变化。
举个例子,假设你正在编写一个用 ASCII 字符绘制几何形状的程序模块。每个几何形状结构体的基本特征是有一个 draw() 函数,该函数返回表示那个几何形状的字符串,这样你就可以把这个基本特征作为 Shape 协议的要求之一:
protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
var size: Int
func draw() -> String {
var result: [String] = []
for length in 1...size {
result.append(String(repeating: "*", count: length))
}
return result.joined(separator: "\n")
}
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***
- 如下面的代码所示,你可以使用泛型来实现像垂直翻转某个几何形状这样的操作。然而,这种方法有一个重要的局限性:翻转后的结果会暴露用于创建该结果的确切的泛型类型。
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *
- 又比如下面这样的代码,这种方法定义了一个能将两个形状垂直连接起来的 JoinedShape<T: Shape, U: Shape> 结构体,如果把一个三角形与一个翻转过的三角形连接在一起,就产生了像 JoinedShape<Triangle, FlippedShape<Triangle>> 这样的类型。
struct JoinedShape<T: Shape, U: Shape>: Shape {
var top: T
var bottom: U
func draw() -> String {
return top.draw() + "\n" + bottom.draw()
}
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
返回一个不透明类型
- 你可以把不透明类型看作是泛型类型的反面。
- 泛型类型允许函数的调用端选择参数和返回值的类型,而这些类型与函数的实现是分离的。
- 例如,以下代码中的函数返回一个调用端指定的类型:
func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }
struct Square: Shape {
var size: Int
func draw() -> String {
let line = String(repeating: "*", count: size)
let result = Array<String>(repeating: line, count: size)
return result.joined(separator: "\n")
}
}
// 该函数会返回一个遵循 Shape 协议的某种给定类型的值,却可以不必指定任何特定的具体返回类型
func makeTrapezoid() -> some Shape {
let top = Triangle(size: 2)
let middle = Square(size: 2)
let bottom = FlippedShape(shape: top)
let trapezoid = JoinedShape(
top: top,
bottom: JoinedShape(top: middle, bottom: bottom)
)
return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *
- 你还可以将不透明返回类型与泛型结合使用。以下代码中的函数都返回了遵循 Shape 协议的某种类型的值。
func flip<T: Shape>(_ shape: T) -> some Shape {
return FlippedShape(shape: shape)
}
func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
JoinedShape(top: top, bottom: bottom)
}
let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *
- 如果一个返回不透明类型的函数从多处返回值,则所有可能的返回值必须具有相同的类型。对于一个泛型函数,它可以使用函数的泛型参数作为其返回类型,但这个返回类型仍然必须是相同的某个单一类型。例如,下面是一个不合法的形状翻转函数版本,它包含了正方形的一个特例:
func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
if shape is Square {
return shape // 错误:返回类型不一致
}
return FlippedShape(shape: shape) // 错误:返回类型不一致
}
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
if shape is Square {
return shape.draw()
}
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
- 始终返回同一种单一类型的要求并不妨碍你在不透明返回类型中使用泛型。以下是一个示例函数,它将它的类型参数作为其返回值的基础类型:
func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
return Array<T>(repeating: shape, count: count)
}
封装协议类型
封装协议类型有时也被称为存在类型(existential type),这个术语源于这样的一个表达:“存在一个类型 T,使得 T 遵循该协议”。要创建一个封装协议类型,在协议名称前加上 any。
struct VerticalShapes: Shape {
var shapes: [any Shape]
func draw() -> String {
return shapes.map { $0.draw() }.joined(separator: "\n\n")
}
}
let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())
- 你可以在知道被封装值的基础类型时使用一个 as 来进行类型转换。例如:
if let downcastTriangle = vertical.shapes[0] as? Triangle {
print(downcastTriangle.size)
}
// 打印输出 "5"
不透明类型与封装协议类型之间的区别
函数返回一个不透明类型与返回一个封装协议类型看起来非常相似,但这两种返回类型在是否保留类型的身份信息上有所不同。
不透明类型指的是某种特定类型,尽管函数的调用者无法看到是哪种具体类型;而封装协议类型可以指任何遵循该协议的类型。
一般来说,封装协议类型在存储值的底层类型上提供了更多的灵活性,而不透明类型则需要你对这些底层类型做出更严格的保证。
你不能将 Container 用作函数的返回类型,因为该协议具有一个关联类型(Associated Type)。你也不能将其用作泛型返回类型的约束,因为在函数体外部没有足够的信息来推断泛型类型需要是什么具体类型
protocol Container {
associatedtype Item
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container { }
// 错误:具有关联类型的协议不能用作返回类型。
func makeProtocolContainer<T>(item: T) -> Container {
return [item]
}
// 错误:没有足够的信息来推断 C 的类型。
func makeProtocolContainer<T, C: Container>(item: T) -> C {
return [item]
}
- 使用不透明类型 some Container 作为返回类型,可以表达想要的 API 合约 —— 函数返回一个容器,但不指定容器的具体类型:
func makeOpaqueContainer<T>(item: T) -> some Container {
return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// 打印输出 "Int"
不透明参数类型
除了使用 some 来返回不透明类型外, 你也可以在函数、下标或构造器的参数类型中使用 some。 然而,当你在参数类型中使用 some 时, 它仅仅是泛型的简写语法,而不是不透明类型。 例如, 下面的两个函数是等价的:
func drawTwiceGeneric<SomeShape: Shape>(_ shape: SomeShape) -> String {
let drawn = shape.draw()
return drawn + "\n" + drawn
}
func drawTwiceSome(_ shape: some Shape) -> String {
let drawn = shape.draw()
return drawn + "\n" + drawn
}
- 如果你在多个参数的类型前都写了 some, 每个泛型类型都是独立的。
return s1.draw() + "\n" + s2.draw()
}
combine(smallTriangle, trapezoid)
Swift中的不透明类型和封装协议类型有什么区别?
Swift 中的不透明类型(Opaque Types)和封装协议类型(Protocol Types)都能隐藏具体类型信息,但它们的实现机制和适用场景有本质区别。
1. 语法差异
- 不透明类型:使用 some 协议名 语法,返回值类型由函数内部确定,调用者无需知道具体类型。
func createShape() -> some Shape {
return Circle() // 具体类型被隐藏
}
- 封装协议类型:直接使用协议名作为类型,具体类型在运行时动态确定。
func createShape() -> Shape {
return Circle() // 类型被封装为 Shape
}
2. 类型一致性
- 不透明类型:保证每次调用返回的是同一具体类型(但对外隐藏)。
func alwaysCircle() -> some Shape {
return Circle() // 始终返回 Circle,只是类型不公开
}
- 封装协议类型:允许返回不同具体类型,只要符合协议即可。
func randomShape() -> Shape {
return Bool.random() ? Circle() : Square() // 动态类型
}
3. 反向引用能力
- 不透明类型:保留具体类型信息,支持协议中依赖具体类型的操作(如关联类型)。
protocol Container {
associatedtype Item
func getItem() -> Item
}
func createIntContainer() -> some Container {
struct IntContainer: Container {
func getItem() -> Int { 42 }
}
return IntContainer() // 正确:具体类型满足关联类型要求
}
- 封装协议类型:丢失具体类型信息,无法满足需要具体类型的约束。
func createContainer() -> Container { // 错误:协议类型不能直接使用关联类型
return IntContainer()
}
4. 底层实现
- 不透明类型:通过类型擦除(Type Erasure)实现,但保留了具体类型的元数据,编译器可以进行静态分发(Static Dispatch),性能更高。
- 封装协议类型:使用动态分发(Dynamic Dispatch),运行时通过虚表(Virtual Table)调用方法,性能略低。
5. 适用场景
- 支持关联类型,使用不透明类型,不能用封装协议类型
- 动态多态(运行时切换类型):封装协议类型主要应用场景;不透明类型不支持(返回类型必须固定);(或者需使用 any)
6. 示例对比
- 不透明类型示例
// 隐藏视图实现细节(SwiftUI常用)
func makeView() -> some View {
Text("Hello")
}
- 封装协议类型示例
// 动态多态
let shapes: [Shape] = [Circle(), Square()]
for shape in shapes {
print(shape.draw()) // 运行时确定具体类型
}
自动引用计数
- Swift 使用 自动引用计数 (ARC)来跟踪和管理应用的内存使用。
- 在大多数情况下,这意味着 Swift 中的内存管理”自动运行”,你不需要自己考虑内存管理。
- 当类实例不再需要时,ARC 会自动释放这些实例使用的内存。
- 然而,在少数情况下,ARC 需要更多关于代码各部分之间关系的信息,以便为你管理内存。
- 为了确保实例在仍然需要时不会消失,ARC 会跟踪当前有多少属性、常量和变量正在引用每个类实例。只要至少还存在一个对该实例的活动引用,ARC 就不会释放该实例。
- 为了实现这一点,每当你将类实例分配给属性、常量或变量时,该属性、常量或变量就会对该实例进行 强引用 。之所以称之为”强”引用,是因为它牢牢地持有该实例,只要该强引用存在,就不允许释放该实例。
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"
强引用循环
一个类的实例 永远 不会到达它有零个强引用的时刻。如果两个类实例彼此持有强引用,使得每个实例都使对方保持活跃,就可能发生这种情况。这被称为 强引用循环 。
你可以通过将类之间的某些关系定义为弱引用或无主引用,而不是强引用,来解决强引用循环。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
// 注意,当你将这两个变量设置为 nil 时,两个析构器都没有被调用。强引用循环阻止了 Person 和 Apartment 实例被释放,导致你的应用出现内存泄漏。
john = nil
unit4A = nil
- Swift 提供了两种方法来解决你使用类类型的属性时出现的强引用循环:弱引用和无主引用。
- 弱引用 是一种不会对其引用的实例保持强持有的引用,因此不会阻止 ARC 处置被引用的实例。
- 你可以通过在属性或变量声明前放置 weak 关键字来表示一个弱引用。
- 因为弱引用不会对其引用的实例保持强持有,所以有可能在弱引用仍然引用该实例时,该实例被释放。
- 当弱引用所引用的实例被释放时,ARC 会自动将弱引用设置为 nil。
- 下面的例子与上面的 Person 和 Apartment 例子相同,只有一个重要的区别。这次,Apartment 类型的 tenant 属性被声明为弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
// Prints "John Appleseed is being deinitialized"
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
- 像弱引用一样, 无主引用 不会对它引用的实例保持强持有。
- 然而,与弱引用不同,无主引用是在另一个实例具有相同或更长的生命周期时使用的。
- 你通过在属性或变量声明前放置 unowned 关键字来表示一个无主引用。
- 与弱引用不同,无主引用总是被期望有一个值。
- 将一个值标记为无主不会使它成为可选的,ARC 也永远不会将无主引用的值设置为 nil。
- 只有当你确定该引用 总是 指向一个尚未被释放的实例时,才使用无主引用。
- 如果你在该实例被释放后尝试访问无主引用的值,你会得到一个运行时错误。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
// 因为信用卡总是会有一个客户,你将其 customer 属性定义为无主引用,以避免强引用循环:
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
- 你可以将对类的可选引用标记为无主的。在 ARC 所有权模型中,无主可选引用和弱引用可以在相同的上下文中使用。
- 不同之处在于,当你使用无主可选引用时,你有责任确保它始终引用一个有效的对象或被设置为 nil。
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
let department = Department(name: "Horticulture")
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
- 两个 属性都应该始终有值,一旦初始化完成,两个属性都不应该为 nil。在这种情况下,将一个类上的无主属性与另一个类上的隐式解包可选属性结合使用是很有用的。
class Country {
let name: String
// 为了应对这个要求,你将 Country 的 capitalCity 属性声明为隐式解包可选属性,通过在其类型注解末尾加上感叹号(City!)来表示。这意味着 capitalCity 属性有一个默认值 nil
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
如果你将一个闭包分配给类实例的一个属性,并且该闭包的主体捕获了该实例,也可能发生强引用循环。这种捕获可能发生是因为闭包的主体访问了该实例的一个属性,例如 self.someProperty,或者因为闭包在该实例上调用了一个方法,例如 self.someMethod()。无论哪种情况,这些访问都导致闭包”捕获”self,创建了一个强引用循环。
Swift 为这个问题提供了一个优雅的解决方案,称为 闭包捕获列表 。
你可以通过在闭包的定义中定义一个捕获列表来解决闭包和类实例之间的强引用循环。
捕获列表定义了在闭包体内捕获一个或多个引用类型时要使用的规则。就像两个类实例之间的强引用循环一样,你声明每个被捕获的引用为弱引用或无主引用,而不是强引用。
选择弱引用还是无主引用取决于代码不同部分之间的关系。
捕获列表中的每个项目都是 weak 或 unowned 关键字与对类实例的引用(如 self )或初始化为某个值的变量(如 delegate = self.delegate )的配对。这些配对写在一对方括号内,用逗号分隔。
如果提供了闭包的参数列表和返回类型,则将捕获列表放在它们之前:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
- 如果闭包没有指定参数列表或返回类型,因为它们可以从上下文推断出来,则将捕获列表放在闭包的最开始,后面跟着 in 关键字:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}
- 当闭包和它捕获的实例总是相互引用,并且总是同时被释放时,将闭包中的捕获定义为无主引用。
- 当被捕获的引用在将来的某个时刻可能变为 nil 时,将捕获定义为弱引用。弱引用总是可选类型,并且当它们引用的实例被释放时自动变为 nil。这使你能够在闭包体内检查它们是否存在。
- 注意:如果被捕获的引用永远不会变为 nil,它应该始终被捕获为无主引用,而不是弱引用。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
paragraph = nil
// Prints "p is being deinitialized"
- 实际使用中,weak用得比较多,遇到不能为nil的特殊情况,才考虑用unowned
内存安全
默认情况下,Swift 会阻止代码中不安全的行为。例如,Swift 会确保所有变量都在使用前被初始化、内存不可在被释放后访问、还会对数组的下标做越界检查。
Swift 会要求修改内存的代码拥有对被修改区域的独占访问权,以确保对同一块内存区域的多次访问不会产生冲突。
读操作和写操作之间的区别是显而易见的:写操作会改变内存区域,而读操作不会。「内存区域」指的是被访问的内容(例如变量、常量、属性)。内存访问的要么瞬间完成,要么持续较长时间。
这种长时保持的写访问带来的问题是:即便作用域和访问权限规则允许,你也不能再访问以 in-out 形式传入的原始变量。这是因为任何访问原始变量的行为都会造成冲突,例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// 错误:stepSize 访问冲突
- 其中一个解决冲突的方式是显式地复制一份 stepSize:
// 显式复制
var copyOfStepSize = stepSize
increment(©OfStepSize)
// 更新原来的值
stepSize = copyOfStepSize
// stepSize 现在的值是 2
- 对于 in-out 参数保持长时写访问的另一个后果是,往同一个函数的多个 in-out 参数里传入同一个变量也会产生冲突。例如:
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore) // OK
balance(&playerOneScore, &playerOneScore)
// 错误:playerOneScore 访问冲突
- 一个结构体的变值(mutating)方法会在其被调用期间保持对于 self 的长时写访问。
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK
oscar.shareHealth(with: &oscar)
// 错误:对 oscar 的访问出现冲突
- 结构体、元组、枚举这样的类型是由多个独立的值构成的(例如结构体的属性、元组的元素)。它们都是值类型,所以修改值的任何一部分都是对于整个值的修改。这意味着对于其中任何一个属性的读或写访问,都需要对于整个值的访问。例如,对于元组元素的重叠写访问会造成冲突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 错误:对 playerInformation 的属性访问有冲突
- 下方的代码展示了对一个存储在全局变量中的结构体的重叠写访问,这会导致相同的错误。
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 错误
- 特别地,在以下条件满足时,编译器就可以证明对结构体属性的重叠访问是安全的:
- 代码只访问了实例的存储属性,而没有访问计算属性或类属性
- 结构体是本地变量,而非全局变量的值
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了