第六章——函数(计算属性和下标脚本)

437 阅读6分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

除了普通的方法,还有两种特殊的方法:计算属性和下标脚本。计算属性和普通的属性看上去类似,但它不会占用内存空间来存储某些内容,而是每次被访问时进行动态的计算。下标脚本本质上也是方法,只是它的定义和调用与普通的方法不同。举个例子,下面是一个表示文件的结构体,它有一个方法用于计算文件大小:

struct File {
let path: String  // 文件路径

func computeSize() -> Int {
var size = 0
// 这里有一些复杂的I/O和计算
return size
}
}

computeSize可能是一个非常耗时的方法,因为它有可能会递归调用子目录的方法。这时候我们可以考虑对计算结果做一个缓存,也就是用一个私有属性来存储computeSize的返回值。因为要在结构体的方法中修改结构体的属性,所以需要把方法标记为mutating

private var cachedSize: Int?

mutating func computeSize() -> Int {
guard cachedSize == nil else { return cachedSize! }
// 如果缓存不存在,那么再运行下面的代码
var size = 0
// 这里有一些复杂的I/O和计算
cachedSize = size  // 添加到缓存中
return size
}

这种延迟加载+数据缓存的模式在Swift中非常常见,遇到这种问题时,更好的解决办法是使用lazy关键字:

lazy var size: Int? = {
var size: Int? = 0
// 这里有一些复杂的I/O和计算
return size
}()

标记为lazy的属性是可变的,所以它必须被声明成var。它被定义成一个闭包,当属性第一次被访问时,会立即执行这个闭包(这就是为什么闭包的结尾有一对括号),闭包的返回值会赋值给这个属性。

不过,上面两种方法都有一个缺点:结构体变量必须定义成var。因为它的内部有mutating方法或lazy成员。

如果我们不希望做缓存,又想通过属性而不是函数来获取文件大小,我们可以把这个属性定义为计算属性,这样每次它被访问时,值都会被重新计算:

var size: Int? {
var size: Int? = 0
// 这里有一些复杂的I/O和计算
return size
}

这里我们实现的是计算属性的get方法,如果还想实现它的set方法,则需要把它与get方法分开实现:

var data: NSData? {
get {
return nil
}
set {
data = newValue
}
}

不管是普通属性,还是计算属性,我们都可以实现它的didSetwillSet回调函数,这两个回调函数分别在set方法调用前、后被调用,一个常用的场景是在IBOutlet被连接后进行一些初始化设置:

class ViewController: UIViewController {
@IBOutlet weak var label: UILabel? {
didSet {
label?.textColor = .blackColor()
}
}
}

重载下标脚本

你一定已经见过下标脚本的使用:字典通过下标脚本查找元素。下标脚本也是函数,只不过是语法比较怪异。下标脚本可以是只读的(使用get),也可以是可读可写的(使用getset)。和普通函数一样,下标脚本也可以重载,它可以有不同类型的参数,这一点我们在使用数组切片时也曾见过:

let fibs = [0,1,1,2,3,5]
let first = fibs[0] // 下标脚本的参数是Int类型
let nums = fibs[1..<3]  // 下标脚本的参数是Range<Int>类型
print(nums)  // 结果是: [1,1]

在Swift中,Range类型表示一段有界的区间:每个Range变量都有开始位置和结束位置。我们还可以拓展一下集合类型,使它的下标脚本支持开始位置和结束位置只有一个确定的半有界区间,首先定义两个新的结构体:

struct RangeStart<I: ForwardIndexType> {
let start: I
}

struct RangeEnd<I: ForwardIndexType> {
let end: I
}

我们可以定义两个便捷运算符来表示半确定区间,它们是单目运算符,一个是前缀运算符,一个是后缀运算符。这样一来,只知道开始位置的半确定区间可以表示为x..<,只知道结束位置的半确定区间可以表示为..<x

postfix operator ..< {}
postfix func ..<<I: ForwardIndexType>(lhs: I) -> RangeStart<I> {
return RangeStart(start: lhs)
}

prefix operator ..< {}
prefix func ..<<I: ForwardIndexType>(rhs: I) -> RangeEnd<I> {
return RangeEnd(end: rhs)
}

做好这些准备工作之后,就可以重载集合类型的下标脚本了:

extension CollectionType {
subscript(r: RangeStart<Self.Index>) -> SubSequence {
return self[r.start..<endIndex]
}

subscript(r: RangeEnd<Self.Index>) -> SubSequence {
return self[startIndex..<r.end]
}
}

我们来测试一下:

let fibs = [0,1,1,2,3,5]
print(fibs[2..<])  // 输出结果:[1, 2, 3, 5]
print(fibs[..<4])  // 输出结果:[0, 1, 1, 2]

除此以外,半确定区间还可以用于实现一个搜索函数,用于进行模式匹配,它可以查找某个子集在集合中第一次出现的位置:

extension CollectionType
where Generator.Element: Equatable, SubSequence.Generator.Element == Generator.Element {
func search<S: SequenceType where S.Generator.Element == Generator.Element>
(pattern: S) -> Self.Index? {
return self.indices.indexOf {
//这里的每一个$0,都是集合的一个下标,所以self[$0..<]是一个逐渐缩短的字串
self[$0..<].startsWith(pattern)  //如果返回true说明pattern第一次出现的位置就是$0
}
}
}

我们通过search方法查找字符串中某个字串第一次出现的位置,基于这个位置获得从字符串的开端到这个位置之间所有的字符:

let greeting = "Hello, world"
if let index = greeting.characters.search(", ".characters) {
print(String(greeting.characters[..<index]))
}

这个方法可以理解为低效、简单版的KMP算法。

下标脚本进阶

下标脚本不仅可以重载(接收不同类型的参数),还可以接收多个参数(准确的说这也是重载),这一点与函数完全相同。我们可以拓展字典类型,使它的下标脚本具有默认返回值,也就是如果没有找到key,则返回这个默认值。在下标脚本的set方法中则无需这个默认值,因为newValue不可能是可选类型的:

extension Dictionary {
subscript(key: Key, or or: Value) -> Value {
get {
return self[key] ?? or  // 使用空合运算符,如果self[key]为nil则返回or
}

set {
self[key] = newValue
}
}
}

这样的下标脚本可以简化不少代码。比如我们实现一个函数,统计数组中所有元素出现的频率,统计结果用字典表示,键就是元素,值是元素出现的次数:

extension SequenceType where Generator.Element: Hashable {
func frequencies() -> [Generator.Element:Int] {
var result: [Generator.Element:Int] = [:]
for x in self {
result[x, or: 0]++
}
return result
}
}

这段代码最核心、也是最巧妙的地方在于result[x, or: 0]++这句,如果你认为当result[x, or: 0] = 0时,这句话等价于0++,那么你会发现这完全无法在字典中新增一条键值对。事实上,它的实现如下:

result[x, or: 0] = result[x, or: 0] + 1

也就是说我们会先后调用到下标脚本的setget。你可以通过断点自己体会一番。

测试一下:

var array = [100,200,100,300,200,200]
print(array.frequencies())   // 输出结果:[100: 2, 200: 3, 300: 1]