Nate cook   cover

Swift 皇冠上的明珠:不安全的 Swift 和指针类型

Swift 能够为我们提供优异的性能,同时还能够通过强类型 (strong types)、值语义 (value semantics) 和自动内存管理 (ARC, automatic memory management) 来为我们提供安全性。不过当您想要跨越这些安全边界的时候,Swift 也提供了直接分配和操作内存的工具。本次讲演将探讨 Swift 中对于指针使用的相关注意事项:类型指针 (typed pointer)、原始指针 (raw pointer) 和缓冲区 (buffer) 的使用,以及隐式桥接 (implicit bridging) 和隐式转换 (casting),此外还有一些在使用不安全的 API 的时候,如何保证安全的小贴士。


概述

我在这里将与大家讨论 Swift 皇冠上的明珠 (pointy bits),也就是 Swift 标准库中所谓的不安全的指针类型。我们将来看一看不同的指针类型是如何工作的、探讨为什么它们要按照这种方式来运作,并将它们的设计方式探索出来,最后讨论我们该如何用安全的方式来使用这些它们。

是什么让 Swift 如此安全的呢?

在我们探索指针类型,以及研究为什么指针类型不安全之前,我首先想从「是什么让 Swift 如此安全的呢」这个话题开始。Swift 通常被设计为是一种尽可能优先考虑安全性的一门语言,但是我不是很确定自己真的清楚 Swift 当中的安全性意味着什么。当 Swift 首次推出的时候,我觉得这种特性听起来真的很棒。Swift 是非常安全的。这样我就可以写一个永远不会崩溃的程序出来。这真是太了不起了。就某些方面而言,Swift 也的确履行了这一承诺。可选值 (optionals) 显然是安全性的重要组成部分之一,因为它们可以很容易地处理诸如空指针之类的问题。

例如,假设我有一个关于年龄的数组,之后我会将这个数组用于执行一些计算,让我们来看一看 Swift 的安全性是如何保护我免受崩溃困扰的。

let ages: [Double] = []
let firstPlusOne = ages.first! + 1

这就是可选值阻止我生成错误的情形之一。数组的 first 属性产生的是一个可选值。因此如果我尝试向这个值加 1 的话,编译器就会让我知道这里发生了问题。感谢 Swift 编译器的帮助。那么我该如何修复这个问题呢?很简单。选择 “Fix all in scope” 这个选项即可。现在,可选属性被解包,这样我们就可以得到数组第一个值,然后用来加 1。非常棒。

有没有人觉得这里有问题呢?没问题,好吧。给我的修改提示是在 first 后面添加一个强制解包运算符。那么,如果我的数组为空,会发生什么呢?会发生崩溃。这就是我的错误了,对吧?我们知道有很多可以安全解开可选值的方法。我们可以使用可选值绑定。

我们还可以使用空合运算符。这样就可以非常优雅地来处理没有值时候的情形,而不会导致崩溃的发生。另一方面,使用强制解包运算符是非常容易引起崩溃的。

let ages = [13.3, 17.5, 18.9, 21.2]
let average = ages.reduce(0, +) / Double(ages.count)
// average = 17.725

接下来,我的任务是计算这个列表中所有值的平均值。我有一个很好用的函数式方法来计算平均值。

我完全没必要使用循环去将数组中的值相加。我可以使用 reduce,然后再除以数组的元素总数即可。

这就是我喜欢的 Swift 编码方式。这段代码非常干净、可读性也很高,这里也不用去担心任何可选值之类的东西。

let ages: [Double] = []
let average = ages.reduce(0, +) / Double(ages.count)
// => 等价于
// let average = ages.reduce(0, +) / 0

好吧,我们再来看一下,假设我的数组为空,导致我除以的时候除数变成了 0。如果我们除以 0 的时候,会发生什么呢?我们的程序发生了崩溃,即便这段代码看起来在 Swift 中没啥问题。

let ages = [13.3, 17.5, 18.9, 21.2]
let last = ages[4] // 缓冲溢出错误
// 好吧,看起来,没什么问题

Receive news and updates from Realm straight to your inbox

最后再举一个例子。我想要从这段列表中找到最后一个年龄。这里有1、2、3、4,好的,所有我直接取下标 4,等待 1 分钟。发现索引超出了范围,对吧?

所以这就是所谓的「缓冲溢出错误 (off by one)」。发生了崩溃。这里发生了什么呢?我觉得我写的代码没啥问题啊,我遵循了 Swift 的规则,但是我的程序还是崩溃了。

那么这真的是一门安全的语言么?为什么这么安全的语言会如此容易地发生崩溃呢?或者说我换另一种方式来表述:如果 Swift 具备安全性,那么就意味着 Swift 就不应该会发生崩溃,这是什么意思呢?Swift保证了哪些地方的安全呢?

让我们回到上一步来,来看一看我所引出的这个「缓冲溢出错误」。

我肯定在某些地方搞砸了。索引 4 当中并没有任何元素,这导致我的程序发生了崩溃,因为我尝试去访问不存在的元素。那么 Swift 是保证了哪些地方的安全呢?还有什么情况会比崩溃更糟糕的呢?大家知道什么会比程序崩溃更糟糕的吗?正是我的程序并没有崩溃。如果我的程序没有崩溃,如果它只是强行让这个错误不发生崩溃,那么鬼知道我取的这个数当中会有什么数据。

未预料的行为

这种未预料的行为在模拟器当中可能是有意义的,但是在设备上确实一场灾难。或者说当我在调试模式下进行编译时程序或许可以工作,但是在发布的时候却崩溃了。或者说这种未预料的行为或许不会立即出现,但是很可能会导致我的程序出现问题,这使得我们很难去调试和追踪问题发生在哪里。

Swift 保证的正是让我们免受未预料行为的困扰。通过诸如类型安全、值语义、集合边界、数字类型以及通过提供自动内存管理来保证我们免受未预料行为的困扰。Swift 是安全的,而其他语言并不能承诺总是如此。然而,有些时候我们仍然有需要跨过这些限制。当我们需要需要更精细的控制、更好的性能,而 Swift 的安全机制无法提供的时候,这样我们就需要使用到不安全 API 了。

Swift 的不安全 API

当我们跨过这些边界之后,Swift 对我们的提示非常清晰。每个指针类型的名称上都添加了 Unsafe 标签,这是故意看起来有点吓人的。当您在使用 Swift 的不安全 API 的时候,您会放弃该语言所提供的一些安全性,并且要自行保证安全。借助指针,您可以直接访问和读取内存中的内容。

内存布局和 Swift 中的指针

现在我将快速介绍一下内存、内存布局 (memory layout) 和指针是如何在 Swift 当中工作的,因此之后我们在编写实际 Swift 代码的时候,我们会停留在同一个幻灯片当中。

首先,我们来看一看内存本身。当我们运行一个用 Swift 编写的程序时,计算机内存中的某一部分 (chunk) 将存放我们在程序中所编写的类型和函数,每当我们创建变量或者常量的时候,这个变量的值都是用内存中的二进制所表示的。这就是内存。

当然,二进制就是简单的 1 和 0,电脑内存当中堆砌了无数个 1 和 0。我们通常将八个一组的 1 和 0 称之为字节 (bytes),就像这样。如果我们使用的是十六进制而不是二进制来编写的话,那么看起来则像这样。如果我们将这些字节折叠起来,就能够得到内存数据的一个很棒的紧凑视图。这些行中,每一行代表了 8 个字节,也就是 64 位 (bits),当我们使用 64 位处理器的时候,这就称之为一个字 (word),这也正是如今我们运行的所有设备的内存样式。

内存中的每一个位置都有一个地址,以便我们可以在内存中存储和检索值。我们可以在左边添加这些地址表述。地址也是使用十六进制表述的。如果仔细观察的话,您会发现每行的地址实际上都比上一行要多八位。同样,每一行表示的都是八个字节。所以我们是在字节级别来处理内存的。即使我们是以位(或者说 1 和 0 的组合)来开始的,但就存储器地址而言,字节是最小的单位。

我们在程序中创建的任何值都需要存储在内存当中,您可以在这里看到。

var age = 5

withUnsafePointer(to: &age) {
  // ...
}

age 是一个 Int 类型的数据,它表达的是一个字大小的有符号整数。所以在 64 位设备当中,这个值是使用一个字的全部 64 位来存储的。在这个表当中,age 变量使用了一行内存空间。当我使用 Swift 的 withUnsafePointer 函数的时候,我就能够临时访问一个指针,这个指针直接指向这个变量,而不是指向值本身。

在这里,我将使用 & 符号来传递 age 变量,在这里,它与 C 语言中传统的 & 符号意义非常接近。在 withUnsafePointer 后面所执行的尾随闭包中,我将得到一个指针参数,这本质上就是使用 age 的地址,而不是使用值本身。

var age = 5

withUnsafePointer(to: &age) { agePointer in
  print(agePointer.pointee)
}
// > 5

这里的 agePointer 参数表示的是 age 变量的地址,由 agePointer 表示这个值在内存当中的位置,而不是这个值本身。如果我需要访问指针所指向的值的话,那么我就使用指针的 pointee 属性。

总而言之,指针是使用或操纵内存中值位置的方式,而不是对值本身进行操作的方式。

不安全指针类型

这里我们终于来介绍 Swift 指针类型了。标准库当中存在八种不安全指针类型。其中有四种指针,另外四种 Swift 称之为缓冲区指针 (buffer pointer)。

我们先来看一下四种指针类型。好的,它们出现了。我们有 UnsafePointer<T>UnsafeRawPointerUnsafeMutablePointer<T>UnsafeMutableRawPointer。这些基本上都是对内存地址的封装。在底层都只是无符号整数而已。那么为什么我们会有四种不同的类型呢?

即使这些 API 是不安全的 API,它们可以让您跳过常规的边界检查以及内存管理当中的类型安全机制,但是 Swift 仍然希望帮助您做正确的事情。所以,它把一些检查带入了类型系统当中。从这个角度来看,我们可以在两条不同的轴当中来看待这四种类型。

类型指针 VS 原始指针

首先我们来看一下类型指针与原始指针之间的区别。左边的这两个泛型指针就是所谓的类型指针了。这意味着这些指针所表示的内存地址将会保存特定类型的值。如果您有一个指向 Int 的指针,访问该指针的 pointee 将会返回一个 Int 类型的值。Swift 需要严格的别名,这意味着您无法安全地以两个不同的类型来访问相同的内存。用有符号整数或者其他类型来访问这段相同的内存将属于未定义的行为。所以,类型指针可以阻止这类访问操作。当您需要更换类型的时候,有方法是可以暂时或者永久地将内存重新绑定到不同的底层类型上。

类型指针知道它所引用类型的大小和对齐方式。所以当您使用类型指针的时候,您不需要考虑分段或者排列的问题。回顾 age 变量的那个 UnsafePointer 类型,由于类型指针同时知道位置和类型,所以您可以将其视为对整个值的引用。

而当您通过内存将类型指针向前推进 (advance) 的时候,比如说,如果要访问数组的连续内容的时候——我们后面会详细说明,每次调用,类型指针将会步进 (step) 到下一个实例。所以您不会意外地访问到示例的中间部分内容。

另一方面,原始类型永远不会存储任何关于底层内存类型的任何信息。当您访问原始指针所引用的数据时,您可以通过 Raw 字节来访问,也可以通过将您想要从内存中读取的数据类型进行特定命名来访问。如果我要获取 age 属性的一个原始指针,而不是获取类型指针的话,那么它会引用相同的内存位置,但是里面没有任何的类型信息。它不知道,也不关心该地址上是否是一个 64 位的有符号整数。原始指针就是内存地址本身。您可以只访问一个字节位置的内存,您可以从该位置读取任何类型的值,也可以通过绑定该位置的内存,将原始指针转化为类型指针。

可变指针 VS 不可变指针

第二个访问就是可变性 (mutability)。指针是引用语义的文本实例 (literal embodiment),因此即便我们用 let 声明一个指针,我们仍然可以在技术上对该指针所引用的内存进行修改。因此,指针不在实例级别控制可变性,就像我们在值类型当中所做的那样,Swift 让我们可以在类型级别对其进行控制,只允许对可变指针所引用的值进行更改。

类型指针和原始指针都有不可变的版本,它们只允许对所引用的内存进行只读访问,同时也允许我们使用可变版本。您可以对可变指针的内容进行初始化、销毁、分配或者移动。所以,这就是我们所述的四种指针类型。

缓冲区指针

那么缓冲区指针是什么呢?缓冲区指针本质上是一个与计数相结合的指针。所以,它不存储一个特定的地址,它描述的是一段范围的内存地址。每个缓冲区指针都需要指针类型的配合。所以,缓冲区指针既可以是类型或者原始的,也可以是可变或者不可变的。很简单,缓冲区指针的表现和集合很类似。

您可以遍历内存区域当中的所有内容,或者借助与操作数组相似的大多数工具,来执行其他操作。

在这段代码中,我将 age 传递给 withUnsafeBytes函数,这将导致闭包中带有的是一个 UnsafeRawBuffer 指针,引用的是您所传递的变量的字节。

var age = 5
    
withUnsafeBytes(of: &age) { ageBytes in
  ageBytes.count // 8
  ageBytes.first // Optional(5)
  ageBytes[0] // 5
}

我可以像标准的集合那样来使用这个缓冲区。在这里,我获取了缓冲区的元素总数,并通过 first 属性和下标来访问它的第一个值。记住这些都是这个值的原始字节。这也就是为什么总数是 8,一个 Int 类型是八个字节长的,所以这个不安全字节的总数是 8,并且我检索了 age 值得第一个字节,它的值为 5。

var age = 2000

withUnsafeBytes(of: &age) { ageBytes in
  ageBytes.count // 8
  ageBytes.first // Optional(208)
  ageBytes[0] // 208
}

如果 age 的值不能完全放到单个字节当中的话,比如说假设我们记录了甘道夫的年龄,他活了 2,000 岁,那么 age 内存的第一个字节将是十六进制的 d0,对不起,是 208。再次说一遍,因为我们这里使用的是原始指针,所以我们是在字节级别而不是在实例级别来读取这个值的。

引入 C API

那么我们什么时候会想去使用这些不安全的指针类型呢?对于大多数开发者而言,会有两种情况。第一种,这些指针类型提供了与很多 C API 的互操作性,而这些 C API 需要对类型或者 Void 指针进行操作;第二种,有些类型的优化操作只能借助不安全的指针类型来进行。下面我们来看几个例子:

func SKSearchFindMatches(
  _ inMaximumCount: CFIndex,
  _ outIDsArray: UnsafeMutablePointer<SKDocumentID>!,
  _ outFoundCount: UnsafeMutablePointer<CFIndex>!
) -> Bool

这是 SKSearchFindMatches 函数的简化版本,这是 OS X 的 Search Kit 框架中的一个 C 函数。这是一个很复杂的函数系列,但是对于必须处理输入和输出参数的 C 函数而言,这个函数仍然是相当典型的。它有三个参数,其中一个用于函数的输入,另外两个用于输出。

这个函数的工作方式是在初始化搜索之后对其重复调用。每次获取到一批结果之后,这个函数就会向输出参数中写入结果,然后在完成所有的匹配项检索后返回 false。您传递给 inMaximumCount 的数字决定了您所能够获取的结果数目。这是一个非常重要的参数,因为第一个输出参数需要一个 C 数组,这个数组必须有空间去存储 documentId 的最大数目。

第二个输出参数将保存返回的实际结果数。两个输出参数都是可变类型指针,它们的 pointee 类型各不相同。这是一个很不错的部分。在完成对指针类型的所有准备之后,我们甚至不需要使用不安全指针,就可以调用此函数。

Swift 可以借助 & 语法,来将变量或者数组隐式转换为类型指针或者原始指针。

let limit = 100
var foundCount = 0 as CFIndex
var documentIDs = Array(repeating: 0 as SKDocumentID, count: limit)

_ = SKSearchFindMatches(CFIndex(limit), &documentIDs, &foundCount)
        
for i in 0..<Int(foundCount) {
  loadDocument(id: documentIDs[i])
}

在这里我们创建了一个 documentIds 数组,并用另一个变量来保存 foundCount。然后,您可以看到我们使用了 & 语法来将这些变量传递到 SKSearchFindMatches 函数当中。&documentIds 将被转换为一个指向 documentIds 数组内容的指针,而 &foundCount 将被转换为一个指向 findCount 变量的指针。

了解这种隐式转换的限制是很重要的。当我们使用隐式指针转换来传递 documentIds 数组的时候,唯一传递的是指向该数组内容中第一个元素的指针。接收该指针的函数并没有关于数组大小的任何信息,也没有任何改变数组总数的能力。

这就是为什么我们需要传递一个有足够空间来存储元素的数组,从而能保存我们所传递的最大检索元素数目,这是非常重要的。另外,使用这种隐式转换所创建的指针只在所调用的函数的生命周期内有效。如果其中某个指针发生了逃逸、或者在函数完成之后使用这些指针将属于未定义行为。

在我们获得了输出的 documentIds 之后,我们就可以循环遍历数组,直到所找到的文件总数为止,然后再去处理每个结果。所以,这就是使用 C API 的正确方式,它能够通过测试,但是在我们进行了一些性能测试之后,我们需要让这段代码速度更快一些,因此我们将明确使用不安全指针。

这里就需要做一个权衡。我们放弃了语言所提供的安全性。只有经过测试,并确定只有进行这样的优化才能够提供您所需要的好处时,再去执行这样的优化操作。

这段代码中,有两处地方需要进行额外的修改,这样才能够保证 Swift 的安全性。首先,我们来看一看我们声明 documentIds 数组的方式。我们使用了数组的重复计数构造器 (repeating count initializer)。这个构造器为 documentIds 数组分配了 100 个元素的空间,然后将每个条目初始化为 0。

通常而言,这个方法是很好的。除非数组类型被正确初始化,否则就不允许我们访问元素,以此来提供安全性。在这种情况下,在尝试访问任何元素之前,我们将数组传递给 SKSearchFindMatches,然后等待其将所检索到的内容写入到数组当中。所以,初始化步骤是不必要的。

那么我们是否还有别的方法呢?我们能否将 documentIds 创建成空数组,并保留适当的容量么?很不幸的是,我们不能这么做。虽然这种做法看起来似乎是一种解决方案,但请记住,我们传递 documentIds 的函数只能够获得一个指针。它并不能看到数组大小,它也不能改变我们所传递的数组大小。

所以在数组返回之前,我们是没有办法去访问放入到数组当中的元素的。所以这种做法不起作用。因此我们需要做的第二个额外操作就在这里:当我们在访问 documentIds 的第 i 个元素时。每次在数组中使用下标时,都会执行一次边界检查,以确保您不会在数组定义的边界外去获取元素。

同样,这是一个非常重要的安全功能。但是我们需要努力去榨干每一滴性能。只要我们谨慎地在迭代中留在我们所创建的边界当中,我们实际上就不需要对每次访问执行边界检查。

这两个解决方案就是将 documentIds 从一个数组转换为一个可变类型指针。我们的数组会被隐式转换为同样的类型。当我调用静态分配方法的时候,我传递的元素数目空间就能够分配出来,并返回指向该内存块头部的指针。

当然,我们要一直记得释放所有我们所分配的内存空间。这是另一种安全性保证,这样我们从数组切换到不安全指针才不会出问题。Swift 尽可能让这些正确的操作尽可能地简单。在分配之后立即将释放代码放到 defer 闭包当中,这样就不会出错了。

我们接下来唯一要修改的地方就是删除参数当中的 & 符号。由于我们现在传递的是一个指针,而不去依赖隐式转换,所以我们不需要添加额外的语法了。

您可以像数组下标那样来使用指针下标。所以我们的代码部分并不会受到影响。这就是我们的整体优化操作。我们已经摆脱了一些不必要的初始化操作和边界检查,我们自行承担了安全性责任,而这三件事是 Swift 之前为我们所做的。我们需要确保在读取 documentIds 的元素之前对其进行初始化,我们需要记住去释放我们所分配的内存,我们还需要保证我们停留在所分配的边界当中。

现在我们在举最后一个例子。这是我最喜欢的排序算法:冒泡排序,之所以以其为例,一个原因是它有一个很可爱的名字,另一个原因是它只用一张幻灯片就能完全显示出来。

func bubbleSort<T: Comparable>(_ array: inout [T]) {
  guard !array.isEmpty else { return }

  array.withUnsafeMutableBufferPointer { buffer in
    for n in 1..<buffer.count {
      for i in 1...(buffer.count - n) {
        if buffer[i - 1] > buffer[i] {
          swap(&buffer[i - 1], &buffer[i])
        }
      }
    }
  }
}

严格来说,如果您尝试将快速排序放在这里的话,那么字体就要非常小。这就不是很好了。冒泡排序需要获取一个可比较元素的数组作为参数,然后按照升序对数组中的元素进行排序。如果您的任务是加快冒泡的速度,我们可以想象一下,我们是无法做出其他明显的优化的,除非我们不使用冒泡排序。

Swift 的数组类型拥有一组方法,可以让数组中的元素下沉,但是这个操作会一直执行边界检查,如果换成不安全的缓冲区指针的话,那么不管是元素、还是数组元素的 Raw 字节,都只会在调试模式下执行边界检查,而在发布模式下就不会执行。

我们将为我们的数组添加一个调用:用 withUnsafeMutableBufferPointer 方法来作为数组的封装,就像我们之前的代码中所做的那样。只有名字不同而已,这样做使我们的性能得以大大地提高。

请注意,这里我使用不安全指针所获取的好处是:通过跳过边界检查来获取更好的性能,同时我需要保证缓冲区中元素的类型不会改变,并且与数组的接口近乎一致。现在我们已经介绍了不安全的指针类型,以及不安全的指针类型是如何帮助我们编写安全的代码的,现在让我们用一个简单的方式来滥用指针。

真的不安全

不安全指针真的不是安全的。所以,在使用不安全的指针类型时,最危险也是最简单导致不安全的做法就是:在通过 withUnsafePointer 或者 withUnsafeBytes 函数以及隐式指针转换中使用不安全指针类型时,让指针逃逸出来。

例如,看一下这段代码。

var age = 2000
let agePointer = withUnsafeMutablePointer(to: &age) { p in
                   return p
                 }
agePointer.pointee = 10
// age == 10

我再一次获取了这个 age 变量,并且我已经使用 UnsafeMutablePointer 的构造器创建了一个指向变量值的指针。然后我对存储在指针地址当中的值进行了更新。由于我的 age 变量使用的是相同的内存,因此它的值也发生了改变。

非常简单明了。但是这里有一个很大的问题。我将不使用隐式转换来更明确地说明这一点。使用隐式指针转换来调用 UnsafeMutablePointer 的构造器本质上是这样的:直接将指针逃逸出 withUnsafeMutablePointer 闭包当中。编译器对此并不会反映出任何问题,但是这属于未定义的行为。这意味着即便现在可以用,但是编译器允许在指针逃逸之后,将使用该指针所执行的任何操作优化掉。

这个问题可能很难识别出来。当我以这种方式编写的话,那么是没有明显的逃逸的,也就是使用 & 这个符号来传递变量,这是调用函数的标准方法。因此要记住的两条规则是:1) 永远不要让从 withUnsafe 函数中获得的指针进行逃逸,2) 永远不要通过隐式转换来获取某个变量的指针。

非常感谢大家。我希望我的本次演讲能在各位下次使用 Swift 不安全指针类型的时候帮到大家。

问答时间到!

问:内存绑定和假定内存已绑定有什么区别?

答:好的,所以,内存绑定和假定内存已绑定有什么区别呢?对于不安全的原始指针而言,即使它们不知道正在寻址的内存的任何信息,内存本身仍然是可以被绑定的。所以您可以拥有一个内存绑定了整数类型的原始指针,或者也可以拥有一个内存绑定了其他类型(例如某个类的实例)的原始指针。当您想要将原始指针转换为类型指针,以便更容易地访问实例的时候,会发生的事情是您既可以绑定内存,也就是告诉编译器我正在将这个内存绑定到这个类型上来,除非我稍后重新绑定,否则就不会对其绑定的类型进行改变。这种绑定方式将返回一个不安全的类型指针。而假定内存已绑定将会绕过检查。它实际上并没有在编译器上执行任何绑定操作,它基本上只是假设类型已经绑定,并且您已经知道内存已经绑定了该类型。如果您这么做但是却没有绑定内存的话,比如说您只是分配了一个原始内存,然后假设这段内存绑定到了某个方法上,那么最终,我觉得这将是一个不安全的未定义行为,因为您试图以类型的方式来访问这段内存,但是实际上它并没有绑定这种类型。希望这个回答能解答您的疑惑。

问:我希望能详细谈论一下最后那个例子,您说不应该让那个指针逃逸,但是在您的例子当中,您在同个函数模块的下一行当中使用了它。这是可以的么,还是说这也属于逃逸呢?

答:这也算是逃逸。当您在使用隐式指针转换的时候,以及当您在使用一个传递的变量是用 & 符号转义的可变指针时,这就是我如何去调用不安全指针的构造器的做法。所谓的逃逸是,如果您在所传递的指针运行之后使用它。当您将其传递给 C 函数时,C 函数通常不会返回……我们是没有办法将其从 C 函数中取回的,但是将其传递给构造器是让指针意外逃逸的最简单方法。返回 withUnsafe 之类的东西。棘手的部分是大部分时间都可能会出现。这个例子实际上是在 Playground 当中撰写的。您可以运行它,它会更新掉 age 变量的值,但是如果您在计算属性当中执行此操作,那么编译器并不会指明这个错误,但是它就不会按照您期望的方式运转了。有各种各样情况您会发现它很诡异地不起任何作用,然后由于这是未定义的行为,所以编译器允许对您在这个指针之后所做的任何操作进行优化。如果我运行这段代码,如果我打开优化程序再编译的话,编译器会将这行代码移除掉,也就是这行 hPointer.pointee = 10,然后我的 age 值实际上就没有发生更新。好的,还有别的问题了吗?好的,非常感谢大家!

About the content

This talk was delivered live in March 2017 at try! Swift Tokyo. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Nate Cook

An independent web and application developer, he works on projects of all sizes, from websites and blogs for nonprofits to customized enterprise applications. He is also the managing editor of NSHipster, where he writes weekly about obscure topics in Objective-C, Swift, and Cocoa.

4 design patterns for a RESTless mobile integration »

close