Collection, Optitonal

Built-In Collections

Arrays

var来保证可变性:

var mutableFibs = [0, 1, 1, 2, 3, 5]

mutableFibs.append(8)
mutableFibs.append(contentsOf: [13, 21])
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]

数组和标准库中的所有集合类型⼀样,是具有值语义的。当你创建⼀个新的数组变量并且把⼀个已经存在的数组赋值给它的时候,这个数组的内容会被复制。

var x = [1,2,3]
var y = x
y.append(4)
y // [1, 2, 3, 4]
x // [1, 2, 3]

对⽐⼀下 Foundation 框架中 NSArray 在可变特性上的处理⽅法。NSArray 中没有更改⽅法,想要更改⼀个数组,你必须使⽤ NSMutableArray。但是,就算你拥有的是⼀个不可变的NSArry,但是它的引⽤特性并不能保证这个数组不会被改变:

let a = NSMutableArray(array: [1,2,3])
// 我们不想让 b 发生改变
let b: NSArray = a
// 但是事实上它依然能够被 a 影响并改变
a.insert(4, at: 3)
b // ( 1, 2, 3, 4 )
正确的⽅式是在赋值时,先⼿动进⾏复制:
let c = NSMutableArray(array: [1,2,3])
// 我们不想让 d 发生改变
let d = c.copy() as! NSArray
c.insert(4, at: 3)
d // ( 1, 2, 3 )

而Swift中的数组则没有这个问题,因为它是值语义,所以它是为"复制"设计的, 但是如此多的复制并不会带来性能上的影响, 因为 Swift 标准库中的所有集合类型都使⽤了“写时复制” 这⼀技术,它能够保证只在必要的时候对数据进⾏复制。在我们的例⼦中,直到y.append 被调⽤的之前,x 和 y 都将共享内部的存储。

Array Indexing

  • 你几乎很少需要手动操作数组的索引, 而应使用遍历, 或者dropLast(5)之类的操作.
  • 你的索引操作如果越界, 会抛出异常, 而不是返回nil或者Optional
    • 相对地, Dictionary的索引操作会返回nil
    • 因为字典的键会来自任何地方, 你不可控, 而索引操作你已经知道了数组容量, 这个控制应该由开发者来负责

高阶函数

map, reduce之类的就不说了, 这里自行封装一个倒序找第一个满足条件的函数:

extension Sequence {
    func last(where predicate: (Iterator.Element) -> Bool) -> Iterator.Element? {
        for element in reversed() where predicate(element) {
            return element
        }
        return nil
    }
}
  • 不推荐在map等函数来做一些主义以外的操作, 那样你只是把它当作一个迭代器在使用, 不如直接用forEach
  • forEachreturn并不会终止循环, 只是退出当前迭代, 所以一定不要用在需要提前退出的场合, 而是用在每个元素都要遍历到的场合

不要这么做:

bigArray.filter { someCondition }.count > 0

filter 会创建⼀个全新的数组,并且会对数组中的每个元素都进⾏操作。而上述代码的意图是判断"有没有", 那么找到一个就可以退出遍历了:

bigArray.contains { someCondition }

reduce的内部实现参考下:

extension Array {
    func reduce<Result>(_ initialResult: Result,
_ nextPartialResult: (Result, Element) -> Result) -> Result {
        var result = initialResult
        for x in self {
            result = nextPartialResult(result, x)
        }
        return result
}}

其实就是要求你送入一个函数, 告诉他怎么处理旧值和新值, 他帮你迭代.

  • flatMap用于将不同层级里的变形结果平铺到同一个层级里, 比如你遍历每个元素产生的结果是一个数组, 那么map出来的结果肯定是一个二维数组, 而flatMap会帮你把二维数组平铺到一维数组里

看一下实现, 就是创建一个一维数组, 不断添加元素就是了:

extension Array {
    func flatMap<T>(_ transform: (Element) -> [T]) -> [T] {
    var result: [T] = []
    for x in self {
        result.append(contentsOf: transform(x))
    }
    return result
}}

Array Slices

let slice = fibs[1..<fibs.endIndex]
slice // [1, 1, 2, 3, 5]
type(of: slice) // ArraySlice<Int>
// 构建回数组:
Array(bs[1..<bs.endIndex]) // [1, 1, 2, 3, 5]
  • 切⽚类型只是数组的⼀种表⽰⽅式,它背后的数据仍然是原来的数组
  • 这意味着原来的数组并不需要被复制。

Dictionaries

  • Dictionary lookup always returns an optional value
  • dictionaries defined using let are immutable, but you can still change the values inside(其实就是键固定了)
  • 设置nil或移除"值", 都会把这个键删除
  • removeValue(forKey:)会顺便把删除的键值对中的""也返回
  • Dictionary的键值对顺序是不确定的
  • merge(_:uniquingKeyWith:)方法用于合并字典, 如果有重复的键, 会用第二个参数来决定保留哪个值($0是旧的值, $1是新值)

Merge

var settings = defaultSettings
let overriddenSettings: [String:Setting] = ["Name": .text("Jane's iPhone")]
settings.merge(overriddenSettings, uniquingKeysWith: { $1 })
settings
// ["Name": Setting.text("Jane\'s iPhone"), "Airplane Mode": Setting.bool(false)]

字典里存的值如果有多种类型, 这个例子把这些值封到枚举的关联数据里, 这样这个字典的键值就成了单一类型的了:

enum Setting {
    case text(String)
    case int(Int)
    case bool(Bool)
}
let defaultSettings: [String: Setting] = [ // 键值成了Setting类型
    "Airplane Mode": .bool(true),
    "Name": .text("My iPhone"),
]

Iterator.Element

字典的Iterator.Element(key, value)的元组, 所以如果对字典进行扩展, 接受的参数只需要验证这一个约束即可, 比如扩展一个merge功能(新版Swift已支持)

extension Dictionary {
    mutating func merge<S>(_ other: S) where S: Sequence, S.Iterator.Element == (key: Key, value: Value) {
        for (k, v) in other {
        self[k] = v
}}}

知道是个元组的集合, 我们也可以自己做一个这样的Iterator:

(1..<5).map { (key: "Alarm \($0)", value: false) }

字典其实是哈希表。字典通过键的 hashValue 来为每个键指定⼀个位置,以及它所对应的存 储。这也就是 Dictionary 要求它的 Key 类型需要遵守 Hashable 协议的原因。标准库中所有的基本数据类型都是遵守 Hashable 协议的,它们包括字符串,整数,浮点数以及布尔值。不带有关联值的枚举类型也会⾃动遵守 Hashable。

Hashable

  • Dictionaries are hash tables
  • All the basic data types in the standard library — including strings, integers, floating-point numbers, and Boolean values — already do.
  • Additionally, many other types — like arrays, sets, and optionals — automatically become hashable if their elements are hashable.
    • Struct的成员如果都是Hashable的, 那么这个Struct也是Hashable
    • Enum没有associated values, 那么它也是Hashable
    • Enum的associated values如果都是Hashable的, 那么这个Enum也是Hashable
    • 这个叫automatic Hashable synthesis
  • 系统根据随机的种子来进行hash, 使得每次生成的hash不会一致, 而内部又是根据hash进行的排序, 所以得到的排序也是随机的

如果你想要将⾃定义的类型⽤作字典的键:

  1. 那么你必须⼿动为你的类型添加 Hashable 并满⾜它,这需要你实现 hashValue 属性。
  2. 另外,因为 Hashable 本⾝是对 Equatable 的扩展,因此你还需要为你的类型重载 == 运算符。
    • 也意味着如果两个对象相等, 它们的hash一定是一样的
    • 反之不需要成立, 因为存在碰撞的情况
  3. 但是你改变了这个对象的某个属性, 那么它的hashValue也会改变, 这会导致字典里找不到这个对象, 以及新值存在了新的位置

Sets

  • 你可以将集合想像为⼀个只存储了键⽽没有存储值的字典。
  • Dictionary ⼀样,Set 也是通过哈希表实现的
  • Set 是标准库中唯⼀实现了 SetAlgebra 的类型,
    • 即支持求交集(intersection), 并集(formUnion), 补集(subtracting)等
    • 但是这个协议在 Foundation 中还被另外两个很有意思的类型实现了:那就是 IndexSetCharacterSet

IndexSet V.S. Set

如果你要存10000个状态中被选中的状态, 那么Set<Int>需要存储被选中的个数那么多的元素, 而IndexSet却能用Range来存储连续选中项, 比如0到500, 显然能节省很多内存(显然SetAlgebra起了作用)

带范围操作的例子:

var indices = IndexSet()
indices.insert(integersIn: 1..<5)
indices.insert(integersIn: 11..<15)
let evenIndices = indices.filter { $0 % 2 == 0 } // [2, 4, 12, 14]

保留顺序的去重(如果不需要保留顺序, 直接把数组转为Set最快):

extension Sequence where Iterator.Element: Hashable {
    func unique() -> [Iterator.Element] {
        var seen: Set<Iterator.Element> = []
        return filter {
            if seen.contains($0) {
                return false
            } else {
                seen.insert($0)
        return true
}}}}
[1,2,3,12,1,3,4,5,6,4,6].unique() // [1, 2, 3, 12, 4, 5, 6]

Ranges

  • RangeClosedRange 都实现了 Sequence 协议, 所以可以用for循环来遍历
  • 前提是元素符合Strideable协议, 实现这个协议能以整数步长移动到下一个元素(整数和指针都符合, 浮点数不行)
  • 就是从lowerBound开始然后不断地调用advanced(by: 1)方法, 直到upperBound为止
    • 所以下面要介绍的PartialRange的(upTothrough)就不能迭代, 因为缺少lowerBound
let fromZero = 0... // Partial Ranges
// 即 let fromZero: PartialRangeFrom<Int> = 0...
let upToZ = ..<Character("z")
// 即 let upToZ: PartialRangeUpTo<Character> = ..<Character("z")
let through10 = ...10
// 即 let through10: PartialRangeThrough<Int> = ...10

所以through是闭区间, up to是开区间

singleDigitNumbers.contains(9) // true
lowercaseLetters.overlaps("c"..<"f") // true

Optionals

一些表示特殊值的标识, 比如nil, EOF, NSNotFound等, 这些值被称为Sentinel Values:

  • EOF is just a #define for -1.
  • [someString rangeOfString:@"Swift"].location != NSNotFound如果someString为空, 那么rangeOfString会返回nil, OC下可以对nil发消息, 所以location会返回0, 会使得判断条件成立 写代码的时候需要自行对这些值进行判断, 如果不是这些值, 就代表正常的业务数据.
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

// 对于可能空值返回:
return .none // or return nil (因为实现了ExpressibleByNilLiteral)
return .some(42)

Swift用枚举代替Sentinel Values(就是可能返空值, 约定无效值等特殊标识的值, 比如EOF), 通过associated value来携带真实的值, 而且你还不能"误用"这个值, 因为你要取的是它的wrapped value, 而不是这个枚举.

var array = ["one", "two", "three"]
let idx = array.firstIndex(of: "four")
// Compile-time error: remove(at:) takes an Int, not an Optional<Int>.
array.remove(at: idx)
// work:
if let index = array.firstIndex(of: "four") {
    array.remove(at: index)
}

嵌套的Optional

let stringNumbers = ["1", "2", "three"]
let maybeInts = stringNumbers.map { Int($0) } // [Optional(1), Optional(2), nil]

for...in来取:

for maybeInt in maybeInts {
    // maybeInt is an Int? here
}

这里其实发生了一次解包, 因为iterator.next()会把每一个返回值包一层optional, 这样, 具体到这个例子, 就成了两层可选Int??, 之所以这里直接解决了, 那是因为for...in只是while的语法糖:

var iterator = maybeInts.makeIterator()
while let maybeInt = iterator.next() {
print(maybeInt, terminator: " ")
}
// Optional(1) Optional(2) nil

此外, 因为是optional数组, 因此for循环可以进一步解包:

for case let i? in maybeInts {
    print(i, terminator: " ")// 1 2
}

又是一个利用了swift枚举特性的奇怪语法, 记下就好了, 直接用case来取.some的值

这种奇怪的语法在另一篇笔记里也有提及:Swift, 主要就是直接写case来使用这个枚举

注意, 你这里以为是个可选的数字, 其实就是在操作Optional这个枚举, 使用的是枚举的特性

guard case .some(let i) = maybeInt else { continue }

也是case开头, 意思就是探测一个这个枚举值, 如果是.some的话, 解包出来赋值给i

最后, 上面从两个来源介绍了利用了枚举特性的解包语法, 我们把它互相改写一下, 熟悉一下两种写法:

for case .some(let i) in maybeInts {
    print(i, terminator: " ")
}

// 或
for case let .some(i) in maybeInts {
     print(i, terminator: " ")
}

guard case let i? = maybeInt else { continue}

这里面case let i? = value的写法最直观, 与传统的guard语法最相近, 只多了一个case, 只是这种简写只适用于Optional, 其它枚举类型还是要用枚举名来写case.

始终记住, i?只是.some的简写, case只是探测这个枚举值, 并解包出来赋值给i. 这个也叫case-based pattern

case-based pattern另一个场景:

let j = 5
if case 0..<10 = j {
    print("\(j) within range")
} // 5 within range

if case ..."z" = "d" {
  print(true)
} // true

目前只知道Range可以这么用.

另外, 上例如果只要过滤出nil来迭代的话(有什么实际意义?), 可以用case .nonecase nil.

Structs and Classes, Enums一节里是正式介绍

其它一些空值/空响应/空返回的处理

Never => public enum Never { }

这个枚举没有值, 因此不能实例化, 也不能赋值, 只能用于表示一个不可能发生的情况, 比如一个函数永远不会返回:

func unimplemented() -> Never {
    fatalError("Something went wrong!")
}

比如把这个"未实现"的函数放在一个switch语句中, 避免编译器报错你有未处理的分支.

Void => public typealias Void = () 这个类型别名, 用来表示一个没有返回值的函数, 事实上就是()的别名, 因此可以这么用:

func doSomething() -> Void {
    // do something
}

在响应式编程中, 如果用Observable<T>T表示发送的事件, 那么在文本框中, 这个T就应该是String, 而如果是一个按钮事件, 那么就不需要包含任何信息, 因此可以Observable<Void>

不过, Void这个别名已经废弃了, 用()代替即可. ??

Optional Chaining

  • Swift用?符号让你清醒地认知到这个对象是可选的, 相反的, 别的程序语言里期望程序员们自己知道就好, 而不是在编译层面和书面上强制体现出来.
  • ?进行的访问出来的结果一定也是?的, 这个叫Optional Chaining
let str: String? = "Never say never"
let lower = str?.uppercased().lowercased() // Optional("never say never")

注意, 为什么uppercased()不需要再加?呢? 因为Optional是一个flatterning操作, 即摊平, 摊平的意思是只要有一个Optional, 它后续操作的结果就会用Optional包裹起来, 而如果有多层Optional, 也只会摊平成只有一层, 而不会返成嵌套的, 如Int???.

假定一个对整数取半的操作:

extension Int {
    var half: Int? {
    guard self < -1 || self > 1 else { return nil }
        return self / 2
    }
}

20.half?.half?.half // Optional(2)

这个结果也只会有一层可选.

但你至少要有一个?符号

let dictOfArrays = ["nine": [0, 1, 2, 3]]
print(dictOfArrays["nine"]?[3])

这个?不可省略, 结果也由两层摊平成一层(当然这种写法把索引越界直接暴露出来了, 会导致崩溃).

另一种解包(探测)的方式:

struct Person {
    var name: String
    var age: Int
}
var optionalLisa: Person? = Person(name: "Lisa Simpson", age: 8)

if var lisa = optionalLisa {
    lisa.age += 1
}
// 你要这样当然也可以:
optionalLisa?.age += 1

这里利用的是值类型的特性, 赋值不改变原始值, 鉴于block中可以变量名可以覆盖, 我一直简写成这种:

  if let optionalLisa {
    print(p.name)
  }

其实就是赋值给了一个同名变量, 在作用域范围内, 其实就是新的结构体了, 出了作用域, 原始变量仍然没动.

上面我们看到Unwrapped value可以直接赋值给Optional, 观察下下面两种赋值的区别:

var a: Int? = 5
a? = 10
a // Optional(10)
var b: Int? = nil
b? = 10
b // nil

为什么先赋值为nil的话, 再赋值这个特性就消失了? 因为?的意思就是如果有, 就怎么怎么样, 现在既然是nil了, 那么这个赋值也就中断了. 所以不带问号的话就行了:

b = .some(10)
b = 10

也就是说, Optional想要后续能赋值成功, 就不能初始化为nil/.none

The nil-Coalescing Operator

let stringteger = "1"
let number = Int(stringteger) ?? 0
let array = [1,2,3]
array.first ?? 0

// chain:
let i: Int? = nil
let j: Int? = nil
let k: Int? = 42
i ?? j ?? k ?? 0 // 42

a ?? b ?? c(a ?? b) ?? c的区别?

最大的区别是a??b是解包, 而(a??b)??是解包再解包, 所以如果a??b出来后不再是可选值, 就不存在这个对比了. 要想实现, 就必须是双层可选值, 如:

let s1: String?? = nil
let s2: String?? = .some(nil)
let a = (s1 ?? "inner") ?? "outer" // inner
let b = (s2 ?? "inner") ?? "outer" // outer
let c = s1 ?? "inner" ?? "outer" // Optional("inner")
let d = s2 ?? "inner" ?? "outer" // nil

ab好理解, c我没理解, Optional("inner")哪来的, 而d的意思是, s2有值(some(nil)), 因此取了s2的值, 后续也不会再跟outer去比, 而它的值刚好是nil.

如何设置不同类型的默认值

当你要print一个Optional的时候, 你可能希望它打印出nil而不是Optional(nil), 这时候你可以用??来设置一个默认值, 但是这个默认值必须和Optional的类型一致, 这里演示一个我们自定义的操作符???:

infix operator ???: NilCoalescingPrecedence

public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String)
-> String
{
  switch optional {
    case let value?: return String(describing: value)
    case nil: return defaultValue()
  }
}

使用

print("Body temperature: \(bodyTemperature ??? "n/a")")
// Body temperature: 37.0
print("Blood glucose level: \(bloodGlucose ??? "n/a")")
// Blood glucose level: n/a

这里面有一个知识点@autoclosure, 别人会说这是"延迟求值", 对我来说, 它更大的作用是把函数调用变成表达式, 上例中, 你直接给出"n/a"作为参数, 而不需要写成{ "n/a" }, 即完整的closure形式. 而且显然@autoclosure的可读性更高, 我们需要的确实是一个值,而不是一个函数. 函数只是构建这个值的语法特征而已.

在更早的笔记Interview里也有提及

至于除了延迟求值, 和书写更直观和简洁外, @autoclosure好像并没有什么别的好处, 因为你本可以直接传值(或表达式), 而需要"显得简洁"的场景, 这个计算肯定不是特别复杂的计算, 所以延迟求值带来的性能提升可以忽略不计. 这种表现上直接传值/表达式, 实际上包成closure的写法, 还需要额外的语法支持, 我是觉得没必要的.

知识点2, NilCoalescingPrecedence指的是你自定义这个运算符的优先级, 默认是100, 也就是说, 它的优先级是最低的(比加法还低AdditionPrecedence), 所以你用的时候必须加括号

Optional Chaining

let john = Person()
let roomCount = john?.apartment?.building?.numberOfRooms
// Optional(nil)

Optional map/flatMap/compactMap

map

let characters: [Character] = ["a", "b", "c"]
let firstChar = characters.first.map { String($0) } // Optional("a")

它与集合类型都叫map, 确实是为了实现转化, 只不过它只针对单值, 实现逻辑是:

extension Optional {
func map<U>(transform: (Wrapped) -> U) -> U? {
        guard let value = self else { return nil }
        return transform(value)
    }
}

假定我要写一个数组的reduce的变种, 把第一个元素作为种子传进去(原版初始值要自己定义), 实现思路肯定是把第一个元素作为种子, 然后对dropFirst后的数组进行reduce, 因为要操作第一个元素, 干脆就对第一个元素进行map:

extension Array {
  func reduce_alt(_ nextPartialResult: (Element, Element) -> Element)
  -> Element?
  {
    return first.map {
      dropFirst().reduce($0, nextPartialResult)
    }
  }
}

当然这个例子有点无聊, 跟你用一个if-else没差多少

flatMap

而如果你的map的处理方法有可能返回的是Optional类型, 那么你可能需要flatMap, 避免嵌套:

let stringNumbers = ["1", "2", "3", "foo"]
let x = stringNumbers.first.flatMap { Int($0) } // Optional(Optional(1))
// 当然我们其实平时是这么用的
if let a = stringNumbers.first, let b = Int(a) {
    print(b)
} // 1

更有用的例子

let urlString = "https://www.objc.io/logo.png"
let view = URL(string: urlString)
    .flatMap { try? Data(contentsOf: $0) }
    .flatMap { UIImage(data: $0) }
    .map { UIImageView(image: $0) }
if let view = view {
    PlaygroundPage.current.liveView = view
}

对一个单值, 进行转化后可能为空的情况, 我们通常是拼接if(没问题), 现在知道了可以用flatMap.

compactMap

flat解决的是optional嵌套的问题, 如果map的过程中我们碰到空值不是希望用.none返出来, 而是直接跳过, 那么我们可以用compactMap.

这似乎不是Optional的方法, 出现在这一节是什么原因?

extension Sequence {
    func compactMap<B>(_ transform: (Element) -> B?) -> [B] {
        return lazy.map(transform).filter { $0 != nil }.map { $0! }
    }
}

回顾一下:

let numbers = ["1", "2", "3", "foo"]
var sum = 0
for i in numbers.map({ Int($0) }) {
    print(i); // Optional(1), Optional(2), Optional(3), nil
}

既然我们知道了for case的语法, 而这个例子中for的刚好是一个enum:

for case let i? in numbers.map({ Int($0) }) {
    print(i);
}

这样就通过if let过滤掉了nil(问号不可省略).

Equating Optionals

When comparing two optionals, there are four possibilities:

  1. they’re both nil
  2. they both have a value
  3. either one or the other is nil. 系统实现了每一种比较, 也就是说, 你的wrappedValue必须实现Equatable协议, 才能进行==比较. 此外, 两个nil也被认为是相等.

然后"hello".first == "h"也是成立的, 不需要写成"hello".first == Optional("h")这是因为Swift永远会尝试把非可选值转化为可转值以保持类型一致.

回顾一下字典的取值, 你设进去的是一个确定值, 但是用key取出来一定是一个可选值, 如果没有这种隐式转换, 你就必须设一个可选值, 因为语法要求赋值左右两边类型相等.

但是你要对某个键设置为nil的话, 得还原成Optional, 不然会处理为remove这个key

var dict = [1: "one", 2: "two"]
dict[1] = nil // remove
// set nil as value: 以下三种均可
dict[1] = .some(nil) 
dict[1] = Optional(nil)
dict[1]? = nil // <- 原理: 可选链

因为这是显式把字典的赋值语法写出来了, 所以你要知道你赋的值是nil而不是.some(nil)

Comparing Optionals

要比较可选值, 最好还是把它们都解包出来, 因为nil < 0, 如果这可能是你期望之外的比较, 所以最好自行处理nil等场景. Swift 3之后便移除了Optional的比较操作符

When to Force-Unwrap

只有在非常确认不可能为 nil的场景, 才能对一个Optional进行force-unwrap, 否则你可能会遇到运行时错误.

let ages = [
    "Tim": 53, "Angela": 54, "Craig": 44,
    "Jony": 47, "Chris": 37, "Michael": 34,
]
ages.keys
    .filter { name in ages[name]! < 50 }
    .sorted()
    // ["Chris", "Craig", "Jony", "Michael"]

用key进行的遍历过程中没有理由会不存在, 所以这里的!是安全的. 但即使这样, 你也可以用别的方式:

ages.filter { (_, age) in age < 50 }
    .map { (name, _) in name }
    .sorted()

首先, 你每当在代码里碰到!, 你是肯定需要花精力去追溯一下代码, 看是否用得安全, 其次, 在上面的改写例子中, 效率甚至更高了, 因为没有了对keylookup.

最后, 如果你确实希望这里一定要有个值, 如果没有, 宁愿程序崩溃也不愿兼容一个空值, 那么也是可以使用!的, 表明你的业务场景不接受这种数据.

那么你可以选择一种更友好的方式, 比如定义一个运算符!!:

infix operator !!
func !! <T>(wrapped: T?, failureText: @autoclosure () -> String) -> T {
    if let x = wrapped { return x }
    fatalError(failureText()) // 手动中止程序
}

仍然是让程序崩溃, 但你在代码里有了解释和打印的机会:

let s = "foo"
let i = Int(s) !! "Expecting integer, got \"\(s)\""

推荐这种做法, 而不是一味地去兼容空值让程序不崩溃却跑在错误的数据上.

上例中给了期望中的崩溃, 但其实有更好的方案, debug阶段才崩溃, 发行版本还是期望不要有崩溃发生, 宁愿显示用默认值填充的错误数据, 所以你应该用Assert:

infix operator !?
func !?<T: ExpressibleByIntegerLiteral>
(wrapped: T?, failureText: @autoclosure () -> String) -> T
{
    assert(wrapped != nil, failureText())
    return wrapped ?? 0
}

// 使用
let s = "20"
let i = Int(s) !? "Expecting integer, got \"\(s)\""

其中ExpressibleByIntegerLiteralInt的协议, 你要实现String, Array等版本的方法可以照抄上面的方法, 只需要修改为相应的协议即可, 比如ExpressibleByArrayLiteral, 以及不同的默认值. 而操作符仍然可以不变(这叫Overloading),

三种中止程序的方法:

  1. fatalError(): 无条件中止程序
  2. assert(): 条件中止程序, debug阶段有效, 发行版本会移除
  3. precondition(): 条件中止程序, 发行版本仍然有效

Implicitly Unwrapped Optionals

用感叹号标记的变量, 仍然是可选变量,比如UIView, 只是它在使用的时候能自动解包(真是不可选变量不带任何符号就是了), 之所以存在, 是为了兼容Objective-CC编写的代码. 所以你也不可能在纯Swift代码里看到这种写法.

非要用的话, 你可以随时把它当成一个Optional, 但是也可以直接当成一个非可选值来用(因为它一定有值), 这就是所谓的Implicitly Unwrapped Optionals, 只要你遵循它一定有值的设定:

var s: String! = "Hello"
s?.isEmpty // Optional(false)
s.isEmpty // false, 隐式解包 // 如果是String? 那么这里会报错
if let s = s { print(s) } // Hello
s = nil
s ?? "Goodbye" // Goodbye

Backlinks