创建 Swift 自定义集合类

前端之家收集整理的这篇文章主要介绍了创建 Swift 自定义集合类前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

原文:Building a Custom Collection in Swift
作者:Eric Cerney
译者:kmyhy

数组、字典和集合是常见的集合类型,它们都内置在 Swift 标准库中。但如果它们不能满足你的 App 的需要的时候怎么办?

一种最常见的办法是使用 Array 或 Dictionary,然后用一堆业务逻辑去保存你的数据结构。但这种方式太过于直接且难于维护。

这样,创建自定义集合类型就变得有意义了。在本文,你将学习用 Swift 的 collection 协议创建自定义集合类型。

当文本结束,你会拥有一个强大的自定义集合类型,拥有 Swift 内置集合的所有功能

注:本文用 Swift 3.0。小于次的版本无法编译,因为 Swift 标准库发生了剧烈改变。

开始

在本文中,你将从头开始创建一个“多集合”(Bag)类型。

一个 Bag 对象就像一个 Set,用于存储不会重复的对象。在一个 Set 集合中,重复对象会被忽略。在一个 Bag 中,每个对象都会被算进去。

一个好例子是购物清单。你拥有一个清单,每个商品都和一个数量关联。如果添加了重复的商品,则我们会增加已有商品的数量,而不是重新插入一条商品记录。

在介绍 collection 协议前,首先来实现一个基本的 Bag。
创建一个新的 Playground:在 Xcode 中,选择 File\New\Playground… 然后给 playground 命名为 Bag。 你可以选择任何平台,因为本教程是和平台无关的,你只需要选择 Swift 语言就可以了。
点击 Next,选择一个地方保存 playground,然后点 Create。

编辑 playground 文件为:

struct Bag<Element: Hashable> { }

Bag 结构是一个泛型结构,需要元素类型必须是 Hashable 的。Hashable 允许你对元素进行比较,在 0(1)时间复杂度上只存储唯一值。也就是说,无论内容有多复杂,Bag 存取速度相同。你通过定义一个结构体,强制让它具备值语义(C++ 术语),这就和 Swift 标准库保持一致了。

然后为 Bag 添加属性

// 1
fileprivate var contents: [Element: Int] = [:]

// 2
var uniqueCount: Int {
  return contents.count
}

// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

这是 Bag 的基本属性

  • 用一个作为内部存储结构。因为字典的键是唯一的,你可以用它来存储数据。字典的值则表示每个元素的个数。fileprivate 关键字表示这个属性是隐藏的,在外部不可访问。
  • uniqueCount 返回了字典的每一种对象的统计值,并不累加它们每一个的数量。例如,一个 Bag 中有 4 个橙子和 2 个苹果只会返回 2。
  • totalCount 返回的是 Bag 中所有对象的总计。以同一个例子为例,totalCount 返回值为 6。

现在,需要几个方法以便增减 Bag 中的内容。在属性声明下面加入:

// 1
mutating func add(_ member: Element,occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0,"Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

代码解释如下:

  • add(_:occurrences:) 方法提供了增加元素的方法。它需要两个参数:泛型参数 Element 和一个 Optional 的元素个数。如果 Bag 实例以常量形式 let 定义而不是变量 var 形式定义的话,则这个方法无效。
  • precondition(_:_:)方法的第一个参数是一个 Boolean 表达式,如果为 false,则程序会中断,并在 Debug 窗口输出第二个参数的内容。这个方法有一个前置条件,以保证 Bag 能够被正确使用。这个方法检查了调用 add 方法时符合我们的预设。
  • if 语句判断元素是否已经存在,如果存在,则累加它的计数器,否则加入新的元素。

另外,你还需要一个删除元素的方法。在 add 方法后新增方法

mutating func remove(_ member: Element,occurrences: Int = 1) {
  // 1
  guard let currentCount = contents[member],currentCount >= occurrences else {
    preconditionFailure("Removed non-existent elements")
  }

  // 2
  precondition(occurrences > 0,"Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

remove(_:occurrences:) 方法使用的参数和 add 方法一模一样,只不过做了相反的事情:

  • 首先判断元素是否存在,确保在删除它时起码有足够的数目能够被删除
  • 然后保证元素个数大于 0.
  • 最后,如果元素存在则将元素个数减少。如果元素个数减少后小于等于 0,直接删除整个元素。

这里,Bag 还不能干更多的事情,甚至无法访问它的内容。你还不能使用 Dictionary 中存在的高阶方法

但亡羊补牢为时未晚。我们开始在 Bag 中一一添加这些代码。现在的任务是保持你的代码整洁。

请先等一下!Swift 提供了让 Bag 符合传统集合的所有工具。
你需要先了解一下在 Swift 中,让一个对象变成集合需要做些什么。

自定义集合需要做些什么?

要理解什么是 Swift 集合,首先需要它继承的协议层次:

Sequence 协议表示类型支持排序、以迭代的方式访问其元素。你可以把一个 Sequence 对象视作一个元素的列表,允许你挨个挨个地访问其中的元素。

迭代(Iteration)是一个简单概念,但它能给你的对象提供许多功能。它允许你各种强大的操作比如:

  • map(_:): 用一个闭包将 Sequence 中的每个元素挨个进行转换,并构成另一个数组返回。
  • filter(_:): 用一个闭包过滤所需的元素,将符合闭包谓词所指定条件的元素放到新数组中返回。
  • reduce(_:_:): 用一个闭包将 Sequence 中的所有元素合并成一个值返回。
  • sorted(by:): 根据指定的闭包谓词,将 Sequence 中的元素进行排序并返回排序后的数组。

这只是其中很少的一部分功能。要查看 Sequence 中提供的所有方法,请查看 Sequence 的文档

需要说明的一点是,采用 Sequence 协议的类型强制要求是破坏性的或者是非破坏性的。这意味着,在迭代之后,无法保证下一次迭代会从头开始。

这是一个大问题,如果你的数据准备迭代不止一次的话。要实现非破坏性的迭代,你的对象需要使用 Collection 协议。

Collection 协议继承了 Sequence 和 Indexable 协议。Collection 和 Sequence 的主要区别是,你可以迭代多次,而且可以用索引来访问。

实现 Collection 协议之后,你会获得更多“免费”的方法属性,例如:

  • isEmpty: 返回一个布尔值,表示集合是否为空。
  • first: 返回集合中的第一个元素。
  • count: 返回集合中的元素个数。

依据集合中的元素类型的不同,你还可能拥有更多的方法属性。如果你想了解更多,请查看Collection 的文档

在实现这些协议之前,Bag 还有一个地方需要改进。

打印对象

当前,Bag 对象可以用 print(_:) 方法或在 Result Sidebar 视图中暴露出的信息很少。
在 Playground 中加入如下代码

var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange",occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

这里创建了一个 Bag 对象,并加入了几种水果。如果你查看Playground 的调试窗口,你会看到这些对象的类型信息而不是它保存的内容

你可以用 Swift 标准库中的一个协议来解决这个问题。在 shoppingCart 变量上面的 Bag 类型定义结束的 } 之后添加

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

采用 CustomStringConvertible 协议需要实现一个属性,叫做 description。这个属性返回一个实例对象的文字表示。

在这里,你可以放入任何足以表示你的数据的逻辑。因为字典已经继承了这个协议,你可以简单调用 contents 对象的 description 值。

看一眼 shopingCart 的 debug 信息:

漂亮!现在你已经为 Bag 添加功能,你可以对它的 contents 进行校验了。

在 Playground 中编写代码时,你可以使用 precondition(_:_:) 来检验返回结果。这会避免你突然破坏之前编写的功能。可以用这个工具作为你的单元测试——将它放到你的日常编码中去做是一个不错的主意!

在最后一次调用 remove(_:occurrences:) 之后加入:

precondition("\(shoppingCart)" == "\(shoppingCart.contents)","Expected bag description to match its contents description")

如果 shoppingCart 的 description 属性不等于 contents 的 description,则会导致一个错误

为了创建我们屌爆了的集合类型,接下来的步骤自然就是初始化。

初始化

每次只能加一个元素真的很烦。通常的办法是在初始化的时候用另一个集合来进行初始化。

这正是我们希望 Bag 能够做到的。在 Playground 的最后加入:

let dataArray = ["Banana","Orange","Banana"]
let dataDictionary = ["Banana": 2,"Orange": 1]
let dataSet: Set = ["Banana","Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary,"Expected arrayBag contents to match \(dataDictionary)") var dictionaryBag = Bag(dataDictionary) precondition(dictionaryBag.contents == dataDictionary,"Expected dictionaryBag contents to match \(dataDictionary)") var setBag = Bag(dataSet) precondition(setBag.contents == ["Banana": 1,"Orange": 1],"Expected setBag contents to match \(["Banana": 1,"Orange": 1])")

无法进行编译,因为还没有定义针对这些类型的初始化方法。不要为每种类型创建一种初始化方法,你可以使用泛型。
在 Bag 定义的 totalCount 后面添加

// 1
init() { }
 // 2
init<S: Sequence>(_ sequence: S) where S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}
 // 3
init<S: Sequence>(_ sequence: S) where S.Iterator.Element == (key: Element,value: Int) {
  for (element,count) in sequence {
    add(element,occurrences: count)
  }
}

代码解释如下:

  • 首先,创建一个空的 init 方法。在定义了其他初始化方法之后,你必须添加这个方法,否则编译器会报错。
  • 然后,定义一个初始化方法并接受一个元素为 Sequence 集合的参数。sequence 参数的类型必须和元素类型匹配。例如,Array 和 Set 对象。然后,对 sequence 进行迭代,挨个添加元素。
  • 最后一个方法类似,但元素类型变成元素了(Element,Int)。这种情况最典型的例子就是字典。 这里,你依然对 sequence 中的元素进行迭代并以指定的个数来添加元素。

这些泛型初始化方法为 Bag 对象添加了大量的数据源。但是,它们仍然你初始化另一个 Sequence 对象然后传递给 Bag。

为了避免这个,Swift 标准库提供了两个协议。这两个协议支持以 Sequence 的写法进行初始化。这种写法能让你用更简短的方式定义数据,而不必显式地创建一个对象。

在 Playground 最后加入下列代码,来看看如何使用这种写法:

var arrayLiteralBag: Bag = ["Banana","Banana"]
precondition(arrayLiteralBag.contents == dataDictionary,"Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2,"Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary,"Expected dictionaryLiteralBag contents to match \(dataDictionary)")

没说的,编译器报错了,我们后面再来解决。这种写法用初始化数组和字典的写法来进行初始化,而不需要创建对象。

在 Bag 的其它扩展之后定义两个扩展:

extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element,Int)...) {
    // The map converts elements to the "named" tuple the initializer expects.
    self.init(elements.map { (key: $0.0,value: $0.1) })
  }
}

ExpressibleByArrayLiteral 和 ExpressibleByDictionaryLiteral 扩展需要实现一个初始化方法,以处理它们对应的参数的那种写法。由于前面已经定义的初始化方法,它们的实现都非常简单。

现在 Bag 已经非常像原生的集合类型了,我们该来点猛货了。

Sequence

集合类型的最常用的操作是对其元素进行迭代。来看一个例子,在 Playground 最后添加如下代码

for element in shoppingCart {
  print(element)
}

超级简单。就像数组和字典一样,你可以遍历一个 Bag 对象。因为 Bag 还没有实现 Sequence 协议,编译不能通过。
在 ExpressibleByDictionaryLiteral 扩展后加入另一个扩展:

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator<Element,Int>

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

不需要实现太多方法代码解释如下:

  • 定义了一个类型别名,叫做 Iterator,这是 Sequence 中指定的,需要实现 IteratorProtocol 协议。DictionaryIterator 类型是字典用于迭代其元素的。你可以用它,因为 Bag 在底层使用了字典来存储数据的。
  • makeIterator() 方法返回一个 Iterator,它会用来对序列中的每个元素进行迭代。
  • 调用 contents 的 makeIterator() 方法创建一个 Iterator,它已经实现了 Sequence 协议。

这就是 Bag 对 Sequence 协议进行实现的全部。
你现在可以迭代 Bag 对象中的每个元素了,并可以获得每个对象的个数。在之前的 for-in 循环后加入:

for (element,count) in shoppingCart {
  print("Element: \(element),Count: \(count)")
}

打开 Debug 视图,你会看到每个元素都打印出来了:

实现 Sequence 协议之后,你就可以使用许多 Sequence 中有的方法了。

在 Playground 最后加入代码试一试:

// 查找所有数目大于 1 的对象
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2,"Expected moreThanOne contents to be [(\"Banana\",2)]")

// 获取所有对象的数组(不需要数量)
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(itemList == ["Orange","Banana"],"Expected itemList contents to be [\"Orange\",\"Banana\"]")

// 获得所有对象的加总数据
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3,"Expected numberOfItems contents to be 3")

// 所有商品按照数量降序排序
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(sorted.first!.key == "Banana" && moreThanOne.first!.value == 2,"Expected sorted contents to be [(\"Banana\",2),(\"Orange\",1)]")

所有 Sequence 对象能有的方法都能用——它们完全是免费的。

现在,你可能满足于以这种方式使用 Bag,但还有比这更好玩的吗?当前的 Squence 实现仍然还有改进的余地。

加强版的 Sequence

当前,你依赖于字典为你提供底层支持。这很好,对你来说,这不乏为一种轻松实现功能强大的集合的方式。问题是,它会让 Bag 的用户感到奇怪和困惑。

例如,Bag 会返回一个 DictionaryIterator 类型的 Iterator 好像不妥。你可以创建自己的 Iterator 类型,但这次不是免费的了。

Swift 提供了一个 AnyIterator 类型,将底层的 itertator 隐藏起来。
将 Sequence 的实现修改为:

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator<(element: Element,count: Int)>

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

Playground 报了一堆错,等会来解决。除了使用了一个 AnyIterator 外,这个实现和之前没有太大区别:

  1. AnyIterator 是一个无类型的 Iterator,它只暴露出底层实际的 Iterator 的 next() 方法给你。这样你就可以将实际使用的 Iterator 类型隐藏起来。
  2. 跟前面一样,通过 contents 创建了一个新的 DictionaryIterator 实例。
  3. 最后,将 Iterator 包装成 AnyIterator 以传递其 next() 方法

现在来解决先前的报错。你看到的是这两个错误

前面,你用 DictionaryIterator 的元组命名为 key 和 value。现在你已经将 DictionaryIterator 隐藏起来了,并且将元组的名字修改为 element 和 count。要解决这个错误,将 key 和 value 替换成 element 和 count。

这样你的 precondition 语句的问题就解决了。这就是前置条件的好处了,它能保证某些东西不会被意外修改

现在,任何人都不知道你在用字典进行所有的工作。
现在的 Bag 让你感觉更好了吧?是时候让它回家了。呃,将你激动的心情收起来吧,它是属于集合的!

Collectdion

闲话少说,接下来还有一道大菜,创建一个集合……即 Collection 协议!再次声明,集合是能够通过索引进行访问并进行多次非破坏性迭代的结合。

为了符合 Collection 协议,你需要提供这些数据:

  1. startIndex 和 endIndex: 指定集合的边界,并说明遍历的起点。
  2. subscript (position:): 允许你通过索引找到集合中的任意元素。这个访问方法的时间复杂度应控制在 0(1) 上。
  3. index(after:): 返回传入的索引的下一个索引。

使用 Cellection 协议时只需要这 4 个数据。在 Sequence 扩展后新增扩展:

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex<Element,Int>

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position),"out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key,count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

代码非常简单:

  1. 首先用 DictionaryIndex 来声明一个 Index 类型,DictionaryIndex 在 Collection 协议中已定义。注意,其实编译器会基于你后面的实现来推断这个类型,但为了保持代码的清晰和可维护性,我们显示地指定了类型。
  2. 然后用 contents 来返回第一个索引和最后一个索引。
  3. 用一个前置条件来强制校验索引的有效性。然后,以元组的方式返回 contents 中位于该索引的元素。
  4. 最后,调用 contents.index(after:) 方法并返回结果。

通过添加几个属性方法,你创建了一个功能完整的集合!在 Playground 最后用几行代码来测试这些功能

// 读取 Bag 中的第一个对象
let firstItem = shoppingCart.first
precondition(firstItem!.element == "Orange" && firstItem!.count == 1,"Expected first item of shopping cart to be (\"Orange\",1)")

// 判断 Bag 是否为空
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,"Expected shopping cart to not be empty")

// 获取 Bag 中的商品种类数
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,"Expected shoppingCart to have 2 unique items")

// 查找第一个名为 Banana 的元素
let bananaIndex = shoppingCart.indices.first { shoppingCart[$0].element == "Banana" }!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2,"Expected banana to have value (\"Banana\",2)")

漂亮!(当你对自己所做的一切感到心满意足时,“等等,你还可以做得更好”这句话又在等着你了……)

是的,你说的没错!你可以做得更好。仍然能够从 Bag 中看出一丝字典的模样。

加强版的 Collection

Bag 暴露了太多底层实现细节。Bag 的用户仍然需要使用 DictionaryIndex 对象去访问集合中的元素。
这个很好搞定。在 Collection 扩展后面增加

// 1
struct BagIndex<Element: Hashable> {
  // 2
  fileprivate let index: DictionaryIndex<Element,Int>

  // 3
  fileprivate init(_ dictionaryIndex: DictionaryIndex<Element,Int>) {
    self.index = dictionaryIndex
  }
}

没有任何新奇的玩意儿,但我们还是来过一下吧:

  1. 定义了一个泛型类型 BagIndex,和 Bag 一样,为了访问字典对象,它需要一个 Hashable 的泛型参数。
  2. index 类型的真实类型是一个私有的 DictionaryIndex 对象。BagIndex 仅仅是一个封装,隐藏了它真正的 index 类型。
  3. 最后,创建一个私有的初始化方法,接收一个 DictionaryIndex 参数。

Collection 对象需要索引对象实现 Comparable 协议,能够对两个索引进行比较以进行某些操作。因此,BagIndex 必须实现 Comparable 协议。在 BagIndex 扩展之后添加

extension BagIndex: Comparable {
  static func == (lhs: BagIndex,rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func < (lhs: BagIndex,rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

这个逻辑非常简单;方法返回的结果直接调用 DictionaryIndex 已实现的 Comparable 协议的相同方法

现在将 Bag 修改为使用 BagIndex。将 Collecdtion 扩展替换成:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex<Element>

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position),"out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key,count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

注释中的数字标出了修改之处。分别解释如下:

  1. 首先将 Index 的类型从 DictionaryIndex 修改为 BagIndex。
  2. 然后,是 startIndex 和 endIndex,你换成新的 BagIndex。
  3. 接着,用这个 BagIndex 从 contents 后访问对应元素并返回。
  4. 最后,结合上面几个步骤。从 contents 中获取 DictionaryIndex,然后用它来创建一个 BagIndex。

就这样!用户不会知道你底层是用什么来存储数据的了。你未来还有可能对索引对象的获得更大的控制。

在完成之前,还有一个很重要的地方。除了基于索引来访问元素,你还以通过一段连续索引来访问集合中的值。

要实现这个,你可以看一下集合中是如何进行切片操作的。

切片

切片就是查看集合中多个连续的元素。它允许你在集合元素的子集上进行某些操作,而不用复制这些元素。
切片只会保存原来集合中的元素的引用。它还存储了元素子集的起、始索引。切片的时间复杂度为 0(1),因为它们直接引用了它的原始集合。

切片直接重用原始集合的索引,这使得它们尤其有用。
要测试切片操作,在 Playground 最后添加代码

// 1
let fruitBasket = Bag(dictionaryLiteral: ("Apple",5),("Orange",2),("Pear",3),("Banana",7))

// 2
let fruitSlice = fruitBasket.dropFirst() // No pun intended ;]

// 3
if let fruitMinIndex = fruitSlice.indices.min(by: { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let minFruitFromSlice = fruitSlice[fruitMinIndex]
  let minFruitFromBasket = fruitBasket[fruitMinIndex]
}

我们来看一下这些代码做了些什么,以及它们的意思:

  1. 首先,创建一个由 4 种水果构成的水果篮。
  2. 然后拿走第一种水果。这会创建一个水果篮的切片,但第一种水果不见了。
  3. 通过切片找到数量最少的水果的索引。
  4. 尽管上一步的索引是从切片中得到的,你可以把这个索引同时用在原来的集合和后面的切片中来访问对象。

注意:切片对于基于哈希的字典和 Bag 来说,用处不大,因为它们的顺序在任何方向上都是不确定的。而数组则相反,数组是集合中的一个极端例子,在执行顺序操作时,切片扮演很重要的角色。

祝贺你——你现在是一个集合方面的专家了!你可以创建任意内容的 Bag 对象来表示庆祝。

结束

完整的 Playground 代码可以在这里下载。如果你想了解或者实现更完整的 Bag,请从 github 中 checkout 项目。

在这篇文章里,你学习了在 Swift 中,如何通过一个数据结构来创建自己的集合。你使用了 Sequence 、Collection 、CustomStringConvertible、 ExpressibleByArrayLiteral、ExpressibleByDictionaryLiteral 协议以及资第一的索引类型。

这仅仅是对 Swift 提供的用于创建健壮、使用的结合类型的协议的一点尝试。如果你想看一下还有什么其他的协议,你可以参考:

希望你能喜欢这篇教程!创建自定义的集合并不是一个常见的需求,当它能加深你对 Swift 内置集合类型的裂解。

如果有任何疑问或建议,请在下面留言。

原文链接:https://www.f2er.com/swift/322288.html

猜你在找的Swift相关文章