Swift学习笔记--Set

set

除了Dictionary之外,Set是Swift标准库中,另一个主要的无序集合类型,包含一组不重复的值。可以把Set理解为一个只包含key而没有value的集合。本质上,Set也是一个哈希表,因此它有着和Dictionary诸多类似的地方。

[TOC]

初始化Set

例如,我们要创建一个包含所有元音的Set:

var vowel: Set = ["a","o","e","i","u"]

这里,由于初始化SetArray的方式是一样的,因此,我们要定义一个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值表示判断结果;
  • removeSet中删除参数制定的元素,如果元素存在就成功删除并返回删除的元素,否则就返回nil
  • insertSet中插入参数指定的内容,如果插入的内容已存在,会返回一个值为(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]

然后,我们就可以对setAsetB进行下面的运算:

//{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 = []
        
        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里,却还有两个额外的类型:IndexSetCharacterSet
其中,IndexSetSet是非常类似的,例如:

let oneToSix: IndexSet = [1,2,3,4,5, 6]

但当我们要表达一连串正整数时,尤其是这个证书范围比较大的时候,使用IndexSet要比使用Set更加经济一些。因为Set会保存这个范围内里的每一个整数,而IndexSet则会使用类似1...6这样的形式保存一个范围。因为,要表达的范围越大,是用IndexSet就会越经济。并且,由于IndexSet也完全实现了SetAlgebraCollection这两个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来实现的,叫做CountableRangeCountableClosedRange,他们都遵从ComparableStrideableprotocol。
其中:

  • 只有半开半闭区间可以表达“空区间”的概念,例如: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没有遵从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类型参数的函数来说,我们甚至无法传递一个ClosedRange类型的对象。

为什么会这样呢?其实,和(Closed)Range无法通过for循环遍历一样,我们无从根据一个CloseRange的结束位置,找到闭区间结束位置的上一个位置,因此,这种转换是无法完成的。如果从Swift语言的角度来说,就是,(Closed)Range仅仅实现了Comparable protocol,而没有实现Strideable protocol。

因此,面对这类情况,我们只能自己根据ClosedRange计算需要的范围,再重新创建正确的Range对象。

你可能感兴趣的:(Swift学习笔记--Set)