[Swift翻译]不安全的Swift:使用指针和与C语言互动

355 阅读18分钟

本文由 简悦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>是一个在编译时评估的通用类型。它确定每个指定的Typesizealignmentstride并返回一个字节数。

例如,一个 "Int16 "的 "大小 "是两个字节,"对齐 "也是两个。这意味着它必须从偶数地址开始 - 也就是能被2整除的地址。

例如,在地址100分配一个`Int16'是合法的,但在101就不合法了--奇数违反了要求的对齐方式。

当你把一堆 "Int16 "打包在一起时,它们以 "stride "的间隔打包。对于这些基本类型, sizestride是一样的.

检查结构布局

接下来,通过在操场上添加以下内容,看看一些用户定义的`结构'的布局。

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都必须有一个唯一的内存地址,尽管它的大小是零。

对于SampleStructsize是5,但stride是8。这是因为它的对齐方式要求它在4字节的边界上。鉴于此,Swift能做的最好的就是以8个字节的间隔打包。

要看看classstruct的布局有什么不同,请添加以下内容。

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)")
  }
}

下面是发生的事情。

  1. 这些常数持有经常使用的数值。
    • Count 保持要存储的整数的数量。
    • Stride 保持Int类型的跨度。
    • Alignment 持有Int类型的对齐方式。
    • ByteCount 保存需要的字节总数。
  2. do块增加了一个范围级别,所以你可以在接下来的例子中重复使用这些变量的名字。
  3. UnsafeMutableRawPointer.allocate分配所需的字节。这个方法返回一个UnsafeMutableRawPointer。该类型的名称告诉你该指针可以加载和存储,或 mutate ,原始字节。
  4. 一个 "defer "块可以确保你正确地去分配指针。ARC在这里并不能帮助你--你需要自己处理内存管理问题 你可以在Swift官方文档中阅读更多关于defer语句
  5. storeBytesload,不出所料,存储和加载字节。你通过推进指针的stride字节来计算第二个整数的内存地址。由于指针是Strideable,你也可以使用指针运算,比如。(pointer+stride).storeBytes(of: 6, as: Int.self)
  6. 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类型的值。
  • 你必须在使用前初始化类型的内存,并在使用后去初始化它。你可以分别用initializedeinitialize方法来做。去初始化只对_非琐碎类型_有要求。然而,包括去初始化是一个很好的方法,可以在你改变为非微不足道的东西时保护你的代码。这通常不需要花费什么,因为编译器会把它优化掉。
  • 类型化指针有一个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也可以作为ArrayData的实例方法使用。

计算校验和

使用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

下面是正在发生的事情。

  1. 分配一个compression_stream,用defer块安排它的去分配。

  2. 然后,使用pointee属性,获得流并将其传递给compression_stream_init函数。

    编译器在这里做了一些特别的事情。它使用in-out &标记来获取你的compression_stream,并将其变成UnsafeMutablePointer<compression_stream>。另外,你也可以通过streamPointer。那么你就不需要这种特殊的转换。

  3. 最后,你创建一个目标缓冲区,作为你的工作缓冲区。

接下来,完成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
}

这是真正发生工作的地方。下面是它正在做的事情。

  1. 创建一个Data对象,它将包含输出--压缩或解压缩的数据,取决于这是什么操作。
  2. 用你分配的指针和它们的大小来设置源缓冲区和目的缓冲区。
  3. 在这里,你继续调用compression_stream_process,只要它返回COMPRESSION_STATUS_OK
  4. 然后你把目标缓冲区复制到output中,这个函数最终会返回。
  5. 当最后一个数据包进来时,标记为COMPRESSION_STATUS_END,你可能只需要复制目标缓冲区的一部分。

在这个例子中,你可以看到10,000个元素的数组被压缩到了153字节。不算太差。

不安全的Swift实例2。随机生成器

随机数对很多应用都很重要,从游戏到机器学习。

macOS提供了arc4random,可以产生具有密码学意义的随机数。不幸的是,这个调用在Linux上是不可用的。此外,arc4random只提供UInt32的随机数。然而, /dev/urandom 提供了一个好的随机数的无限来源。

在本节中,你将使用你的新知识来读取这个文件并创建类型安全的随机数。

image.png

首先创建一个新的游戏场,将其称为 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的信息。

我希望你喜欢这个教程。如果你有问题或经验想分享,欢迎在论坛上分享


www.deepl.com 翻译