本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com
在本教程中,你将学习如何使用不安全的Swift,通过各种......,直接访问内存。
Update note : Brody Eller针对Swift 5.1更新了本教程。Ray Fix写了原文。
默认情况下,Swift是内存安全的。它防止对内存的直接访问,并确保你在使用之前已经初始化了一切。关键的短语是 "默认"。你也可以使用_非安全的Swift_,它允许你通过指针直接访问内存。
本教程将带你旋风式地参观Swift的所谓_不安全_功能。
不安全并不意味着可能无法工作的危险的坏代码。相反,它指的是需要特别注意的代码,因为它限制了编译器保护你不犯错的方式。
如果你与不安全的语言(如C语言)互通有无,需要获得额外的运行时性能,或者只是想探索Swift的内部结构,这些功能就很有用。在本教程中,你将学习如何使用指针并与内存系统直接互动。
Note。虽然这是一个高级话题,但如果你对Swift有一定的能力,你就可以跟着学。如果你需要提高你的技能,请查看我们的iOS和Swift的初学者系列。有C语言经验是有益的,但不是必须的。
入门
点击教程顶部或底部的_Download Materials_按钮,下载开始项目。
本教程由三个空的Swift操场组成。
- 在第一个操场上,你将使用几个简短的代码片段来探索内存布局。你还将尝试使用不安全指针。
- 在第二个操场上,你将使用一个执行流式数据压缩的低级C语言API,并用一个Swifty接口将其包裹。
- 在最后的游戏中,你将创建一个独立于平台的
arc4random的替代品来生成随机数字。它使用不安全的Swift,但对用户隐藏了这个细节。
首先打开_UnsafeSwift_游戏场。由于本教程中的所有代码都是平台无关的,你可以选择任何平台。
用不安全的Swift探索内存布局
Unsafe Swift直接与内存系统一起工作。你可以把内存想象成一系列的盒子--实际上是数十亿个盒子--每个盒子都包含一个数字。
每个盒子都有一个唯一的 内存地址 。最小的可寻址存储单位是一个 字节 ,通常由八个 比特 组成。
八位字节可以存储0-255的数值。处理器也可以有效地访问内存的 字 ,它通常超过一个字节。
例如,在一个64位系统中,一个字是8个字节或64位。为了看到这一点,你将使用MemoryLayout来告诉你一些本地Swift类型的组件的大小和排列。
在你的操场上添加以下内容。
import Foundation
MemoryLayout<Int>.size // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment // returns 8 (on 64-bit)
MemoryLayout<Int>.stride // returns 8 (on 64-bit)
MemoryLayout<Int16>.size // returns 2
MemoryLayout<Int16>.alignment // returns 2
MemoryLayout<Int16>.stride // returns 2
MemoryLayout<Bool>.size // returns 1
MemoryLayout<Bool>.alignment // returns 1
MemoryLayout<Bool>.stride // returns 1
MemoryLayout<Float>.size // returns 4
MemoryLayout<Float>.alignment // returns 4
MemoryLayout<Float>.stride // returns 4
MemoryLayout<Double>.size // returns 8
MemoryLayout<Double>.alignment // returns 8
MemoryLayout<Double>.stride // returns 8
MemoryLayout<Type>是一个在编译时评估的通用类型。它确定每个指定的Type的size、alignment和stride并返回一个字节数。
例如,一个 "Int16 "的 "大小 "是两个字节,"对齐 "也是两个。这意味着它必须从偶数地址开始 - 也就是能被2整除的地址。
例如,在地址100分配一个`Int16'是合法的,但在101就不合法了--奇数违反了要求的对齐方式。
当你把一堆 "Int16 "打包在一起时,它们以 "stride "的间隔打包。对于这些基本类型, size和stride是一样的.
检查结构布局
接下来,通过在操场上添加以下内容,看看一些用户定义的`结构'的布局。
struct EmptyStruct {}
MemoryLayout<EmptyStruct>.size // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride // returns 1
struct SampleStruct {
let number: UInt32
let flag: Bool
}
MemoryLayout<SampleStruct>.size // returns 5
MemoryLayout<SampleStruct>.alignment // returns 4
MemoryLayout<SampleStruct>.stride // returns 8
空结构的大小为零。它可以存在于任何地址,因为alignment是1,而且所有的数字都能被1平均整除。
奇怪的是,"stride "是1。这是因为你创建的每个EmptyStruct都必须有一个唯一的内存地址,尽管它的大小是零。
对于SampleStruct,size是5,但stride是8。这是因为它的对齐方式要求它在4字节的边界上。鉴于此,Swift能做的最好的就是以8个字节的间隔打包。
要看看class和struct的布局有什么不同,请添加以下内容。
class EmptyClass {}
MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)
class SampleClass {
let number: Int64 = 0
let flag = false
}
MemoryLayout<SampleClass>.size // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
类是引用类型,所以MemoryLayout报告引用的大小。8个字节。
如果你想更详细地探索内存布局,可以看看Mike Ash的精彩演讲,Exploring Swift Memory Layout。
在不安全的Swift中使用指针
一个 pointer 封装了一个内存地址。
涉及直接内存访问的类型有一个 unsafe 的前缀,所以指针类型的名称是UnsafePointer。
额外的类型可能看起来很烦人,但它提醒你,你正在访问编译器不检查的内存。如果操作不当,这可能会导致未定义行为,而不仅仅是可预测的崩溃。
Swift并没有像C的char *那样,只提供一个单一的UnsafePointer类型,以非结构化的方式访问内存。Swift包含了近十种指针类型,每种类型都有不同的能力和用途。
你总是希望为你的目的使用最合适的指针类型。这可以更好地传达意图,减少错误,并避免未定义的行为。
不安全的Swift指针使用一个可预测的命名方案,描述指针的特征:可变或不可变,原始或类型,缓冲区风格或不。总共有八种指针组合。你将在下面的章节中进一步了解它们。
使用原始指针
在本节中,你将使用不安全的Swift指针来存储和加载两个整数。在你的操场上添加以下代码。
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count
// 2
do {
print("Raw pointers")
// 3
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
// 4
defer {
pointer.deallocate()
}
// 5
pointer.storeBytes(of: 42, as: Int.self)
pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
pointer.load(as: Int.self)
pointer.advanced(by: stride).load(as: Int.self)
// 6
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
for (index, byte) in bufferPointer.enumerated() {
print("byte \(index): \(byte)")
}
}
下面是发生的事情。
- 这些常数持有经常使用的数值。
- Count 保持要存储的整数的数量。
- Stride 保持
Int类型的跨度。 - Alignment 持有
Int类型的对齐方式。 - ByteCount 保存需要的字节总数。
do块增加了一个范围级别,所以你可以在接下来的例子中重复使用这些变量的名字。UnsafeMutableRawPointer.allocate分配所需的字节。这个方法返回一个UnsafeMutableRawPointer。该类型的名称告诉你该指针可以加载和存储,或 mutate ,原始字节。- 一个 "defer "块可以确保你正确地去分配指针。ARC在这里并不能帮助你--你需要自己处理内存管理问题 你可以在Swift官方文档中阅读更多关于defer语句。
storeBytes和load,不出所料,存储和加载字节。你通过推进指针的stride字节来计算第二个整数的内存地址。由于指针是Strideable,你也可以使用指针运算,比如。(pointer+stride).storeBytes(of: 6, as: Int.self)。- UnsafeRawBufferPointer "允许你访问内存,就像访问一个字节的集合一样。这意味着你可以遍历这些字节,并使用下标来访问它们。你还可以使用很酷的方法,如
过滤、映射和重现。你使用原始指针初始化缓冲区的指针。
尽管UnsafeRawBufferPointer是不安全的,你仍然可以通过将其限制在特定的类型上使其更安全。
使用类型化的指针
你可以通过使用类型化指针来简化前面的例子。在你的操场上添加以下代码。
do {
print("Typed pointers")
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count)
defer {
pointer.deinitialize(count: count)
pointer.deallocate()
}
pointer.pointee = 42
pointer.advanced(by: 1).pointee = 6
pointer.pointee
pointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value \(index): \(value)")
}
}
请注意以下区别。
- 你使用
UnsafeMutablePointer.allocate来分配内存。通用参数让Swift知道你在使用指针来加载和存储Int类型的值。 - 你必须在使用前初始化类型的内存,并在使用后去初始化它。你可以分别用
initialize和deinitialize方法来做。去初始化只对_非琐碎类型_有要求。然而,包括去初始化是一个很好的方法,可以在你改变为非微不足道的东西时保护你的代码。这通常不需要花费什么,因为编译器会把它优化掉。 - 类型化指针有一个
pointee属性,提供了一种类型安全的方式来加载和存储值。 - 当推进一个类型化的指针时,你可以简单地说明你想推进的值的数量。指针可以根据它所指向的值的类型计算出正确的跨度。同样,指针算术也是可以的。你也可以说
(pointer+1).pointee = 6。 - 对于类型化的缓冲区指针也是如此。它们在数值上迭代,而不是在字节上迭代。
接下来,你将学习如何从无约束的UnsafeRawBufferPointer到更安全的、有类型约束的UnsafeRawBufferPointer。
将原始指针转换为类型化指针
你并不总是需要直接初始化类型化的指针。你也可以从原始指针派生出它们。
在你的操场上添加以下代码。
do {
print("Converting raw pointers to typed pointers")
let rawPointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
defer {
rawPointer.deallocate()
}
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(repeating: 0, count: count)
defer {
typedPointer.deinitialize(count: count)
}
typedPointer.pointee = 42
typedPointer.advanced(by: 1).pointee = 6
typedPointer.pointee
typedPointer.advanced(by: 1).pointee
let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
for (index, value) in bufferPointer.enumerated() {
print("value \(index): \(value)")
}
}
这个例子与前面的例子相似,只是它首先创建了一个原始指针。你通过将内存与所需类型Int绑定来创建类型化的指针。
通过绑定内存,你可以以一种类型安全的方式访问它。当你创建一个类型化的指针时,内存绑定是在幕后进行的。
这个例子的其余部分也和前面的例子一样。一旦你进入了类型化指针领域,你就可以使用pointee,例如。
获取一个实例的字节数
通常,你有一个类型的现有实例,你想检查组成它的字节。你可以使用一个叫做withUnsafeBytes(of:)的方法来实现这个目的。
要做到这一点,请在你的操场上添加以下代码。
do {
print("Getting the bytes of an instance")
var sampleStruct = SampleStruct(number: 25, flag: true)
withUnsafeBytes(of: &sampleStruct) { bytes in
for byte in bytes {
print(byte)
}
}
}
这将打印出SampleStruct实例的原始字节。
withUnsafeBytes(of:)让你获得一个不安全的缓冲区指针,你可以在闭包中使用。
withUnsafeBytes也可以作为Array和Data的实例方法使用。
计算校验和
使用withUnsafeBytes(of:),你可以返回一个结果。例如,你可以用它来计算一个结构中的字节的32位校验和。
在你的操场上添加以下代码。
do {
print("Checksum the bytes of a struct")
var sampleStruct = SampleStruct(number: 25, flag: true)
let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
}
print("checksum", checksum) // prints checksum 4294967269
}
reduce调用添加字节,然后~翻转比特。虽然不是最强大的错误检测,但它显示了这个概念。
现在你知道了如何使用不安全的Swift,是时候学习一些你绝对不应该用它做的事情了。
不安全俱乐部的三条规则
在编写不安全代码时,要注意避免未定义行为。下面是几个_不好的代码的例子。
不要从withUnsafeBytes返回指针!
// Rule #1
do {
print("1. Don't return the pointer from withUnsafeBytes!")
var sampleStruct = SampleStruct(number: 25, flag: true)
let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
return bytes // strange bugs here we come ☠️☠️☠️
}
print("Horse is out of the barn!", bytes) // undefined!!!
}
你不应该让指针逃脱withUnsafeBytes(of:)的封闭。即使你的代码今天还能工作,将来也可能导致奇怪的bug。
一次只绑定一个类型!
// Rule #2
do {
print("2. Only bind to one type at a time!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)
// Breakin' the Law... Breakin' the Law (Undefined behavior)
let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)
// If you must, do it this way:
typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
(boolPointer: UnsafeMutablePointer<Bool>) in
print(boolPointer.pointee) // See Rule #1, don't return the pointer
}
}
不要同时将内存绑定到两个不相关的类型上。这被称为 Type Punning ,Swift不喜欢双关语。]
相反,要用withMemoryRebound(to:capacity:)这样的方法暂时重新绑定内存。
另外,从一个琐碎的类型,如Int,重新绑定到一个非琐碎的类型,如class,是不合法的。请不要这样做。
不要走到最后...喔!
// Rule #3... wait
do {
print("3. Don't walk off the end... whoops!")
let count = 3
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: byteCount,
alignment: alignment)
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1)
// OMG +1????
for byte in bufferPointer {
print(byte) // pawing through memory like an animal
}
}
在不安全的代码中,永远存在的逐一错误的问题变得更加严重。要小心,要审查和测试!
不安全的Swift实例1:压缩
是时候利用你所有的知识,用它来包装一个C语言API了。Cocoa包括一个C模块,实现了一些常见的数据压缩算法。这些算法包括。
- LZ4 用于速度要求很高的情况。
- LZ4A,当你需要最高的压缩率而不关心速度的时候。
- ZLIB,它平衡了空间和速度。
- 新的、开源的 LZFSE ,它在平衡空间和速度方面做得甚至更好。
现在,打开begin项目中的 Compression 游戏场。
首先,你要用Data定义一个纯粹的Swift API,用下面的代码替换playground的内容。
import Foundation
import Compression
enum CompressionAlgorithm {
case lz4 // speed is critical
case lz4a // space is critical
case zlib // reasonable speed and space
case lzfse // better speed and space
}
enum CompressionOperation {
case compression, decompression
}
/// return compressed or uncompressed data depending on the operation
func perform(
_ operation: CompressionOperation,
on input: Data,
using algorithm: CompressionAlgorithm,
workingBufferSize: Int = 2000)
-> Data? {
return nil
}
进行压缩和解压的函数是perform,目前它的存根是返回nil。你很快就会向它添加一些不安全的代码。
接下来,在playground的末尾添加以下代码。
/// Compressed keeps the compressed data and the algorithm
/// together as one unit, so you never forget how the data was
/// compressed.
struct Compressed {
let data: Data
let algorithm: CompressionAlgorithm
init(data: Data, algorithm: CompressionAlgorithm) {
self.data = data
self.algorithm = algorithm
}
/// Compresses the input with the specified algorithm. Returns nil if it fails.
static func compress(
input: Data,with algorithm: CompressionAlgorithm)
-> Compressed? {
guard let data = perform(.compression, on: input, using: algorithm) else {
return nil
}
return Compressed(data: data, algorithm: algorithm)
}
/// Uncompressed data. Returns nil if the data cannot be decompressed.
func decompressed() -> Data? {
return perform(.decompression, on: data, using: algorithm)
}
}
Compressed结构同时存储了压缩后的数据和用于创建数据的算法。这使得它在决定使用何种解压算法时不容易出错。
接下来,在playground的末尾添加以下代码。
/// For discoverability, adds a compressed method to Data
extension Data {
/// Returns compressed data or nil if compression fails.
func compressed(with algorithm: CompressionAlgorithm) -> Compressed? {
return Compressed.compress(input: self, with: algorithm)
}
}
// Example usage:
let input = Data(Array(repeating: UInt8(123), count: 10000))
let compressed = input.compressed(with: .lzfse)
compressed?.data.count // in most cases much less than original input count
let restoredInput = compressed?.decompressed()
input == restoredInput // true
主入口点是对Data类型的扩展。你添加了一个名为compressed(with:)的方法,返回一个可选的Compressed结构。这个方法只是调用Compressed的静态方法compress(input:with:)。
文末有一个例子,但目前还不能用。是时候解决这个问题了!
向上滚动到你输入的第一个代码块,开始实现perform(_:on:using:workingBufferSize:),在return nil前插入以下内容。
// 设置算法
// set the algorithm
let streamAlgorithm: compression_algorithm
switch algorithm {
case .lz4: streamAlgorithm = COMPRESSION_LZ4
case .lz4a: streamAlgorithm = COMPRESSION_LZMA
case .zlib: streamAlgorithm = COMPRESSION_ZLIB
case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
}
// set the stream operation and flags
let streamOperation: compression_stream_operation
let flags: Int32
switch operation {
case .compression:
streamOperation = COMPRESSION_STREAM_ENCODE
flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
case .decompression:
streamOperation = COMPRESSION_STREAM_DECODE
flags = 0
}
这就把你的Swift类型转换为压缩算法所需的C类型。
接下来,将return nil替换为。
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
streamPointer.deallocate()
}
// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
return nil
}
defer {
compression_stream_destroy(&stream)
}
// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
dstPointer.deallocate()
}
return nil // To be continued
下面是正在发生的事情。
-
分配一个
compression_stream,用defer块安排它的去分配。 -
然后,使用
pointee属性,获得流并将其传递给compression_stream_init函数。编译器在这里做了一些特别的事情。它使用in-out
&标记来获取你的compression_stream,并将其变成UnsafeMutablePointer<compression_stream>。另外,你也可以通过streamPointer。那么你就不需要这种特殊的转换。 -
最后,你创建一个目标缓冲区,作为你的工作缓冲区。
接下来,完成perform,将最后的return nil替换为。
// process the input
return input.withUnsafeBytes { srcRawBufferPointer in
// 1
var output = Data()
// 2
let srcBufferPointer = srcRawBufferPointer.bindMemory(to: UInt8.self)
guard let srcPointer = srcBufferPointer.baseAddress else {
return nil
}
stream.src_ptr = srcPointer
stream.src_size = input.count
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
// 3
while status == COMPRESSION_STATUS_OK {
// process the stream
status = compression_stream_process(&stream, flags)
// collect bytes from the stream and reset
switch status {
case COMPRESSION_STATUS_OK:
// 4
output.append(dstPointer, count: dstSize)
stream.dst_ptr = dstPointer
stream.dst_size = dstSize
case COMPRESSION_STATUS_ERROR:
return nil
case COMPRESSION_STATUS_END:
// 5
output.append(dstPointer, count: stream.dst_ptr - dstPointer)
default:
fatalError()
}
}
return output
}
这是真正发生工作的地方。下面是它正在做的事情。
- 创建一个
Data对象,它将包含输出--压缩或解压缩的数据,取决于这是什么操作。 - 用你分配的指针和它们的大小来设置源缓冲区和目的缓冲区。
- 在这里,你继续调用
compression_stream_process,只要它返回COMPRESSION_STATUS_OK。 - 然后你把目标缓冲区复制到
output中,这个函数最终会返回。 - 当最后一个数据包进来时,标记为
COMPRESSION_STATUS_END,你可能只需要复制目标缓冲区的一部分。
在这个例子中,你可以看到10,000个元素的数组被压缩到了153字节。不算太差。
不安全的Swift实例2。随机生成器
随机数对很多应用都很重要,从游戏到机器学习。
macOS提供了arc4random,可以产生具有密码学意义的随机数。不幸的是,这个调用在Linux上是不可用的。此外,arc4random只提供UInt32的随机数。然而, /dev/urandom 提供了一个好的随机数的无限来源。
在本节中,你将使用你的新知识来读取这个文件并创建类型安全的随机数。
首先创建一个新的游戏场,将其称为 RandomNumbers ,或者在begin项目中打开游戏场。
这次一定要选择 macOS 平台。
准备好后,用以下内容替换默认内容。
import Foundation
enum RandomSource {
static let file = fopen("/dev/urandom", "r")!
static let queue = DispatchQueue(label: "random")
static func get(count: Int) -> [Int8] {
let capacity = count + 1 // fgets adds null termination
var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
defer {
data.deallocate()
}
queue.sync {
fgets(data, Int32(capacity), file)
}
return Array(UnsafeMutableBufferPointer(start: data, count: count))
}
}
你将file变量声明为static,所以系统中只存在一个。你将依靠系统在进程退出时关闭它。
由于多个线程可能想要随机数,你需要用一个串行的GCD队列来保护对它的访问。
get函数是工作发生的地方。
首先,创建未分配的存储空间,这个存储空间超出了你的需要,因为fgets总是0终止的。
接下来,从文件中获取数据,确保在GCD队列上进行操作。
最后,将数据复制到一个标准数组中,首先将其包裹在一个可以作为序列'的不安全可变缓冲区指针'中。
到目前为止,这只能安全地给你一个Int8值的数组。现在你要对其进行扩展。
在你的游乐场的末尾添加以下内容。
extension BinaryInteger {
static var randomized: Self {
let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
return numbers.withUnsafeBufferPointer { bufferPointer in
return bufferPointer.baseAddress!.withMemoryRebound(
to: Self.self,
capacity: 1) {
return $0.pointee
}
}
}
}
Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized
这为 "二进制整数 "协议的所有子类型添加了一个静态的随机属性。关于这一点,请查看我们的[面向协议的编程]教程(www.raywenderlich.com/148448/intr…
首先,你得到随机数。通过返回的数组字节,你将Int8值重新绑定为所要求的类型,并返回一个副本。
这就是了! 你现在正以安全的方式生成随机数,在引擎盖下使用不安全的Swift。
接下来该怎么做?
恭喜你完成了本教程!你可以在以下网站下载完成的项目文件 你可以使用_Download Materials_下载本教程顶部或底部的完整项目文件。
你可以探索许多其他资源,以了解更多关于使用不安全的Swift的信息。
- Swift Evolution 0107: UnsafeRawPointer API详细介绍了Swift的内存模型,使阅读API文档更加容易理解。
- Swift Evolution 0138: UnsafeRawBufferPointer API广泛地讨论了与非类型化内存的工作,并有链接到受益于使用它们的开源项目。
- 导入的C和Objective-C APIs将让你深入了解Swift与C的交互方式。
我希望你喜欢这个教程。如果你有问题或经验想分享,欢迎在论坛上分享