《Pro Swift》 第二章:类型(Types)

第一章:语法(Syntax)

我最喜欢的 Swift 单行代码是使用flatMap()来对一个数组进行降维和过滤:

let myCustomViews = allViews.flatMap { $0 as? MyCustomView }

这行代码看起来很简单,但它包含了很多很棒的 Swift 特性,如果你将其与 Objective-C 中最接近的开箱即用的特性进行比较,就会发现这些特性最为明显:

NSArray<MyCustomView *> *myCustomViews = (NSArray<MyCustomView *> *) [allViews filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
      return [evaluatedObject isKindOfClass:[MyCustomView class]];
}]];

-- Matt Gallagher, CocoaWithLove.com 的作者

高效初始化器(Useful initializers)

理解 Swift 中初始化器是如何工作的并不容易,但这也是你们很久以前学过的东西,所以我就不在这里重复了。相反,我想关注一些有趣的初始化器,它们可能有助你更有效地使用常见的 Swift 类型。

重复值(Repeating values)

我最喜欢的字符串和数组初始化器是 repeat:count:,它允许你快速创建大量值。例如,你可以通过在一些文本下面写等号来创建 Markdown 文本格式的标题,如下所示:

This is a heading
=================

Markdown 是一种很有用的格式,因为它可以被计算机解析,同时对人类也具有视觉吸引力,而且下划线为repeat:count: 提供了一个很好的例子。要使用这个初始化器,为其第一个参数指定一个字符串,并为其第二个参数指定重复的次数,如下所示:

let heading = "This is a heading"
let underline = String(repeating: "=", count: heading.characters.count)

你也可以对数组这样做:

let equalsArray = [String](repeating: "=", count: heading.characters.count)

这个数组初始化器足够灵活,你可以使用它非常容易地创建多维数组。例如,这创建了一个准备填充 10x10 数组:

 var board = [[String]](repeating: [String](repeating: "", count: 10), count: 10)

转换为数字和从数字转换(Converting to and from numbers)

当我看到这样的代码时,我头疼不已:

let str1 = "\(someInteger)"

这是浪费和不必要的,但是字符串插值是一个很好的特性,使用它是值得原谅。事实上,我很确定我已经使用过它好几次了,毫无疑问!

Swift 有一个简单、更好的方法,可以使用初始化器根据整型创建字符串类型:

let str2 = String(someInteger)

当使用这种方式进行转换时,事情会变得稍微困难一些,因为你可能会尝试传入一个无效的数字,例如:

let int1 = Int("elephant")

那么,这个初始化器将返回 Int? :如果你给它一个有效的数字,你会得到一个整数,否则你会得到nil

如果你不想要一个可选值,你应该对结果解包:

if let int2 = Int("1989") {
   print(int2)
}

或者,使用空合操作符(??)提供一个合理的默认值,如下所示:

let int3 = Int("1989") ?? 0
print(int3)

Swift 在这两个初始化器上有一些处理不同变量基数的变体。例如,如果你想使用十六进制(以 16 为基数),你可以让 Swift 给你一个十六进制数字的字符串表示形式:

let str3 = String(28, radix: 16)

这将把 str3 设置为 1c。如果你更喜欢 1C,即大写——请尝试以下方法:

let str4 = String(28, radix: 16, uppercase: true)

要将其转换回整数—请记住它是可选值!——用这个:

let int4 = Int("1C", radix: 16)

唯一的数组(Unique arrays)

如果你有一个包含重复值的数组,并且希望找到一种快速删除重复值的方法,则你需要的找的是Set。这是一个内建的数据类型,具有与普通数组互相转换的初始化器,这意味着你只需使用初始化器即可快速高效地消除数组中的重复数据:

let scores = [5, 3, 6, 1, 3, 5, 3, 9]
let scoresSet = Set(scores)
let uniqueScores = Array(scoresSet)

这就是它所需要的一切——难怪我这么喜欢集合!

字典的容量(Dictionary capacities)

以一个简单的初始化器结尾:如果要单独向字典添加项,但是知道想添加多少项,请使用minimumCapacity:initializer创建字典,如下所示:

var dictionary = Dictionary<String, String>(minimumCapacity: 100)

这有助于通过预先分配足够的空间来快速优化执行。注意:在后台,Swift 的字典增加了 2 的幂次方的容量,所以当你请求一个像 100 这样的非 2 的幂次方的容量时,你实际上会得到一个最小容量为 128 的字典。记住,这是最小容量——如果你想添加更多的对象,这不是问题。

枚举(Enums)

在模式匹配一章中,我已经讨论了枚举关联值,但这里我想重点讨论枚举本身,因为它们的功能非常强大。

让我们从一个非常简单的枚举开始,跟踪一些基本的颜色:

enum Color {
   case unknown
   case blue
   case green
   case pink
   case purple
   case red
}

如果你愿意,可以将所有case项写在一行上,如下所示:

enum Color {
   case unknown, blue, green, pink, purple, red
}

为了便于测试,让我们用一个表示玩具的简单结构体来包装它:

struct Toy {
   let name: String
   let color: Color
}

Swift 的类型推断可以推断出Toycolor属性是一个Color枚举,这意味着在创建玩具结构体时不需要编写Color.blue。例如,我们可以创建两个这样的玩具:

let barbie = Toy(name: "Barbie", color: .pink)
let raceCar = Toy(name: "Lightning McQueen", color: .red)

初始值(Raw values)

让我们从初始值开始:每个枚举项的基础数据类型。默认情况下,枚举没有初始值,因此如果需要初始值,则需要声明它。例如,我们可以给颜色一个这样的整型初始值:

enum Color: Int {
   case unknown, blue, green, pink, purple, red
}

只需添加:Int Swift 将为每种颜色都指定了一个匹配的整数,从0 开始向上计数。也就是说,unknown等于 0blue等于 1 ,以此类推。有时,默认值对你来说并没有用,所以如果需要,你可以为每个初始值指定单独的整数。或者,你可以指定一个不同的起点,使 Xcode 从那里开始计数。

例如,我们可以像这样为太阳系的四个行星创建一个枚举:

enum Planet: Int {
   case mercury = 1
   case venus
   case earth
   case mars
   case unknown
}

通过明确指定水星的值为 1Xcode 将从那里向上计数:金星是 2,地球是 3,火星是 4

现在行星的编号是合理的,我们可以像这样得到任何的行星的初始值:

let marsNumber = Planet.mars.rawValue

另一种方法并不那么容易:是的,既然我们已经有了初始值,你可以从一个数字创建一个Planet 枚举,但是这样做会创建一个可选的枚举。这是因为你可以尝试创建一个初始值为 99 的行星,而这个行星并不存在——至少目前还不存在。

幸运的是,我在行星枚举中添加了一个unknown,当请求无效的行星编号时,我们可以从其初始值创建行星枚举,并使用空值合并运算符提供合理的默认值:

let mars = Planet(rawValue: 556) ?? Planet.unknown

对于行星来说,数字是可以的,但是当涉及到颜色时,你可能会发现使用字符串更容易。除非你有非常特殊的需要,否则只需指定String作为枚举的原始数据类型就足以为它们提供有意义的名称—— Swift 会自动将你的枚举名称映射到一个字符串。例如,这将打印 Pink:

enum Color: String {
   case unknown, blue, green, pink, purple, red
}
let pink = Color.pink.rawValue
print(pink)

不管初始值的数据类型是什么,或者是否有初始值,当枚举被用作字符串插值的一部分时,Swift 都会自动对枚举进行字符串化。但是,以这种方式使用并不会使它们变成字符串,所以如果你想调用任何字符串方法,你需要自己根据它们创建一个字符串。例如:

let barbie = Toy(name: "Barbie", color: .pink)
let raceCar = Toy(name: "Lightning McQueen", color: .red)
// regular string interpolation
print("The \(barbie.name) toy is \(barbie.color)")
// get the string form of the Color then call a method on it
print("The \(barbie.name) toy is \(barbie.color.rawValue.uppercased())")

计算属性和方法(Computed properties and methods)

枚举没有结构体和类那么强大,但是它们允许你在其中封装一些有用的功能。例如,除非枚举存储的属性是静态的,否则不能给它们赋值,因为这样做没有意义,但是你可以添加在运行一些代码之后返回值的计算属性。

为了让你了解一些有用的内容,让我们向Color枚举添加一个计算属性,该属性将打印颜色的简要描述。

enum Color {
   case unknown, blue, green, pink, purple, red
   var description: String {
      switch self {
      case .unknown:
         return "the color of magic"
      case .blue:
         return "the color of the sky"
      case .green:
         return "the color of grass"
      case .pink:
         return "the color of carnations"
      case .purple:
         return "the color of rain"
      case .red:
         return "the color of desire"
      }
   } 
}
let barbie = Toy(name: "Barbie", color: .pink)
print("This \(barbie.name) toy is \(barbie.color.description)")

当然,计算属性只是封装方法的语法糖,所以你可以直接将方法添加到枚举中也就不足为奇了。现在让我们通过向Color 枚举添加两个新方法来实现这一点,forBoys()forGirls(),根据颜色来判断一个玩具是为女孩还是男孩准备的——只需在我们刚刚添加的description 属性下面添加以下内容:

func forBoys() -> Bool {
   return true
}
func forGirls() -> Bool {
   return true
}

如果你想知道,根据颜色来决定哪个玩具是男孩的还是女孩的有点上世纪 70 年代的味道:这些方法都返回true是有原因的!

因此:我们的枚举现在有一个初始值、一个计算属性和一些方法。我希望你能明白为什么我把枚举描述为看起来很强大——它们可以做很多事情!

数组(Arrays)

数组是 Swift 的真正主力之一。当然,它们在大多数应用程序中都很重要,但是它们对泛型的使用使它们在添加一些有用功能的同时保证类型安全。我不打算详细介绍它们的基本用法;相反,我想向你介绍一些你可能不知道的有用方法。

第一:排序。只要数组存储的元素类型遵循Comparable协议,就会得到sorted()sort()方法——前者返回一个已排序的数组,而后者修改调用它的数组。如果你不打算遵循Comparable协议,可以使用sorted()sort()的替代版本,让你指定数据项应该如何排序。

为了演示下面的例子,我们将使用这两个数组:

var names = ["Taylor", "Timothy", "Tyler", "Thomas", "Tobias", "Tabitha"]
let numbers = [5, 3, 1, 9, 5, 2, 7, 8]

要按字母顺序排列names数组,使用sorted()sort()方法取决于你的需要。

let sorted = names.sorted()

一旦代码运行,sorted将包含["Tabitha", "Taylor", "Thomas", "Timothy", "Tobias", "Tyler"]

如果你想编写自己的排序函数 - 如果你不采用Comparable则是必需的,否则是可选的 - 编写一个接受两个字符串的闭包,如果第一个字符串应该在排在第二个字符串之前,则返回true

例如,我们可以编写一个字符串排序算法,它的行为与常规的字母排序相同,但它总是将名称 Taylor 放在前面。我敢肯定,这正是Taylor Swift(美国女歌手)想要的:

names.sort {
   print("Comparing \($0) and \($1)")
   if ($0 == "Taylor") {
      return true
   } else if $1 == "Taylor" {
      return false
   } else {
      return $0 < $1
  } 
}

该代码使用sort()而不是sorted(),这将使数组按适当位置排序,而不是返回一个新的排序数组。我还在其中添加了一个print()调用,这样你就可以确切地看到sort()是如何工作的。这是输出结果:

Comparing Timothy and Taylor
Comparing Tyler and Timothy
Comparing Thomas and Tyler
Comparing Thomas and Timothy
Comparing Thomas and Taylor
Comparing Tobias and Tyler
Comparing Tobias and Timothy
Comparing Tabitha and Tyler
Comparing Tabitha and Tobias
Comparing Tabitha and Timothy
Comparing Tabitha and Thomas
Comparing Tabitha and Taylor

如你所见,随着算法的发展,名称可以显示为 $0$1,这就是为什么我在自定义排序函数中比较这两种可能性的原因。

排序很容易,但采用Comparable还可以实现两个更有用的方法:min()max()。就像sort()一样,如果不采用Comparable的方法,这些方法也可以接受一个闭包,但是代码是相同的,因为操作是相同的:A项应该出现在B项之前吗?

使用前面的number数组,我们可以在两行代码中找到数组中的最高值和最低值:

let lowest = numbers.min()
let highest = numbers.max()

对于字符串,min()返回排序后的第一个字符串,max()返回最后一个字符串。如果你尝试重用我为自定义排序提供的相同闭包,包括print()语句,你将看到min()max()实际上比使用sort()更高效,因为它们不需要移动每一项。

遵循Comparable协议(Conforming to Comparable)

对于字符串和整型等基本数据类型,使用sort()min()max()非常简单。但是你怎么把别的东西完全分类呢,比如奶酪的种类或者狗的品种?我已经向你展示了如何编写自定义闭包,但是如果你必须进行多次排序,那么这种方法就会变得非常麻烦—你最终会复制代码,这将带来维护的噩梦。

更聪明的解决方案是实现Comparable协议,这反过来要求你使用操作符重载。稍后我们将对此进行更详细的讨论,但现在我只想向你展示足以进行比较的工作。首先,这里有一个基本的Dog结构,它包含一些信息:

struct Dog {
   var breed: String
   var age: Int
}

为了便于测试,我们将创建三只 dog 并将它们放到数组里:

let poppy = Dog(breed: "Poodle", age: 5)
let rusty = Dog(breed: "Labrador", age: 2)
let rover = Dog(breed: "Corgi", age: 11)
var dogs = [poppy, rusty, rover]

因为 Dog结构体没有遵循 Comparable协议,所以我们没有在dogs数组上获得简单的sort()ordered()方法,我们只获得了需要自定义闭包才能运行的方法。

要使Dog遵循 Comparable协议,如下所示:

struct Dog: Comparable {
   var breed: String
   var age: Int
}

你会得到错误,没关系。

下一步是让第一次尝试它的人感到困惑的地方:你需要实现两个新函数,但是它们有一些不同寻常的名称,在处理操作符重载时需要一点时间来适应,这正是我们需要做的。

Dog结构中添加这两个函数:

static func <(lhs: Dog, rhs: Dog) -> Bool {
   return lhs.age < rhs.age
}
static func ==(lhs: Dog, rhs: Dog) -> Bool {
   return lhs.age == rhs.age
}

需要说明的是,你的代码应该如下所示:

struct Dog: Comparable {
   var breed: String
   var age: Int
   static func <(lhs: Dog, rhs: Dog) -> Bool {
      return lhs.age < rhs.age
   }
   static func ==(lhs: Dog, rhs: Dog) -> Bool {
      return lhs.age == rhs.age
   }
}

如果你以前没有使用过运算符重载,那么这些函数名是不常见的,但是我希望你能够确切地了解它们的作用: 当你编写dog1 < dog2时使用 < 函数,当你写dog1 == dog2时使用==函数。

这两个步骤足以完全实现Comparable协议,因此你现在可以轻松地对dogs数组进行排序:

dogs.sort()

添加和删除元素(Adding and removing items)

几乎可以肯定,你已经使用过数组的append()insert()remove(at:)方法,但我想确保你知道添加和删除项的其他方法。

如果想将两个数组相加,可以使用++=来就地相加。例如:

let poppy = Dog(breed: "Poodle", age: 5)
let rusty = Dog(breed: "Labrador", age: 2)
let rover = Dog(breed: "Corgi", age: 11)
var dogs = [poppy, rusty, rover]
let beethoven = Dog(breed: "St Bernard", age: 8)
dogs += [beethoven]

当涉及到删除项目时,有两种有趣的方法可以删除最后一项:removeLast()popLast()。它们都删除数组中的最后一项并将其返回给你,但是popLast()返回的是可选值,而removeLast()不是。考虑一下:dogs.removeLast()必须返回Dog结构的一个实例。 如果数组是空的会发生什么?答案是“坏事情”——你的应用会崩溃。

如果你试图删除一项时,你的数组可能是空的,那么使用popLast(),这样你就可以安全地检查返回值:

if let dog = dogs.popLast() {
   // do stuff with `dog`
}

注意:removeLast()有一个称为removeFirst()的对应项,用于删除和返回数组中的初始项。遗憾的是,popLast()没有类似的方法。

空和容量(Emptiness and capacity)

下面是我想展示的另外两个小技巧: isEmptyreserveCapacity()

第一个是isEmpty,如果数组没有添加任何项,则返回true。 这比使用someArray.count == 0更短,更有效,但由于某种原因使用较少。

reserveCapacity()方法允许您告诉 iOS 打算在数组中存储多少项。这并不是一个严格的限制。如果你预留了 10 个容量,你可以继续存储 20 个,如果你想的话——但它允许 iOS 优化对象存储,确保你有足够的空间来容纳你的建议容量。

警告:使用 reserveCapacity()不是一个免费的操作。在后台,Swift 将创建一个包含相同值的新数组,并为你需要的容量留出空间。它不只是扩展现有数组。这样做的原因是该方法保证得到的数组将具有连续存储(即所有项目彼此相邻存储而不是分散在 RAM 中)因此,Swift 会做大量的移动操作。即使你已经调用了reserveCapacity(),这也适用—尝试将这段代码放到一个 Playground 中,自己看看:

import Foundation
let start = CFAbsoluteTimeGetCurrent()
var array = Array(1...1000000)
array.reserveCapacity(1000000)
array.reserveCapacity(1000000)
let end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

当这段代码运行时,你将看到调用reserveCapacity()时两次都会出现严重的暂停。因为reserveCapacity()是一个O(n)复杂度的调用(其中n是数组的count值),所以应该在向数组添加项之前调用它。

连续数组(Contiguous arrays)

Swift 提供了两种主要的数组,但几乎总是只使用一种。首先,让我们分解一下语法:你应该知道这两行代码在功能上是相同的:

let array1 = [Int]()
let array2 = Array<Int>()

第一行是第二行的语法糖。到目前为止,一切都很简单。但是我想向你们介绍一下连续数组容器的重要性,它看起来是这样的:

let array3 = ContiguousArray<Int>(1...1000000)

就是这样。连续数组具有你习惯使用的所有属性和方法—countsort()min()map()等等—但因为所有项都保证是连续存储的,即你可以得到更好的表现。

苹果的官方文档说,当你需要 C 数组的性能时,应该使用连续数组,而当你想要针对 Cocoa 高效转换优化时,应该使用常规数组。文档还说,当与非类类型一起使用时,ArrayContiguousArray的性能是相同的,这意味着在使用类时,你肯定会得到性能上的改进。

原因很简单:Swift 数组可以桥接到NSArray,这是 Objective-C 开发人员使用的数组类型。 由于历史原因,NSArray无法存储值类型,例如整数,除非它们被包装在对象中。 因此,Swift 编译器可以很聪明:如果你创建一个包含值类型的常规 Swift 数组,它就知道你不能尝试将它桥接到NSArray,因此它可以执行额外的优化来提高性能。

也就是说,我发现连续数组无论如何都比数组快,即使是使用Int这样的基本类型。举个简单的例子,下面的代码把1到100万的数字加起来:

let array2 = Array<Int>(1...1000000)
let array3 = ContiguousArray<Int>(1...1000000)
var start = CFAbsoluteTimeGetCurrent()
array2.reduce(0, +)
var end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")
start = CFAbsoluteTimeGetCurrent()
array3.reduce(0, +)
end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

当我运行这段代码时,数组花费 0.25 秒,连续数组花费 0.13 秒。考虑到我们只是循环了超过 100 万个元素,这并不是非常优秀,但如果你想在你的应用程序或游戏中获得额外的性能提升,你肯定应该尝试使用连续数组。

集合(Sets)

了解集合和数组之间的区别 – 并知道哪一个在何时是正确的选择 - 是任何 Swift 开发人员工具箱中的一项重要技能。集合可以被认为是无序数组,不能包含重复元素。如果你多次添加同一个元素,它将只在集合中出现一次。缺少重复项和不跟踪顺序的组合允许集合比数组快得多,因为数据项是根据哈希而不是递增的整数索引存储的。

要将其置于上下文中,检查数组是否包含项,复杂度为O(n),这意味着“它取决于你在数组中有多少元素”。这是因为Array.contains()需要从 0 开始检查每个元素,所以如果有 50 个元素,则需要执行 50 次检查。检查一个集合是否包含项,复杂度为O(1),这意味着“无论你有多少元素,它始终以相同的速度运行”。这是因为集合的工作原理类似于字典:通过创建对象的 hash 生成键,而该键直接指向对象存储的位置。

基础(The basics)

最好的实验方法是使用 Playground ,试着输入这个:

var set1 = Set<Int>([1, 2, 3, 4, 5])

当它运行时,你将在输出窗口中看到 { 5,2,3,1,4 }。就像我说的,集合是无序的,所以你可能会在 Xcode 窗口中看到一些不同的东西。

这将从数组中创建一个新的集合,但是你也可以从范围中创建它们,就像数组一样:

var set2 = Set(1...100)

你还可以单独向它们添加项,尽管方法名为insert()而不是append(),以反映其无序性:

set1.insert(6)
set1.insert(7)

若要检查集合中是否存在项,请使用像闪电一样快的contains()方法:

if set1.contains(3) {
   print("Number 3 is in there!")
}

并使用remove()从集合中删除项:

set1.remove(3)

数组和集合(Arrays and sets)

数组和集合一起使用时工作得很好,所以它们几乎可以互换也就不足为奇了。首先,数组和集合都有接受另一种类型的构造函数,如下所示:

var set1 = Set<Int>([1, 2, 3, 4, 5])
var array1 = Array(set1)
var set2 = Set(array1)

实际上,将数组转换为集合并返回是删除所有重复项的最快方法,而且只需两行代码。

其次,集合的一些方法返回数组而不是集合,因为这样做更有用。例如,集合上的ordered()map()filter()方法返回一个数组。

所以,虽然你可以像这样直接循环集合:

for number in set1 {
   print(number)
}

…你也可以先将集合按合理的顺序排序,如下所示:

for number in set1.sorted() {
   print(number)
}

像数组一样,集合使用removeFirst()方法从集合的前面删除项。 但是它的用途是不同的:因为集合是无序的,你真的不知道第一个项目是什么,所以removeFirst()实际上意味着“给我任何对象,以便我可以处理它。” 巧妙地,集合有一个popFirst()方法,而数组没有——我真希望知道为什么!

集合操作(Set operations)

集合附带了许多方法,允许你以有趣的方式操作它们。例如,你可以创建两个集合的并集,即两个集合的合并,如下所示:

let spaceships1 = Set(["Serenity", "Nostromo", "Enterprise"])
let spaceships2 = Set(["Voyager", "Serenity", "Executor"])
let union = spaceships1.union(spaceships2)

当代码运行时,union将包含 5 个条目,因为重复的 “Serenity” 只出现一次。

另外两个有用的集合操作是intersection()symmetricDifference()。前者返回一个只包含两个集合中存在的元素的新集合,而后者则相反:它只返回两个集合中不存在的元素。代码是这样的:

let intersection = spaceships1.intersection(spaceships2)
let difference = spaceships1.symmetricDifference(spaceships2)

当它运行时,intersection将包含Serenitydifference将包含NostromoEnterpriseVoyagerExecutor

注意:union()intersection()symmetricDifference()都有直接修改集合的替代方法,可以通过向方法前添加form来调用它们,formUnion()formIntersection()formSymmetricDifference()

集合有几个查询方法,根据提供的内容返回truefalse

这些方法是:

  • A.isSubset(of: B): 如果集合 A 的所有项都在集合 B 中,则返回 true
  • A.isSuperset(of: B): 如果集合 B 的所有项都在集合 A 中,则返回 true
  • A.isDisjoint(with: B): 如果集合 B 的所有项都不在集合 A 中,则返回 true
  • A.isStrictSubset(of: B): 如果集合 A 的所有项都在集合 B 中,则返回 true , 但是 AB 不相等
  • A.isStrictSuperset(of: B): 如果集合 B 的所有项都在集合 A 中,则返回 true ,但是 AB 不相等

集合区分子集和严格子集,不同之处在于后者必须排除相同的集合。 也就是说,如果集合 A 中的每个项目也在集合 B 中,则集合 A 是集合 B 的子集。另一方面,如果集合 A 中的每个元素也在集合 B 中,则集合 A 是集合 B 的严格子集,但是集合 B至少包含集合 A 中缺少的一个项。

下面的代码分别演示了它们,我在注释中标记了每个方法的返回值:

let spaceships1 = Set(["Serenity", "Nostromo", "Enterprise"])
let spaceships2 = Set(["Voyager", "Serenity", "StarDestroyer"])
let spaceships3 = Set(["Galactica", "Sulaco", "Minbari"])
let spaceships1and2 = spaceships1.union(spaceships2)
spaceships1.isSubset(of: spaceships1and2) // true
spaceships1.isSubset(of: spaceships1) // true
spaceships1.isSubset(of: spaceships2) // false
spaceships1.isStrictSubset(of: spaceships1and2) // true
spaceships1.isStrictSubset(of: spaceships1) // false
spaceships1and2.isSuperset(of: spaceships2) // true
spaceships1and2.isSuperset(of: spaceships3) // false
spaceships1and2.isStrictSuperset(of: spaceships1) // true
spaceships1.isDisjoint(with: spaceships2) // false

NSCountedSet

Foundation 库有一个专门的集合叫做NSCountedSet,它是一个具有扭曲(twist)的集合: 项仍然只能出现一次,但是如果你尝试多次添加它们,它将跟踪计数,就像它们确实存在一样。这意味着你可以获得非重复集合的所有速度,但是如果允许重复,你还可以计算项目出现的次数。

你可以根据需要从 Swift 数组或集合创建NSCountedSet。在下面的例子中,我创建了一个大型数组(带有重复项),将它全部添加到计数集,然后打印出两个值的计数:

var spaceships = ["Serenity", "Nostromo", "Enterprise"]
spaceships += ["Voyager", "Serenity", "Star Destroyer"]
spaceships += ["Galactica", "Sulaco", "Minbari"]
let countedSet = NSCountedSet(array: spaceships)
print(countedSet.count(for: "Serenity")) // 2
print(countedSet.count(for: "Sulaco")) // 1

正如你所看到的,您可以使用count(for:)来检索一个元素在计数集合中出现的次数(理论上)。你可以使用countedSet.allObjects属性提取所有对象的数组,但要注意:NSCountedSet不支持泛型,因此你需要将其类型转换回[String]

元组(Tuples)

元组类似于简化的匿名结构体:它们是携带不同信息字段的值类型,但不需要正式定义。由于缺少正式的定义,所以很容易创建和丢弃它们,所以当你需要一个函数返回多个值时,通常会使用它们。

在关于模式匹配和析构的章节中,我介绍了元组如何以其他方式使用——它们确实是无处不在的小东西。有多普遍?那么,考虑以下代码:

func doNothing() { }
let result = doNothing()

思考一下: result常量具有什么数据类型?你可能已经猜到了本章的名称,它是一个元组: ()。在后台,SwiftVoid 数据类型(没有显式返回类型的函数的默认值)映射到一个空元组。

现在考虑一下这个:Swift 中的每一种类型——整数、字符串等等——实际上都是自身的一个单元素元组。请看下面的代码:

let int1: (Int) = 1
let int2: Int = (1)

这段代码完全正确:将一个整数赋值给一个单元素元组和将一个单元素元组赋值给一个整数都做了完全相同的事情。正如 Apple 文档中所说,“如果括号中只有一个元素,那么(元组的)类型就是该元素的类型。”它们实际上是一样的,所以你甚至可以这样写:

var singleTuple = (value: 42)
singleTuple = 69

Swift 编译第一行时,它基本上忽略标签,将其变成一个包含整数的单元素元组——而整数又与整数相同。实际上,这意味着你不能给单元素元组添加标签——如果你试图强制一个数据类型,你会得到一个错误:

var thisIsAllowed = (value: 42)
var thisIsNot: (value: Int) = (value: 42)

如果你没有从一个函数返回任何东西,你得到一个元组,如果你从一个函数返回几个值,你得到一个元组,如果你返回一个值,你实际上也得到一个元组。我认为可以肯定地说,不管您是否知道,您已经是一个频繁使用元组的用户了!

现在,我将在下面介绍元组的一些有趣的方面,但是首先你应该知道元组有几个缺点。具体来说,你不能向元组添加方法或让它们实现协议——如果这是你想要做的,那么你要寻找的是结构体。

元组有类型(Tuples have types)

元组很容易被认为是数据的开放垃圾场,但事实并非如此:它们是强类型的,就像 Swift 中的其他所有东西一样。这意味着你不能改变一个元组的类型一旦它被创建-像这样的代码将无法编译:

var singer = ("Taylor", "Swift")
singer = ("Taylor", "Swift", 26)

如果你不给元组的元素命名,你可以使用从 0 开始的数字来访问它们,就像这样:

var singer = ("Taylor", "Swift")
print(singer.0)

如果元组中有元组(这并不少见),则需要使用 0.0 ,诸如此类:

var singer = (first: "Taylor", last: "Swift", address: ("555 Taylor Swift Avenue", "No, this isn't real", "Nashville"))
print(singer.2.2) // Nashville

这是一种内置的行为,但并不意味着推荐使用它。你可以——通常也应该——给你的元素命名,这样你才能更明智地访问它们:

var singer = (first: "Taylor", last: "Swift")
print(singer.last)

这些名称是类型的一部分,所以这样的代码不会编译通过:

var singer = (first: "Taylor", last: "Swift")
singer = (first: "Justin", fish: "Trout")

元组和闭包(Tuples and closures)

不能向元组添加方法,但可以添加闭包。我同意这种区别很好,但它很重要:向元组添加闭包就像添加任何其他值一样,实际上是将代码作为数据类型附加到元组。因为它不是一个方法,声明有一点不同,但这里有一个例子让你开始:

var singer = (first: "Taylor", last: "Swift", sing: { (lyrics: String) in
   print("\(lyrics)")
})

singer.sing("Haters gonna hate")

注意:这些闭包不能访问同级元素,这意味着这样的代码不能工作:

print("My name is \(first): \(lyrics)")

返回多个值(Returning multiple values)

元组通常用于从一个函数返回多个值。事实上,如果这是元组带给我们的唯一东西,那么与其他语言(包括 Objective-C ) 相比,它们仍然是 Swift 的一个重要特性。

下面是一个 Swift 函数在一个元组中返回多个值的例子:

func fetchWeather() -> (type: String, cloudCover: Int, high: Int, low: Int) {
   return ("Sunny", 50, 32, 26)
}
let weather = fetchWeather()
print(weather.type)

当然,你不必指定元素的名称,但是这无疑是一种很好的实践,这样其他开发人员就知道应该期望什么。

如果你更喜欢析构元组返回函数的结果,那么也很容易做到:

let (type, cloud, high, low) = fetchWeather()

相比之下,如果 Swift 没有元组,那么我们将不得不依赖于返回一个数组和按需要进行类型转换,如下所示:

import Foundation
func fetchWeather() -> [Any] {
   return ["Sunny", 50, 32, 26]
}
let weather = fetchWeather()
let weatherType = weather[0] as! String
let weatherCloud = weather[1] as! Int
let weatherHigh = weather[2] as! Int
let weatherLow = weather[3] as! Int

或者更糟的是,使用inout变量,如下所示:

func fetchWeather(type: inout String, cloudCover: inout Int, high: inout Int, low: inout Int) {
    type = "Sunny"
    cloudCover = 50
    high = 32
    low = 26
 }
var weatherType = ""
var weatherCloud = 0
var weatherHigh = 0
var weatherLow = 0
fetchWeather(type: &weatherType, cloudCover: &weatherCloud, high: &weatherHigh, low: &weatherLow)

说真的:如果inout是答案,你可能问错了问题。

可选元组(Optional tuples)

元组可以包含可选元素,也可以有可选元组。这听起来可能相似,但差别很大:可选元素是元组中的单个项,如String?Int? ,而可选元组是整个结构可能存在也可能不存在。

具有可选元素的元组必须存在,但其可选元素可以为nil。可选元组必须填充其所有元素,或者是nil。具有可选元素的可选元组可能存在,也可能不存在,并且其每个可选元素可能存在,也可能不存在。

当处理可选元组时,Swift 不能使用类型推断,因为元组中的每个元素都有自己的类型。所以,你需要明确声明你想要什么,就像这样:

let optionalElements: (String?, String?) = ("Taylor", nil)
let optionalTuple: (String, String)? = ("Taylor", "Swift")
let optionalBoth: (String?, String?)? = (nil, "Swift")

一般来说,可选元素很常见,可选元组就不那么常见了。

比较元组(Comparing tuples)

Swift 允许你比较最多拥有 6 个参数数量的元组,只要它们具有相同的类型。这意味着您可以使用==比较包含最多 6 个项的元组,如果一个元组中的所有 6 个项都匹配第二个元组中的对应项,则返回true

例如,下面的代码会打印“No match”:

let singer = (first: "Taylor", last: "Swift")
let person = (first: "Justin", last: "Bieber")
if singer == person {
   print("Match!")
} else {
   print("No match")
}

但是要注意:元组比较忽略了元素标签,只关注类型,这可能会产生意想不到的结果。例如,下面的代码将打印“Match!”,即使元组标签不同:

let singer = (first: "Taylor", last: "Swift")
let bird = (name: "Taylor", breed: "Swift")
if singer == bird {
   print("Match!")
} else {
   print("No match")
}

别名(Typealias)

你已经看到了元组是多么强大、灵活和有用,但是有时候你可能想要将一些东西形式化。给你一个斯威夫特主题的例子,考虑这两个元组,代表泰勒·斯威夫特的父母:

let father = (first: "Scott", last: "Swift")
let mother = (first: "Andrea", last: "Finlay")

(不,我没有泰勒·斯威夫特的资料,但我可以用维基百科!)

当他们结婚时,安德里亚·芬利变成了安德里亚·斯威夫特,他们成为了夫妻。我们可以写一个简单的函数来表示这个事件:

func marryTaylorsParents(man: (first: String, last: String), woman: (first: String, last: String)) -> (husband: (first: String, last: String), wife: (first: String, last: String)) {
   return (man, (woman.first, man.last))
}

注:我用了 “man” 和 “wife” ,还让妻子改成了她丈夫的姓,因为 Taylor Swift 的父母就是这么做的。很明显,这只是一种婚姻形式,我希望你能理解这是一个简化的例子,而不是一个政治声明。

father元组和mother元组单独看起来足够好,但是marryTaylorsParents()函数看起来相当糟糕。一次又一次地重复(first: String, last: String)会使它很难阅读,也很难更改。

Swift 的解决方案很简单: typealias关键字。这并不是特定于元组的,但在这里它无疑是最有用的:它允许你为类型附加一个替代名称。例如,我们可以创建这样一个 typealias

typealias Name = (first: String, last: String)

使用这个函数,marryTaylorsParents()函数明显变短:

func marryTaylorsParents(man: Name, woman: Name) -> (husband: Name, wife: Name) {
   return (man, (woman.first, man.last))
}

范型(Generics)

尽管泛型在 Swift 中是一个高级主题,但你一直在使用它们:[String]是你使用数组结构存储字符串的一个例子,这是泛型的一个例子。事实上,使用泛型很简单,但是创建泛型需要一点时间来适应。在本章中,我将演示如何(以及为什么!)创建自己的泛型,从函数开始,然后是结构体,最后是包装 Foundation类型。

让我们从一个简单的问题开始,这个问题演示了泛型是什么以及它们为什么重要:我们将创建一个非常简单的泛型函数。

设想一个函数,它被设计用来打印关于字符串的一些调试信息。它可能是这样的:

func inspectString(_ value: String) {
   print("Received String with the value \(value)")
}
inspectString("Haters gonna hate")

现在让我们创建相同的函数来打印关于整数的信息:

func inspectInt(_ value: Int) {
   print("Received Int with the value \(value)")
}
inspectInt(42)

现在让我们创建打印关于 Double 类型的信息的相同函数。实际上……我们不需要。这显然是非常枯燥的代码,我们需要将其扩展到浮点数、布尔值、数组、字典等等。有一种更智能的解决方案称为泛型编程,它允许我们编写处理稍后指定类型的函数。Swift 中的通用代码使用尖括号<>,所以它非常明显!

要创建inspectString()函数的泛型形式,可以这样写:

func inspect<SomeType>(_ value: SomeType) { }

注意SomeType的用法:在函数名后面的尖括号中,用于描述value参数。尖括号里的第一个是最重要的,因为它定义了你的占位符数据类型:inspect<SomeType>()意味着“一个名为inspect()的函数,可以使用任何类型的数据类型,但是无论使用的数据类型是什么,我想把它称为SomeType。因此,参数value: SomeType现在应该更有意义了:SomeType将被用于调用函数的任何数据类型替换。

稍后你将看到,占位符数据类型也用于返回值。但是首先,这里是inspect()函数的最终版本,它输出正确的信息,无论向它抛出什么数据:

func inspect<T>(_ value: T) {
   print("Received \(type(of: value)) with the value \(value)")
}

inspect("Haters gonna hate")
inspect(56)

我使用了type(of:)函数,以便 Swift正确地输出 “String”、“Int” 等。注意,我还使用了T而不是某种类型,这是一种常见的编码约定:第一个占位符数据类型名为T,第二个U和第三个V,以此类推。在实践中,我发现这个约定没有帮助,也不清楚,所以尽管我将在这里使用它,只是因为你必须习惯它。

现在,你可能想知道泛型给这个函数带来了什么好处——难道它就不能为它的参数类型使用泛型吗?在这种情况下可以,因为占位符只使用一次,所以这在功能上是相同的:

func inspect(_ value: Any) {
   print("Received \(type(of: value)) with the value \(value)")
}

但是,如果我们希望函数接受相同类型的两个参数,那么Any和占位符之间的区别就会变得更加明显。例如:

func inspect<T>(_ value1: T, _ value2: T) {
   print("1. Received \(type(of: value1)) with the value \(value1)")
   print("2. Received \(type(of: value2)) with the value \(value2)") 
}

现在接受T类型的两个参数,这是占位符数据类型。同样,我们不知道这将是什么,这就是为什么我们给它一个抽象的名称,如 “T ”,而不是一个特定的数据类型,如IntString。然而,这两个参数的类型都是T,这意味着无论最终是什么类型,它们都必须是相同的类型。所以,这个代码是合法的:

inspect(42, 42)

但这是行不通的,因为它混合了数据类型:

inspect(42, "Dolphin")

如果我们对数据类型使用了Any参数,那么 Swift 就不能确保两个参数都是相同的类型——一个可以是Int,另一个可以是String。所以,这段代码将是正确的:

func inspect(_ value1: Any, _ value2: Any) {
   print("1. Received \(type(of: value1)) with the value \(value1)")
   print("2. Received \(type(of: value2)) with the value \(value2)")
}
inspect(42, "Dolphin")

范型限制(Limiting generics)

你常常希望限制泛型,以便它们只能对类似类型的数据进行操作,而 Swift 使这一点变得既简单又容易。下一个函数将对任意两个整数进行平方,不管它们是IntUIntInt64,等等:

func square<T: Integer>(_ value: T) -> T {
   return value * value
}

注意,我为返回值添加了一个占位符数据类型。在本例中,它意味着函数将返回与它接受的数据类型相同的值。

扩展square()以支持其他类型的数字(如双精度和浮点数)比较困难,因为没有覆盖所有内置数字类型的协议。我们来创建一个:

protocol Numeric {
   static func *(lhs: Self, rhs: Self) -> Self
}

它不包含任何代码,它只定义了一个名为Numeric的协议,并声明任何符合该协议的东西都必须能够自我相乘。我们想把这个协议应用到 FloatDoubleInt ,所以在协议下面加上这三行:

extension Float: Numeric {}
extension Double: Numeric {}
extension Int: Numeric {}

有了这个新协议,你可以满足任何你想要的:

func square<T: Numeric>(_ value: T) -> T {
   return value * value
}
square(42)
square(42.556)

创建泛型数据类型(Creating a generic data type)

既然你已经掌握了泛型函数,让我们进一步了解完全泛型数据类型:我们将创建一个泛型结构。在创建泛型数据类型时,需要将占位符数据类型声明为结构名称的一部分,然后可以根据需要在每个属性和方法中使用该占位符。

我们将要构建的结构名为 deque,这是一种常见的抽象数据类型,意思是“双端队列”。常规队列是将东西添加到队列末尾,然后从队列前端删除它们的队列。deque 是一个队列,你可以将内容添加到开头或结尾,也可以从开头或结尾删除内容。我选择在这里使用deque,因为重用 Swift 的内置数组非常简单——这里的关键是概念,而不是实现!

为了创建 deque 结构,我们将给它一个存储数组属性,它本身是通用的,因为它需要保存 deque 存储的任何数据类型。我们还将添加四个方法: pushBack()pushFront() 将接受类型为T的参数并将其添加到正确的位置,而popBack()popFront()将返回一个T?(占位符可选数据类型),如果存在值,它将从后面或前面返回值。

只有一个很小的复杂性,那就是数组没有返回T?popFirst()方法,因此我们需要添加一些额外的代码,以便在数组为空时运行。这是代码:

struct deque<T> {
   var array = [T]()
   mutating func pushBack(_ obj: T) {
      array.append(obj)
    }
   mutating func pushFront(_ obj: T) {
      array.insert(obj, at: 0)
   }
   mutating func popBack() -> T? {
      return array.popLast()
   }
   mutating func popFront() -> T? {
      if array.isEmpty {
         return nil
      } else {
         return array.removeFirst()
      }
  } 
}

有了这个结构,我们可以立即开始使用它:

var testDeque = deque<Int>()
testDeque.pushBack(5)
testDeque.pushFront(2)
testDeque.pushFront(1)
testDeque.popBack()

使用Cocoa类型(Working with Cocoa types)

Cocoa 数据类型—— NSArrayNSDictionary 等等——从 Swift 最早的版本开始就可以使用了,但是它们很难使用,因为 Objective-C 对泛型的支持是最近的,也是有限的。

NSCountedSet 是我最喜欢的基础类型之一,它根本不支持泛型。这意味着你失去了 Swift 编译器赋予你的自动类型安全,而这又让你离 JavaScript 程序员更近了一步——你不想这样吧? 当然不。

幸运的是,我将向你演示如何通过围绕 NSCountedSet 创建泛型包装来创建自己的泛型数据类型。

这就像一个常规集合,每个条目只存储一次,但是它还有一个额外的好处,那“你添加了 20 次数字 5 ”,尽管实际上它只在那里出现过一次。

这个的基本代码并不难,尽管你需要导入 Foundation 来访问NSCountedSet :

import Foundation
struct CustomCountedSet<T: Any> {

   let internalSet = NSCountedSet()

   mutating func add(_ obj: T) {
      internalSet.add(obj)
   }
   mutating func remove(_ obj: T) {
      internalSet.remove(obj)
   }
   func count(for obj: T) -> Int {
      return internalSet.count(for: obj)
   }
}

有了新的数据类型,你可以这样使用它:

var countedSet = CustomCountedSet<String>()
countedSet.add("Hello")
countedSet.add("Hello")
countedSet.count(for: "Hello")
var countedSet2 = CustomCountedSet<Int>()
countedSet2.add(5)
countedSet2.count(for: 5)

我们的结构体所做的就是包装NSCountedSet使其类型安全,但这总是一个受欢迎的改进。考虑到苹果在 Swift 3 中的发展方向,如果他们在未来将NSCountedSet重新实现为一个通用的基于结构体的CountedSet,我不会感到惊讶——让我们拭目以待!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容