WWDC20 10648 Unsafe Swift

本文翻译基于 wwdc20-Unsafe Swift

这篇将会讨论下Swift中不安全的API。

标准库中提供了许多不同的结构,类型,协议,功能,属性等,其中少量被明确标记为不安全。

我们没办法从接口名字上直接知道安全类型和不安全类型具体的区别是什么。实际上他们的区别在于对待无效输入时的处理实现。标准库中的大多数操作在执行之前都会完全验证其输入,因此我们可以放心地假定,我们可能犯的任何严重编码错误都将可靠地捕获并报告。

“安全”和“不安全“的定义

这里有一个强制展开Optional类型的例子:

我们知道value一定不能是nil,如果我们将value赋值为nil,然后使用强解操作符,我们的程序会马上crash。虽然尝试强制展开nil值仍然是严重的编程错误,但是因为其后果已得到很好的定义。
所以我们可以说强解这个操作是“安全的”,因为我们可以很清楚的知道对于各种输入会有什么表现形式。(包括强解nil会crash)。

从广义上讲,不安全的操作指的是存在于某些输入,会有着不确定的行为发生。

unsafe 接口

Optional还通过其"unsafelyUnwrapped"属性提供了"unsafe"强制展开操作。就像常规的强制展开运算符一样,这也要求基础值必须为非零。

但是,在启用编译优化的情况下,此属性不会去校验,他信任开发者只会在非nil的情况下调用这个接口。如果我们不小心对nil调用这个属性,它可能会引发立即崩溃,或者可能返回一些垃圾值。会无法预知发生什么事情,这对调试问题来说是非常困难的。这就是一个典型的不安全类型。
标准库中的不安全类型都有着这样的属性:它们是不会对输入做完全的校验的

unsafe- 前缀就像是一个危险符号的命名约定。它警告您和任何阅读您的代码的人使用中存在着潜在危险。但是,标记为 "unsafe" 的接口仍然可以用来构建可靠运行的代码。实际上,某些任务只能使用它们来完成。只是在使用的时候需要格外小心,并且必须完全了解其使用条件。

unsafe 接口的作用

  1. 提供swift与C或者Objective-C相互操作的能力。
  2. 提供了对运行时性能或程序执行的其他方面上更加细致的细粒度控制。

Optional的unsafelyUnwrapped属性恰好属于第二类。它省去了对值是否nil的多余判断(因为完全相信开发者)。这最好用应用在代码库中最重要的部分,性能测量表明,这些不必要的检查尽管成本微小,但是仍然会对性能产生不利影响。

这个地方就像以前我们在写oc的setter方法有一种观点是:

- (void)setValue:(Int)value
{
//    if (_value != value) {  这个判断是不需要的,因为既然调用setter方法就是要赋值,在大多数情况下,这个判断都只是多余的执行
       _value = value
//    }
}

但是xcode为了让我们能及时发现这些不安全代码的潜在错误,unsafelyUnwrapped消除了优化构建中的nil检查。在平时未优化的debug版本中,它会完全验证其输入值。确保我们对nil执行unsafelyUnwrapped时会马上终止程序。


需要注意的是,安全的API不是为了防止崩溃。相反的。而是当给定的输入超出其约束范围时,安全的API会通过引发致命的运行时错误来确保停止执行。这些情况表明存在严重的编程错误:我们的代码在关键逻辑上出现错误,我们需要马上去修复它。规避崩溃而继续执行将是不负责任的。(译者:当然在我个人实际工程开发里面,我更习惯于在核心关键代码出加上比较详细的log日志。)

Swift是一种安全的编程语言时,意思是,默认情况下,它的语言和库级功能完全验证了它们的输入。对于不能做到这一点的类型,都会被标记上"unsafe"。Swift标准库提供了功能强大的不安全指针类型,这些类型与C编程语言中的指针大致处于同一抽象级别。

unsafe和指针

为了理解指针是如何工作的,我们必须谈论一些有关内存的知识。
Swift的内存模型是平面内存模型

(相关文章:https://juejin.im/entry/59156846a22b9d0058007283)

在运行时,地址空间中是稀疏的填充着应用的数据和状态
它包括:

  • 我们应用程序的可执行二进制文件
  • 我们已经导入的所有库和框架;
  • 栈区为本地和临时变量以及一些函数参数提供存储;
  • 动态内存区域,包括类实例存储和我们手动分配的内存;
  • 有些区域甚至可能映射到只读资源,例如图像文件。

每个单独的项目分配了一个连续的存储区域,该区域特定的位置会存储特定的某种数据。当您的应用执行时,其内存状态会不断发生改变。栈空间内不断变化,新对象被创建分配内存,旧对象被销毁。幸运的是,Swift语言和运行时会跟踪我们的情况。我们通常不需要在Swift中手动管理内存。

但是,当有必要的时候。不安全的指针会为我们提供所有有效管理内存所需的低层操作。不过同时,我们需要手动的完全接管这些不安全指针的声明周期和使用中的每个细节。这些指针仅表示内存中某个位置的地址。它们提供了强大的操作,但是你必须确保正确地使用它们,这从根本上来说是不安全的。

如果不小心,指针操作可能会在整个地址空间上“乱涂乱画”,从而破坏应用程序精心维护的状态。

例如:

  1. 为整数值动态分配存储空间会为您创建一个存储位置,并为您提供直接指向它的指针。指针使您可以完全控制基础内存,但不能为您管理。以后也无法跟踪该存储位置发生了什么。它仅执行您要求执行的操作。
  2. 随着底层内存的初始化和释放,指针将失效。
  3. 但是,无效指针看起来就像是常规有效指针。指针本身不知道它已经变得无效。取消引用这种悬空指针的任何尝试都是严重的编程错误。

如果幸运的话,释放位置将使内存位置完全无法访问,而尝试访问它将导致立即崩溃。
但是,这不能保证。随后的分配可能已经重用了相同的地址来存储其他值。在这种情况下,取消引用悬空指针可能会导致更严重的问题。甚至导致用户数据丢失。

当我们访问的值包含对象引用,或者内存现在包含不兼容类型的Swift值时,此类错误特别危险。

Xcode提供了称为Address Sanitizer的运行时调试工具,以帮助您捕获此类内存问题。

有关此和类似Xcode工具的更多信息,请参见上一届会议的使用Xcode运行时工具查找错误会话。
有关如何避免指针类型安全性问题的更详细讨论,请查看Safely manage pointers in Swift

Unsafe用于与C或者Objective-C互相操作

因此,如果指针是如此危险,那么为什么还要使用它们呢?嗯,一个很大的原因是与不安全的语言(例如C或Objective-C)的互操作性。
C指针类型与其对应的Swift不安全指针对应物之间存在直接映射。

C指针类型与其对应的Swift不安全指针对应物之间存在直接映射。

这里有C语言接口映射的例子

这里有C语言接口映射的例子

const int 指针参数被转换为隐式解包的可选不安全指针类型 UnsafePointer!。

以下是获取此类指针的一种方法:

  1. 在UnsafeMutablePointer上使用静态分配方法来创建适合于保存整数值的动态缓冲区。
  2. 用指针算术和专用的初始化方法将缓冲区的元素设置为特定值。
  3. 一切都安排好之后,可以调用C函数,并将指针传递给初始化缓冲区。
  4. 当函数返回时,我们可以取消初始化并重新分配缓冲区,从而允许Swift稍后将其内存位置重新用于其他用途。

但是从根本上讲,以上每个步骤都是不安全的:

  1. 分配缓冲区的生存期不受返回指针的管理,我们必须记住在适当的时间手动将其分配,否则它将永久存在,从而导致内存泄漏。
top Created with Sketch.