最近在学习中遇到到 Swift 和 C 第三方库交互的问题,Google 到了这篇文章顺便记录了下,文章原文出处《unsafe Swift: Using Pointers And Interacting With C》
默认情况下,Swift 是内存安全的,这意味着避免直接访问内存,并且在使用前要确保一切都是初始化过的。当然这是在默认情况下,在我们需要时不安全(unsafe)Swift (以下内容将直接用 unsafe Swift,翻译了感觉别扭)允许我们通过指针直接访问内存。
这篇教程将带你快速领略所谓“unsafe"的 Swift 特征。术语“unsafe”有时会让人感到迷惑,它并不是说你写的代码是危险的、不可运行的烂代码,仅仅意味着你在写代码时需要格外的谨慎,因为编译器在这方面对你的帮助是有限的。
当你要与类似 C 这样的不安全语言进行交互时就会用到这些特征,比如要�提高运行时性能或探究 Swift 的内部原理。虽然这是一个高级课程,但如果你有正常的 Swift 能力,你应该能够跟得上课程。 C 经验将对你的学习有所帮助,但并不是必需的。
入门
这一课程由三个 playgrounds 组成,在第一个 playground,你将创建几个短片段来探索内存布局并使用不安全的指针,在第二个 playground,你将使用 Swift 接口封装一个低阶的 C API 来展示数据流压缩,最后一个 playground,您将创建一个平台独立的替代 arc4random,在使用不安全的 Swift 时,隐藏用户的细节。
开始创建一个新的 playground,命名为 UnsafeSwift, 您可以选择任何平台,本教程中的所有代码都与平台无关, 确保导入 Foundation 框架。
内存布局
unsafe Swift 直接与内存系统配合使用,内存可以被视为一系列的盒子(实际上是数十亿个盒子),每个盒子里面都有一个数字,每一个盒子都有一个唯一的与之关联的内存地址,最小的寻址储存单元是一个字节,一个字节由 8 位(bits) 组成,8 位字节可以存储 0-255 的值。处理器通常也可以有效地访问多于一个字节的存储器的字。 例如,在 64 位系统上,一个字是 8 字节或 64 位长度。
Swift 有一个 MemoryLayout 技巧,可以告诉你程序中的类型大小和对齐方式。
将以下代码添加到你的 playground:
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> 是在编译时评估的通用类型,用于确定每个指定类型的大小,对齐方式和步幅,返回的数字是对应数据类型的字节数。 例如,Int16 的大小是两个字节,并且也有两个对齐方式,这意味着它必须从偶数地址开始(可以被 2 整除)。
因此,例如,给一个 Int16 数据类型分配的地址是 100,那就是合法的,但如果分配的是 101 就不合法了,因为它违反了对齐方式的要求。当你将一堆的 Int16 类型数据放在一起,他们将以一定的间隔(步幅)打包在一起,对于这些基本类型,大小与间隔(步幅)都是相同。
接下来,查看一些自定义的结构体的布局,并将以下内容添加到 playground 中:
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
空结构体的大小为 0,它可以放在任何地址,因为它对齐方式是 1(即所有数字都可以被 1 整除)。奇怪的是步幅也是 1,这是因为你创建的每个 EmptyStruct 必须具有唯一的内存地址,尽管大小为零。
对于 SampleStruct,大小是 5,但间隔(步幅)却是 8,这是由其对齐方式以 4 个字节为边界决定的,考虑到这,最好情况下 Swift 能做到以 8 个字节间隔打包。
接下来添加:
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: Bool = 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 个字节,如果你想了解更详细的内存布局,请看this excellent talk by Mike Ash。
指针
指针指向(封装)的是一个内存地址,那些涉及到直接访问内存地址的类型都会有“unsafe”前缀,因此,指针类型也被称作 UnsafePointer。虽然额外的输入可能看起来很烦人,但它让你和你的读者知道你正在将内存中的非编译器检查的访问权限浸入到未正确使用的操作中,从而导致不可预测的行为(而不仅仅是可预测的崩溃)。
Swift 的设计师可以仅仅创建单一 UnsafePointer 类型,并将其作为 char * 的 C 等价物,可以以非结构化的方式访问内存,但他们没有这样做,相反,Swift 包含几十种指针类型,每种类型具有不同的功能和目的。使用适当的指针类型可以防止错误的发生并且更清晰地表达开发者的意图,让你控制不可预测行为的产生。
Unsafe Swift 指针使用可预测的命名方案,以便你知道指针的特征是什么。可变或者不可变,原生(raw)或者有类型的,是否是缓冲(buffer)类型,这三种特性总共组合出了 8 种指针类型。
在以下部分中,你将了解有关这些指针类型的更多信息。
使用原生的指针
将以下代码添加到你的 playground:
// 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(bytes: byteCount, alignedTo: alignment)
// 4
defer {
pointer.deallocate(bytes: byteCount, alignedTo: alignment)
}
// 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)")
}
}
在这个例子里你使用 Unsafe Swift 指针存储和读取了两个整型数据,接下来说明下它们是什么发生的:
- 这些常量会经常被用到:
- count 表示整型数据存储的个数
- stride 表示 Int 类型的步幅(即占用的空间跨度)
- alignment 表示 Int 类型的对齐方式
- byteCount 表示总共需要的字节数
- 添加一个 do 代码块,控制代码作用域,因此你可以做接下来的例子中重用变量名
- 方法 UnsafeMutableRawPointer.allocate 是用来分配所需的字节数,它返回的是一个 UnsafeMutableRawPointer 类型,通过名字你就可以知道这个指针是可以读取和可存储原生字节数的。(即可变的)
- 添加 defer 代码块是确保指针得到正确的释放,ARC 在这种情况下是帮不了你的,你得自己管理内存,关于 defer 你可以阅读这篇文章。
- 方法 storeBytes 和 load 分别是用来存储和读取字节数的,第二个整数的存储器地址是通过推进指针步幅字节数来计算的。
由于指针是具有跨域能力的,你也可以通过指针运算来调用 (pointer + stride).storeBytes(of: 6, as: Int.self) 。 - UnsafeRawBufferPointer 让你访问内存就像是一个字符集一样,这意味着你可以迭代字节,可以使用下标访问它们,甚至使用酷的方法,如过 filter,map 和 reduce 。缓冲区指针使用了原生指针进行初始化。
使用类型指针
前面的例子可以通过使用类型指针进行简化,将以下代码加到你的 playground:
do {
print("Typed pointers")
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(to: 0, count: count)
defer {
pointer.deinitialize(count: count)
pointer.deallocate(capacity: count)
}
pointer.pointee = 42 // pointer 指向的内存地址存放数值 42
pointer.advanced(by: 1).pointee = 6 // pointer 下一个内存地址存放数值 6,即 pointer 指向的起始地址加 Int 类型的步幅再移动 1 位,就其起始地址
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 分配内存,泛型参数(即<T>中的类型)让 Swift 知道指针将用于加载和存储 Int 类型的值。
- 类型内存必须在使用前初始化,并在使用完之后销毁,这一操作分别是由 initialize 和 deinitialize方法来完成。备注 : 正如用户 atrick 在下面的评论中所指出的,初始化只适用于非基础数据类型。也就是说,包含销毁过程对于你将来校对代码是一种好的方式。通常情况下它是不会有什么不好的结果出现的,因为编译器会对它进行优化。
- 类型指针有一个 pointee 属性提供了类型安全的方式来读取和存储值。
- 当需要指针前进的时候,我们只需要指定想要前进的个数,类型指针会自动根据它所指向的数值类型来计算要间隔的值。同样的,我们可以直接对指针进行算术运算 (pointer + 1).pointee = 6 。
- 有类型的缓冲型指针也会直接操作数值,而非字节。
转换原生指针到类型指针
类型指针并不总是通过直接初始化得到,它们也可以从原生指针派生而来。
添加以下代码到你的 playground:
do {
print("Converting raw pointers to typed pointers")
let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
defer {
rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
}
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(to: 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:) 方法获得。
将以下代码添加到你的 playground:
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:) 方法可以访问到 UnsafeRawBufferPointer 并传入闭包中供你使用。
withUnsafeBytes 也可以对 Array 和 Data 实例使用。
校验和计算
使用 withUnsafeBytes(of:) 你可以得到一个返回值,以下例子使用它来计算 32 位操作系统结构体字节的校验和。( checksum,使用少量的数据来验证数据在传输或者存储时是否存在错误;通常情况下,是用来检查数据的完整性,但是不保证数据的准确性可靠性)
将以下代码添加到你的 playground:
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 调用将所有字节相加,然后使用 ~ 按位取反,这不是一个特别强大的错误检测,但它体现了这个概念。
Unsafe 俱乐部的三大原则
你在写不安全代码时需要格外小心,以避免不可预知的行为,这里有一些烂代码的例子。
不要让 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:) 闭包中出现逃逸指针,这一刻可能能正常执行,但······(下一刻可能就执行不了了)
一次只绑定一种类型
// 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(bytes: byteCount, alignedTo: 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
}
}
不要一次将内存绑定到两个不相关的类型上,这叫做类型歧义,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(bytes: byteCount, alignedTo: alignment)
let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????
for byte in bufferPointer {
print(byte) // pawing through memory like an animal
}
}
随着不安全代码使用,错误的问题会一个接一个呈现出来,这太糟糕,所以使用时要谨慎小心,要进行审查和测试。
Unsafe Swift 例子1:压缩
使用你所掌握的知识封装 C API,在 Cocoa 中引用 C 模块实现一些通用的数据压缩算法,这些包括速度至关重要的 LZ4,当你需要最高的压缩比并且不关心速度时的 LZ4A,能平衡空间和时间的 ZLIB,还有能够更好的平衡空间和时间的新库(开源库)LZFSE。
创建一个新的 playground,命名为 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,很快你就会给它添加些 unsafe 代码。
接下来在 playground 末尾添加以下代码:
// 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
}
// Compress 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, add 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(bytes: Array(repeating: UInt8(123), count: 10000))
let compressed = input.compressed(with: .lzfse)
compressed?.data.count // in most cases much less than orginal input count
let restoredInput = compressed?.decompressed()
input == restoredInput // true
这里主要的入口点是 Data 数据类型的扩展,你已在扩展里添加了一个返回值为可选的 Compressed 结构的名为 compressed(with:) 的方法,这个方法简单的调用了结构体 Compressed 中的静态方法 compress(input:with:) 。在这还实现了使用的例子,但目前是不能正常工作的,我们来开始完善它。
滚动到你写的第一个代码块,按以下内容实现 perform(:on:using:workingBufferSize:)_ 函数:
func perform(_ operation: CompressionOperation,
on input: Data,
using algorithm: CompressionAlgorithm,
workingBufferSize: Int = 2000) -> Data? {
// 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
}
return nil /// To be continued
}
这将从 Swift 类型转换为压缩库所需的 C 类型,用于执行压缩算法和操作。
下一步,实现 return nil 部分:
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
streamPointer.deallocate(capacity: 1)
}
// 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(capacity: dstSize)
}
return nil /// To be continued
以下就是这里发生的事情:
- 给 compression_stream 分配内存空间,并在 defer 代码释放
- 之后,使用 pointee 属性初始化变量 stream 并将其作为参数传递给函数 compression_stream_init,编译器在这里做一些特别的事情,使用取地址符号 & 获取 compression_stream 并将其自动转化为 UnsafeMutablePointer<compression_stream>(你也可以直接把 streamPointer 作为参数传递进去,不需要做特殊的转换)。(其实就是通过 compression_stream_init 给 streamPointer 赋值)
- 最后,创建一个目标缓冲区,作为的工作缓冲区。
替换 return nil 来结束 perform 函数:
// process the input
return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in
// 1
var output = Data()
// 2
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,只有部分目标缓冲区可能需要复制。
在示例使用中,你可以看到 10000 个元素的数组被压缩到 153 个字节, 不是太寒酸。
Unsafe Swift 例子 2:随机发生器
随机数对许多应用程序都是很重要的,从游戏到机器学习,macOS 提供了 arc4random 获取随机数,不幸的是,不可在 Linux 上是调用。此外,arc4random仅仅提供 UInt32 的随机数,但是,文件 / dev / urandom 提供了一个无限制的随机数来源。
在本节中,你将使用新知识来读取此文件并创建类型完全的安全随机数。
首先创建一个新的 playground,命名为 RandomNumbers, 这次确保选择的是
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(capacity: capacity)
}
queue.sync {
fgets(data, Int32(capacity), file)
}
return Array(UnsafeMutableBufferPointer(start: data, count: count))
}
}
这个文件变量被声明为 static,因此只有一个存在系统中,当进程退出时,系统会自动关闭它。有时可能需要在多线程中使用随机数,你需要使用 GCD 串行队列来保护访问。(防止线程锁)
get 函数是真正起作用的地方, 首先,你创建一些未分配的存储区域,它将超出你需要的范围,因为 fgets 终将为 0,接着,你从文件夹中获取数据,确保这做是在 GCD 队列中操作的,最后,将其封装进 UnsafeMutableBufferPointer 序列后复制 data 到标准数组中。
到目前为止,这只会(安全地)给你一个 Int8 值的数组, 现在你要对他进行扩展。
将以下代码添加到你的 playground 末尾处:
extension Integer {
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
在这为 Integer 协议的所有子类添加了一个静态的 randomized 属性(了解更多的面向协议的编程,请看这)。首先你将获得随机数字,并返回字节数组,重新绑定(如 C ++ 的 reinterpret_cast )Int8 值作为请求的类型并返回一个副本,简单吧!🙂
就是这样的!使用 unsafe Swift 引擎,是获取随机数的一种安全的方式。