set
除了Dictionary
之外,Set
是Swift标准库中,另一个主要的无序集合类型,包含一组不重复的值。可以把Set
理解为一个只包含key而没有value的集合。本质上,Set
也是一个哈希表,因此它有着和Dictionary
诸多类似的地方。
[TOC]
初始化Set
例如,我们要创建一个包含所有元音的Set
:
var vowel: Set<Character> = ["a","o","e","i","u"]
这里,由于初始化Set
和Array
的方式是一样的,因此,我们要定义一个Set
对象时,必须明确使用type annotation。Type inference会把这样的定义方式推导为一个Array
Set的常用属性和方法
作为一个集合类型,Set
提供了和Array
以及Dictionary
一样的常用属性:
vowel.count //5
vowel.isEmpty // false
以及常用的编辑方法:
vowel.contains("a") // true
vowel.remove("a") // a
vowel.insert("a") // (true, "a")
vowel.removeAll() // Set([])
在上面的代码里:
-
contains
判断它的参数是否在Set
中,并返回一个bool
值表示判断结果; -
remove
在Set
中删除参数制定的元素,如果元素存在就成功删除并返回删除的元素,否则就返回nil
-
insert
在Set
中插入参数指定的内容,如果插入的内容已存在,会返回一个值为(false,插入值)
的tuple,否则,就返回(true,插入值)
-
removeAll
则删除所有的Set
中的元素,留下一个空的集合
遍历Set
和Dictionary
类似,我们有三种方式来遍历Set
。首先,最普通的for
循环:
for character in vowel {
print(character)
}
// e,a,i,o,u
其次,是集合自身的forEach
方法:
vowel.forEach { print($0) }
// e,a,i,o,u
通过注释中的方法可以看到,当遍历一个Set
时,访问元素的顺序,并不是我们定义Set
时的顺序。当我们遍历Set
时,遍历的顺序,都会根据当前Set
包含的值而有所不同。如果你希望按照某种固定
的排序方式访问Set
中的元素,就要使用它的sorted
方法:
for character in vowel.sorted() {
print(character)
}
// a,e,i,o,u
常用的Set方法
在理解了Set
最基本的操作之后,我们来看一些更实际的Set
用法,它当然不仅仅是和Dictionary
存储值得形式不同那么简单。其中一个要提到的就是,作为表示一组值的无序集合,Set
支持各种常用的代数运算方法。
Set的代数运算
为了介绍各种运算方法,先定义两个Set
:
var setA: Set = [1,2,3,4,5,6]
var setB: Set = [4,5,6,7,8,9]
然后,我们就可以对setA
和setB
进行下面的运算:
//{5,6,4}
let interSectAB: Set = setA.intersection(setB)
//{9,7,2,3,1,8}
let symmetricDiffAB: Set = setA.symmetricDifference(setB)
//{2,4,9,5,6,7,3,1,8}
let unionAB = setA.union(setB)
//{2,3,1}
let aSubstractB: Set = setA.subtracting(setB)
除此之外,上面这些API还有一个"可修改Set自身"的版本,而命名方式,就是在这些API的名称前面,加上form,例如:
setA.formIntersetion(setB) // {5,6,4}
这样setA
的值,就被修改成了取交集之后的值。
把Set用作内部支持类型
很多时候,除了Set
作为一个集合类型返回给用户之外,我们还可以把它作为函数的内部支持类型来使用。例如借助Set
不能包含重复元素的特性,为任意一个序列类型去重。我们给Sequence
添加下面的拓展:
extension Sequence where Interator.Element: Hashable {
func unique() -> [Interator.Element] {
var result: Set<Interator.Element> = []
return filter {
if result.contains($0) {
return false
} else {
result.insert($0)
return true
}
}
}
}
在这个例子里,我们使用了Set
不能包含重复元素的特性,用result
保存了所有已经筛选的元素,如果遇到重复的元素,就跳过,否则,就把它添加到result
里用于下一次筛选。这样,我们就可以使用unique
来去重了:
[1,1,2,2,3,3,4,4].unique() // [1,2,3,4]
IndexSet 和 CharacterSet
在Swift标准库中,Set
是唯一一个支持SetAlgebra
protocol类型。但是,在Foundation里,却还有两个额外的类型:IndexSet
和CharacterSet
。
其中,IndexSet
和Set<Int>
是非常类似的,例如:
let oneToSix: IndexSet = [1,2,3,4,5, 6]
但当我们要表达一连串正整数时,尤其是这个证书范围比较大的时候,使用IndexSet
要比使用Set<Int>
更加经济一些。因为Set<Int>
会保存这个范围内里的每一个整数,而IndexSet
则会使用类似1...6
这样的形式保存一个范围。因为,要表达的范围越大,是用IndexSet
就会越经济。并且,由于IndexSet
也完全实现了SetAlgebra
和Collection
这两个protocol,因此,它的用法和Set
几乎是相同的。
另一个类Set
类型,就是CharacterSet
,它主要表示某一类字符的集合,通常,我们用这个类型来判断字符串中是否包含特定类型的字符,例如:
//String
let hw = "Hellow world"
// CharacterSet
let numbers = CharacterSet(CharactersIn: "123456789")
let letters = CharacterSet.letters
//Actions
hw.rangeOfCharacter(from: numbers) //nil
hw.rangeOfCharacter(from: letters) //
定义好集合以后,我们就可以使用rangeOfCharacter(from:)
来判断String
对象是否包含特定的字符了。如果包含,rangeOfCharacter会返回一个Range
对象,否则,就返回nil
理解Range 和 Collection的关系
在之前Swift操作符的内容里,我们曾经提到了两个和范围有关的操作符:
-
1..<5
表示的半开半闭区间[1,5) -
1...5
表示闭区间[1,5]
Countable range
实际上,这两个区间操作符在Swift中,是通过两个struct
来实现的,叫做CountableRange
和CountableClosedRange
,他们都遵从Comparable
和Strideable
protocol。
其中:
- 只有半开半闭区间可以表达“空区间”的概念,例如:
5..<5
,而5...5
则包含一个元素5 - 只有闭区间可以包含区间位置的最大值,例如:
1 ... Int.max
,而1 ..< Int.max
则表示1 ... (Int.max - 1)
之所以这两个range操作符背后的类型都用Countable开头,意思是指他们是可以被迭代的,也就是可以从头到尾计算范围的值。例如:
for i in 1 ... 5 {
print(i)
}
//12345
Uncountable range
既然有CountableRange
,就不难联想到,是否有uncountable的版本呢?实际上,的确是存在的。只是,他们仅能表示一个区间,但我们不能遍历它。例如:
//The following code will FAIL
for i in 1.0 ... 5.0 {
print(i)
}
这时,Swift编译器就会提示我们ClosedRange<Double>
没有遵从Sequence
protocol。于是,uncountable的版本就出现了,就是这个ClosedRange
。当然还有一个uncountable的半开半闭区间的类型,叫做Range
。
为了遍历这样的浮点数区间,我们只能用stride(from: to:by:)和stride(from:though:by:)来指定起始、结束范围以及步进值。前者,类似于半开半闭区间,后者类似于闭区间:
for i in stride(from: 1.0, to: 5.0, by: 1.0) {
print(i)
}
// 1.0 2.0 3.0 4.0
for i in stride(from: 1.0,though: 5.0,by: 1.0) {
print(i)
}
// 1.0 2.0 3.0 4.0
Conclusion
于是,按照一个区间可以表示的范围,以及它是否可以被遍历,实际上Swift中一共有四种不同的区间类型:
/*
* Half-open | Closed range
* +------------+----------------+----------------------
* | Comparable | Range | ClosedRange |
* +------------+----------------+----------------------
* | Strideable | CountableRange | CountableClosedRange|
* +------------+----------------+----------------------
*/
相信在后续的Swift版本里,还会对这一系列区间类型进行改进和优化。但至少现在,它还是会给我们带来一些麻烦。对于一个接受Range<T>
类型参数的函数来说,我们甚至无法传递一个ClosedRange<T>
类型的对象。
为什么会这样呢?其实,和(Closed)Range无法通过for循环遍历一样,我们无从根据一个CloseRange<T>
的结束位置,找到闭区间结束位置的上一个位置,因此,这种转换是无法完成的。如果从Swift语言的角度来说,就是,(Closed)Range
仅仅实现了Comparable
protocol,而没有实现Strideable
protocol。
因此,面对这类情况,我们只能自己根据ClosedRange<T>
计算需要的范围,再重新创建正确的Range<T>
对象。