《Swift 开发者必备 Tips》 阅读笔记(一)

本编是Swifter - Swift 开发者必备 Tips 阅读笔记

一、柯里化(Currying), 百度百科

  • 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

  • 例如, 现在有如下函数, 作用是每次传入一个数, 返回这个数+1的数字

func addOne(num: Int) -> Int {
    return num + 1
}
  • 如果我们需要一个得到数字 +2, +3.... 等等的函数, 就需要分别定义
func addTwo(num: Int) -> Int {
    return num + 2
}

func addThree(num: Int) -> Int {
    return num + 3
}
  • 我们可以将这类方法简化为, 传入两个参数, 第一个参数是需要增加的数量, 第二个是基数
func addTo(adder: Int, num: Int) -> Int {
    return num + adder
}
  • addTo函数可以进行柯里化, 接收一个单一参数adder, 返回一个接受余下参数的新函数, 并且这个新函数需要返回原有函数的结果
func addTo(adder: Int) -> (Int) -> Int {
    func addNum(num: Int) -> Int {
        return num + adder
    }
    return addNum
}
  • 最终简化为:
func addTo(adder: Int) -> (Int) -> Int {
    return {
        num in
        return num + adder
    }
}
  • 通过将需要传入两个参数的addTo函数, 简化为只需要传入一个参数的addTo函数的过程, 就是柯里化

  • addTo返回的函数, 能够接收余下参数, 并且能够返回最终结果

  • 这时我们想要获取 +2, +3的方法, 只需要如下处理

let addTwo = addTo(2)
print(addTwo(1))          // 打印: 3
print(addTwo(2))          // 打印: 4
print(addTwo(3))          // 打印: 5

let addThree = addTo(3)
print(addThree(1))        // 打印: 4
print(addThree(2))        // 打印: 5
print(addThree(3))        // 打印: 6
  • 柯里化是一种量产相似方法的好办法,可以通过柯里化一个方法模板来避免写出很多重复代码,也方便了今后维护。
柯里化应用实例
  • Swift中, 可以使用如下的方式定义一个方法的管理器
struct Control {
    let target: AnyObject
    let action: Selector
    
    func performAction() {
        target.perform(action)
    }
}
  • Control的实例持有target, 以及target的某一个方法action, 就可以使用调用performAction来调用这个方法
class SomeClass: UIView {
    
    var control: Control?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.control = aControl(target: self, action: NSSelectorFromString("say"))
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        self.control?.performAction()
    }
    
    @objc func say() {
        print("啦啦啦")
    }
    
}
  • 上面的代码中, SomeClass类有一个control属性, 在初始化时, 使control持有了say方法, 当点击SomeClass的实例时, 使用control触发say方法

  • 这种写法针对只有一个调用事件时还算可以, 如果有多个调用事件, 就需要写很多个control属性, 这时就会使代码显得很乱

  • 我们可以通过柯里化, 对这种写法进行优化

  • 首先, 我们定义一个包装target-action的结构体, 这个结构体遵从TargetAction协议

protocol TargetAction {
    func performAction()
}

struct TargetActionWrapper: TargetAction {
    weak var target: T?
    let action: (T) -> () -> ()
    
    func performAction() -> () {
        if let t = target {
            action(t)()
        }
    }
}
  • 接着定义事件触发时机的枚举类型
enum ControlEvent {
    case touchesBegan
    case touchesEnded
    // ...
}
  • 最后定义一个控制容器, 用来存放多个TargetActionWrapper类型的对象, 并管理这些对象
class Control: UIView {
    var actions = [ControlEvent: TargetAction]()
    
    func setTarget(target: T, action: @escaping (T) -> () -> (), controlEvent: ControlEvent) {
        
        actions[controlEvent] = TargetActionWrapper(target: target, action: action)
    }
    
    func removeTargetForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent] = nil
    }
    
    func performActionForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent]?.performAction()
    }
}
  • 我们重写SomClass
class SomeClass: UIView {
    
    var control: Control = Control()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.control.setTarget(target: self, action: SomeClass.touchesBegan, controlEvent: .touchesBegan)
        self.control.setTarget(target: self, action: SomeClass.touchesEnded, controlEvent: .touchesEnded)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        self.control.performActionForControlEvent(controlEvent: .touchesBegan)
    }
    
    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        self.control.performActionForControlEvent(controlEvent: .touchesEnded)
    }
    
    func touchesBegan() {
        print("开始点击")
    }
    
    func touchesEnded() {
        print("结束点击")
    }
}

二、将protocol的方法声明为mutating

  • 对于值类型(结构体和枚举), 想要在实例方法中修改成员变量, 就需要在方法前加上mutating关键字
protocol Vehicle
{
    var numberOfWheels: Int {get}
    var color: UIColor {get set}

    mutating func changeColor()
}

struct MyCar: Vehicle {
    let numberOfWheels = 4
    var color = UIColor.blue

    mutating func changeColor() {
        // 因为 `color` 的类型是 `UIColor`,这里直接写 .red 就足以推断类型了
        color = .red
    }
}
  • 另外,在使用 class 来实现带有 mutating 的方法的协议时,具体实现的前面是不需要加 mutating 修饰的,因为 class 可以随意更改自己的成员变量。所以说在协议里用 mutating 修饰方法,对于 class 的实现是完全透明,可以当作不存在的。

三、Sequence

1、迭代器, 百度百科
  • 迭代器是一个对象,它的工作是遍历并选择序列中的对象,它提供了一种访问一个容器对象中的各个元素,而又不必暴露该对象内部细节的方法。通过迭代器,开发人员不需要了解容器底层的结构,就可以实现对容器的遍历。由于创建迭代器的代价小,因此迭代器通常被称为轻量级的容器。

  • Swift中的迭代器需要遵从IteratorProtocol协议

public protocol IteratorProtocol {

    associatedtype Element

    // 返回下一个元素, 如果没有返回 nil
    // 通过反复调用, 可以获取所有的元素, 一旦元素的序列已经耗尽, 所有后续的调用返回 nil
    public mutating func next() -> Self.Element?
}
  • 以数组的迭代器为例, 可以使用数组的makeIterator()方法, 获取数组的迭代器
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var animalIterator = animals.makeIterator()

while let animal = animalIterator.next() {
    print(animal)
}
// 打印结果:
Antelope
Butterfly
Camel
Dolphin
  • 此时已经通过迭代器遍历了数组中的所有元素

  • 如果再次调用animalIterator.next()方法, 只会返回nil

print(animalIterator.next())
// 打印结果: nil
  • 迭代器只能通过next()方法获取下一个元素, 一直遍历到最后为止, 而无法返回到一开始的位置重新遍历

  • 我们也可以自定义迭代器, 只需准守IteratorProtocol协议

struct CustomIterator: IteratorProtocol {
    
    typealias Element = T
    
    var array: [Element]
    
    var currentIndex = 0
    
    init(_ array: [Element]) {
        self.array = array
    }
    
    mutating func next() -> Element? {
        if currentIndex < array.count {
            let int = array[currentIndex]
            currentIndex += 1
            return int
        }else {
            return nil;
        }
    }
}
  • 通过数组创建一个CustomIterator迭代器对象
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var custom = CustomIterator(animals)
  • 遍历刚创建的结构体
while let animal = custom.next() {
    print(animal)
}

// 打印:
Antelope
Butterfly
Camel
Dolphin
  • 再次调用custom.next(), 将会返回nil
print(custom.next())
// 返回: nil
  • 上面自定义的迭代器是正向的遍历集合中的元素, 我们还可以定义一个反向遍历的迭代器
struct CustomReverseIterator: IteratorProtocol {
    
    typealias Element = T
    
    var array: [Element]
    
    var currentIndex: Int
    
    init(_ array: [Element]) {
        self.array = array
        currentIndex = array.count - 1
    }
    
    mutating func next() -> Element? {
        guard currentIndex >= 0 else {
            return nil
        }
        let int = array[currentIndex]
        currentIndex -= 1
        return int
    }
}
2、Sequence
  • 在Swift中, 所有想要实现for-in循环的对象, 都必须继承Sequence协议

  • 遵守Sequence协议的最简单的类型, 需要实现以下方法

public protocol Sequence {
    // 关联类型, 必须遵守了IteratorProtocol协议
    associatedtype Iterator : IteratorProtocol
    // 获取一个迭代器
    public func makeIterator() -> Self.Iterator
}
  • for-in循环每一次都会调用序列的makeIterator()方法, 来获取一个新的迭代器, 然后开始遍历

  • 现在我们自定义一个可倒序遍历的序列

struct CustomReverseSequence: Sequence {
    
    var array: [T]
    
    init(_ array: [T]) {
        self.array = array
    }
    
    typealias intrator = CustomReverseIterator
    
    func makeIterator() -> intrator {
        return CustomReverseIterator(self.array)
    }
}
  • 我们通过数组创建一个CustomReverseSequence的实例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
  • 此时, 我们可以使用for-in循环, 来遍历sequence对象
for animal in sequence {
    print(animal)
}
// 打印:
Dolphin
Camel
Butterfly
Antelope
  • 这段代码等价于
var iterator = sequence.makeIterator()
while let animal = iterator.next() {
    print(animal)
}
  • 有一点比较方便的是, Sequence扩展中, 实现了mapfilterreduce这些方法, 我们可以直接调用, 下面以map为例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
let newSequence = sequence.map {
    return $0 + "123"
}
print(newSequence)
// 打印: ["Dolphin123", "Camel123", "Butterfly123", "Antelope123"]

四、多元组(Tuple)

  • 元组可以将多种类型的值, 包装起来, 并做为返回值使用

  • 例如OC版本的CGRect, 有一个辅助方法CGRectDivide, 可以讲一个CGRect实例切割成两部分

/*
CGRectDivide(CGRect rect, CGRect *slice, CGRect *remainder, 
                             CGFloat amount, CGRectEdge edge)
*/
CGRect rect = CGRectMake(0, 0, 100, 100);
CGRect small;
CGRect large;
CGRectDivide(rect, &small, &large, 20, CGRectMinXEdge);
  • 上面的代码将 {0,0,100,100} 的 rect 分割为两部分,分别是 {0,0,20,100}small{20,0,80,100}large。由于 C 系语言的单一返回,我们不得不通过传入指针的方式让方法来填充需要的部分,可以说使用起来既不直观,又很麻烦。

  • 而在Swift中, 可以使用元组来返回多个值

extension CGRect {
    //...
    func divided(atDistance: CGFloat, from fromEdge: CGRectEdge) 
                    -> (slice: CGRect, remainder: CGRect)
    //...
}
  • 在使用的时候, 对比OC会方便很多, 也更容易理解
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let (small, large) = rect.divided(atDistance: 20, from: .minXEdge)
  • 上面的代码, 就是使用元组做为返回值的一个例子, 除此之外, 还可以使用元组简化交换两个变量的值的写法
  • 在OC中, 想要交换两个变量的值, 需要使用如下类似的写法, 用一个常量做为中转
func swapMe1( a: inout T, b: inout T) {
    let temp = a
    a = b
    b = temp
}
  • 而使用Swift的元组, 就可以如下处理
func swapMe2(a: inout T, b: inout T) {
    (a, b) = (b, a)
}

五、@autoclosure 和 ??

1、自动闭包 @autoclosure
  • 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。
  • 简单地说, 就是把一句表达式封装成一个闭包, 并将表达式的结果作为返回值
  • 比如有如下函数, logIfTrue接收一个()->Bool类型的闭包
func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print("true")
    }
}
  • 当我们调用logIfTrue方法时
logIfTrue { () -> Bool in
    return 2 > 1
}
  • 可以简化为
logIfTrue {2 > 1}
  • 但是不管哪一种方式, 要么是书法起来十分麻烦, 要么是表达上不太清晰, 所以苹果提供了@autoclosure关键字, 可以将logIfTrue函数的定义改写为下面的样子
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("true")
    }
}
  • 这时候, 再次调用logIfTrue函数
logIfTrue (2 > 1)
  • Swift将会把2 > 1这个表达式自动转换为() -> Bool类型的闭包, 这样我们就得到了一个写法简单, 表意清除的式子
2、??
  • 在Swift中, 有一个非常有用的操作符, 可以用来快速地对nil进行条件判断, 那就是??。这个操作符可以判断输入并在当左侧的值是非nil的Optional值时返回其value, 当左侧是nil时返回右侧的值, 比如:
var a: Int?
var b = 10
var c = a ?? b
  • 这个例子, 我们没有给a赋值, 所以最终会将b的值赋值给c, 即c = 10

  • 我们可以点进??的定义中查看

func ??(optional: T?, defaultValue: @autoclosure () -> T?) -> T?

func ??(optional: T?, defaultValue: @autoclosure () -> T) -> T
  • 在这里, 我们的输入满足的是后者, 虽然表面上看b只是一个Int, 但是在使用的时候, 被自动封装成了() -> Int类型的闭包

  • 此时, 推测一下??的具体实现

func ??(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    switch optional {
    case .some(let value):
        return value
    case .none:
        return defaultValue()
    }
}
  • 这里将defaultValue参数定义为了() -> T类型的自动闭包, 而不是T类型
  • 如果我们直接使用T做为defaultValue参数的类型, 那么在使用??操作符之前就需要准备一个默认值。
  • 正常情况下, 这样不会有大问题, 但是如果这个默认值是通过一系列复杂计算得到的话, 可能会造成资源浪费, 因为如果optional不是nil的话, 这个默认是根本不会被用到, 而会直接返回optional解包后的值
  • 使用autoclosure就避免了这样的开销, 方法会将默认值的计算推迟到optional判定为nil之后

@autoclosure并不支持带有输入参数的写法, 也就是说只有形如() -> T的参数才能使用这个特性简化

六、@escaping

  • Swift中我们可以定义一个接受函数做为参数的函数, 而在调用时, 使用闭包的方式来传递这个参数是常见手段
  • 定义函数
func doWork(block: () -> ()) {
    block()
}
  • 调用函数
doWork {
    print("work")
}
  • 这种最简单的形式的闭包其实还默认隐藏了一个假设, 那就是参数block的内容会在doWork返回前就完成, 也就是说, 对于block的调用是同步行为。
  • 如果我们改变一下代码, 将block放到一个Dispatch中去, 让他在doWork返回后被调用的话, 我们就需要在block的类型前加上@escaping标记来表名这个闭包是会逃逸出该方法的
func doWorkAsync(block: @escaping: () -> ()) {
    DispatchQueue.main.async {
        block()
    }
}
  • 在使用闭包调用这个两个方法时,也会有一些行为的不同。我们知道,闭包是可以捕获其中的变量的。对于 doWork 参数里这样的没有逃逸行为的闭包,因为闭包的作用域不会超过函数本身,所以我们不需要担心在闭包内持有 self 等。
  • 而接受 @escapingdoWorkAsync 则有所不同。由于需要确保闭包内的成员依然有效,如果在闭包内引用了 self 及其成员的话,Swift 将强制我们明确地写出 self。对比下面的两个用例的不同之处:
class S {
    var foo = "foo"

    func method1() {
        doWork {
            print(foo)
        }
        foo = "bar"
    }

    func method2() {
        doWorkAsync {
            print(self.foo)
        }
        foo = "bar"
    }
}

S().method1() // foo
S().method2() // bar
  • 如果我们不希望在闭包中持有self, 可以使用[weak self]的方式来表达:
func method3() {
    doWorkAsync {
        [weak self] in
        print(self?.foo ?? "nil")
    }
    foo = "bar"
}

S().method3() // nil
  • 因为S()没有被强引用, 所以作用域结束后就会销毁, 此时才会去调用doWorkAsync中的block, 这时self已经销毁, 所以才会打印nil

如果你在协议或者父类中定义了一个接受@escaping为参数的方法, 那么在实际协议和类型或者是这个父类的子类时, 对应的方法也必须被声明为@escaping, 否则两个方法会被认为拥有不同的函数签名

protocol P {
    func work(b: @escaping ()->())
}

// 可以编译
class C: P {
    func work(b: @escaping () -> ()) {
        DispatchQueue.main.async {
           print("in C")
           b()
        }
    }
}

// 而这样是无法编译通过的:
class C1: P {
    func work(b: () -> ()) {
        // ...
    }
}

七、Optional Chaining

  • 在使用可选链的时候, 可以让我们摆脱很多不必要的判断和取值
  • 比如有下面的一段代码
class Toy {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class Pet {
    var toy: Toy?
}

class Child {
    var pet: Pet?
}
  • 我们可以使用可选链, 调用一个孩子的宠物的玩具的名字
let xiaoming = Child()
let toyName = xiaoming.pet?.toy?.name
  • 在实际的使用中, 我们大多数情况下可能更希望使用可选绑定来直接取值:
if let toyName = xiaoming.pet?.toy?.name {
    // 小明有宠物, 并且宠物拥有玩具
}
  • 上面的代码是对属性进行可选判断, 除此之外, 还可以对方法进行可选绑定, 判断方法是否调用成功

  • Toy定义一个扩展

extension Toy {
    func play() {
        // 宠物玩玩具
    }
}
  • 还是使用上面的例子, 如果孩子有玩具, 就玩
xiaoming.pet?.toy?.play()
  • 我们知道没有返回值的方法具有隐式的返回类型Void, 等价于(), 或者说空的元组

  • 所以我们可以使用可选链来判断, 孩子的宠物是否真的调用play()方法

if let result: () = xiaoming.pet?.toy?.play() {
    // 玩玩具
}else{
    // 没有玩具可以玩
}
  • 除了例子中的小明, 可能还会有小红, 小李等其他孩子, 这个时候如果每个孩子都判断他们各自的宠物是否玩玩具, 就需要分别写
if let result: () = xiaohong.pet?.toy?.play() {
    // 玩玩具
}else{
    // 没有玩具可以玩
}

if let result: () = xiaoli.pet?.toy?.play() {
    // 玩玩具
}else{
    // 没有玩具可以玩
}
  • 这个时候我们就会想着将xiaoming.pet?.toy?.play()这一段代码抽象出来, 做成一个闭包方便使用。传入一个Child对象, 如果小朋友有宠物并且宠物有玩具的话, 就去玩, 于是很可能你会写出这样的代码:

这是错误的代码

let playClosure = { (child: Child) -> () in
    child.pet?.toy?.play()
}
  • 这样的代码是没有意义的, 因为这段代码等价于
let playClosure = { (child: Child) -> () in
    child.pet?.toy?.play()
   return ()
}
  • 我们需要将child.pet?.toy?.play()的返回值做为闭包playClosure的返回值, 所以我们需要如下代码
let playClosure = { (child: Child) -> ()? in
    child.pet?.toy?.play()
}
  • 上面的代码, 有一个隐式的return, 实际代码如下
let playClosure = { (child: Child) -> ()? in
    return child.pet?.toy?.play()
}
  • 这时我们可以使用可选绑定来判断方法是否调用成功
if let toyName = playClosure(xiaoming) {
    print("玩玩具~")
}else {
    print("没有玩具玩~")
}

if let toyName = playClosure(xiaohong) {
    print("玩玩具~")
}else {
    print("没有玩具玩~")
}

if let toyName = playClosure(xiaoli) {
    print("玩玩具~")
}else {
    print("没有玩具玩~")
}

八、操作符

  • 与Objective-C不同, Swift支持重载操作符这样的特性, 最常见的使用方式可能就是定义一些简便的计算

  • 比如我们需要定义一个表示二位向量的数据结构

struct Vector2D {
    var x = 0.0
    var y = 0.0
}
  • 一个很简单的需求是像个Vector2D相加
let v1 = Vector2D(x: 1, y: 2)
let v2 = Vector2D(x: 3, y: 4)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)
// v3为 Vector2D(x: 4.0, y: 6.0)
  • 这样的计算很简单, 如果只做一次两次很不错, 但是一般情况下我们会进行很多这样的操作, 这样的话, 我们可能更愿意定义一个Vector2D相加的操作, 让代码简化清晰
  • 对于两个向量的相加, 我们可以重载加号(+)操作符:
func + (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
  • 这样, 上面的v3以及之后的所有表示两个向量相加的操作就全部可以用加号来表示了
let v4 = v1 + v2
// v4为 Vector2D(x: 4.0, y: 6.0)
  • 类似的, 我们还可以为Vector2D定义像-(减号, 两个向量相减), -(负号, 单个向量x和y同时取负)等等这样的运算符
// - 减号
func - (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x - right.x, y: left.y - right.y)
}

// - 负号
prefix func - (vector: Vector2D) -> Vector2D {
    return Vector2D(x: -vector.x, y: -vector.y)
}
  • 调用时, 直接使用操作符即可
let v5 = v1 - v2
print(v5)
// v5 为 Vector2D(x: -2.0, y: -2.0)

let v6 = -v1
print(v6)
// v6 为 Vector2D(x: -1.0, y: -2.0)
  • 上面的加号, 减号和负号都是已经存在于Swift中的运算符了, 我们所做的只是变换他的参数进行重载
  • 如果我们想要定义一个全新的运算符的话, 要做的事会多一件
  • 比如点积运算就是一个矢量运算中很常用的运算符, 它表示两个向量对应坐标的乘积的和。
  • 根据定义以及参考重载运算符的方法, 我们选取+*来表示这个运算的话, 不难写出:
func +* (letf: Vector2D, right: Vector2D) -> Double {
    return letf.x * right.x + letf.y * right.y
}
  • 但是编译器会给我们一个错误:

Operator implementation without matching operator declaration

  • 这是因为+*与之前的+, -等操作符不同, +, -这样的操作符是Swift中已经有定义声明的, 如果我们想要添加新的操作符的话, 需要先对其进行生命, 告诉编译器这个符号其实是一个操作符。添加如下代码:
precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}

infix operator +*: DotProductPrecedence

precedencegroup

定义一个操作符优先级别。操作符优先级的定义和类型声明有些类似, 一个操作符需要属于某个特定的优先级。Swift标准库中已经定义了一些常用的运算优先级组, 比如加法优先级组(AdditionPrecedence)和乘法优先级组(MultiplicationPrecedence)等。如果没有合适你的运算符的优先级组, 你就需要向我们在例子中做的这样, 自己指定结合律方式和优先级顺序了

associativity

定义了结合律, 即多个同类的操作符顺序出现时的计算顺序。比较常见的加法和减法都是left, 就是说多个加法同时出现时按照从左往右的顺序计算。
点乘的结果是一个Double, 不会再和其他点乘结合使用, 所以这里是none

higherThan

运算的优先级, 点积运算是优先于乘法运算的。除了higherThan, 也支持使用loverThan来指定优先级低于某个其他组。

infix

表示要定义的是一个中位操作符, 即前后都是输入; 其他的修饰子还包括prefixpostfix

  • 有了这些之后, 点积运算符的定义如下
precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}

infix operator +*: DotProductPrecedence

func +* (letf: Vector2D, right: Vector2D) -> Double {
    return letf.x * right.x + letf.y * right.y
}
  • 我们可以很简单地进行向量的点击运算了:
let result = 3 * 5 + v1 +* v2
print(result)
// 打印: 26.0

最后需要多提一点的是,Swift 的操作符是不能定义在局部域中的,因为至少会希望在能在全局范围使用你的操作符,否则操作符也就失去意义了。另外,来自不同 module 的操作符是有可能冲突的,这对于库开发者来说是需要特别注意的地方。如果库中的操作符冲突的话,使用者是无法像解决类型名冲突那样通过指定库名字来进行调用的。因此在重载或者自定义操作符时,应当尽量将其作为其他某个方法的 "简便写法",而避免在其中实现大量逻辑或者提供独一无二的功能。这样即使出现了冲突,使用者也还可以通过方法名调用的方式使用你的库。运算符的命名也应当尽量明了,避免歧义和可能的误解。因为一个不被公认的操作符是存在冲突风险和理解难度的,所以我们不应该滥用这个特性。在使用重载或者自定义操作符时,请先再三权衡斟酌,你或者你的用户是否真的需要这个操作符。

九、func的参数修饰

  • 在声明一个Swift 的方法的时候, 我们一般不去指定参数前面的修饰符, 而是直接声明参数
func incrementor(variable: Int) -> Int {
    return variable + 1
}
  • 如果我们想要对增加后的变量做点什么,又不想引入一个新的变量的话,很可能会写出这样的代码:

这是错误的代码

func incrementor(variable: Int) -> Int {
   variable += 1
   print(variable)
   return variable
}
  • 之所以会有错误, 是因为Swift是一门讨厌变化的语言, 所有有可能的地方, 都被默认认为是不可变的, 也就是用let进行声明的
  • 现在我们如果想只在函数内部对这样的输入值进行修改的话, 只能显示地在函数内部进行使用var进行赋值以后再操作了:
func incrementor2(variable: Int) -> Int {
    var num = variable
    num += 1
    return num
}
  • 有些时候我们会希望在方法内部直接修改输入的值, 这时候我们可以使用inout来对参数进行修饰
func incrementor(variable: inout Int) {
    variable += 1
}
  • 因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上 & 符号:
var luckyNumber = 7
incrementor(variable: &luckyNumber)

print(luckyNumber)
// luckyNumber = 8
  • 最后,要注意的是参数的修饰是具有传递限制的,就是说对于跨越层级的调用,我们需要保证同一参数的修饰是统一的。举个例子,比如我们想扩展一下上面的方法,实现一个可以累加任意数字的 +N器 的话,可以写成这样:
func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
    func incrementor(variable: inout Int) -> () {
        variable += addNumber;
    }
    return incrementor;
}
  • 外层的 makeIncrementor 的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过。

十、字面量表达

1、字面量
  • 所谓字面量, 就是指像特定的数字, 字符串或者是布尔值这样, 能够直截了当的指出自己的类型并为变量进行赋值的值
  • 比如在下面中的3hello world以及true就称为字面量
let aNumber = 3
let aString = "hello world"
let aBool = true
  • 在Swift中, ArrayDictionary在使用简单的描述赋值的时候, 使用的也是字面量, 比如:
let anArray = [1, 2, 3]
let aDictionary = ["key1" : "value1", "key2" : "vakue2"]

2、使用字面量创建指定类型

  • Swift为我们提供了一组协议, 使用字面量表达特定的类型
  • 这些协议包括了各个原生的字面量, 在开发中可能经常用的有以下几种:
ExpressibleByArrayLiteral
ExpressibleByBooleanLiteral
ExpressibleByDictionaryLiteral
ExpressibleByFloatLiteral
ExpressibleByNilLiteral
ExpressibleByIntegerLiteral
ExpressibleByStringLiteral
  • 对于实现了上面协议的类型, 在提供字面量赋值的时候, 就可以简单地按照协议方法中定义的规则"无缝对应"的通过赋值的方式将值表达为对应类型
  • 所有的字面量表达协议都定义了一个 typealias 和对应的 init 方法。拿 ExpressibleByBooleanLiteral 举个例子:
public protocol ExpressibleByBooleanLiteral {

    associatedtype BooleanLiteralType : _ExpressibleByBuiltinBooleanLiteral

    public init(booleanLiteral value: Self.BooleanLiteralType)
}
  • 在这个协议中, Swift的标准库已经定义了BooleanLiteralType的类型是Bool, 如果在类型中将BooleanLiteralType设置为其他类型, 就会报错
/// The default type for an otherwise-unconstrained boolean literal
typealias BooleanLiteralType = Bool
  • 举一个例子: 我们可以自定义Int的分类, 并遵守ExpressibleByBooleanLiteral协议
extension Int: ExpressibleByBooleanLiteral {
    public typealias BooleanLiteralType = Bool
    
    public init(booleanLiteral value: Int.BooleanLiteralType) {
        self = value ? 1 : 0
    }
}
  • 这时, 我们可以给一个整形变量赋值true:
let num1: Int = true
let num2: Int = false
print(num1, num2)        // 打印: 1 0
3、ExpressibleByStringLiteral
  • ExpressibleByStringLiteral协议稍微有些不同, 情况要复杂一些。
  • 如果类型遵守ExpressibleByStringLiteral协议, 那么同时必须遵守下面的两个协议
ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral
  • 这两个协议我们在日常项目中基本上不会使用,它们对应字符簇和字符的字面量表达。虽然复杂一些,但是形式上还是一致的,只不过在实现 ExpressibleByStringLiteral 时我们需要将这三个 init 方法都进行实现。

  • 还是以例子来说明,比如我们有个 Person 类,里面有这个人的名字:

class Person {
    let name: String
    init(name value: String) {
        self.name = value
    }
}
  • 如果想要通过 String 赋值来生成 Person 对象的话,可以改写这个类:
class Person: ExpressibleByStringLiteral {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required init(stringLiteral value: String) {
        self.name = value
    }

    required init(extendedGraphemeClusterLiteral value: String) {
        self.name = value
    }

    required init(unicodeScalarLiteral value: String) {
        self.name = value
    }
}
  • 在所有的协议定义的 init 前面我们都加上了 required 关键字,这是由初始化方法的完备性需求所决定的,这个类的子类都需要保证能够做类似的字面量表达,以确保类型安全。

注意: 如果是结构体和枚举, 就不需要加required, 这是因为值类型无法被继承, 就不会有子类

  • 在上面的例子里有很多重复的对 self.name 赋值的代码,这是我们所不乐见的。一个改善的方式是在这些初始化方法中去调用原来的 init(name value: String),这种情况下我们需要在这些初始化方法前加上 convenience
class Person: ExpressibleByStringLiteral {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required convenience init(stringLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(unicodeScalarLiteral value: String) {
        self.init(name: value)
    }
}

let p: Person = "xiaoMing"
print(p.name)

// 输出:
// xiaoMing
  • 上面的 Person 的例子中,我们没有像 Int 中做的那样,使用一个 extension 的方式来扩展类使其可以用字面量赋值,这是因为在 extension 中,我们是不能定义 required 的初始化方法的。也就是说,我们无法为现有的非 finalclass 添加字面量表达
总结一下,字面量表达是一个很强大的特性,使用得当的话对缩短代码和清晰表意都很有帮助;但是这同时又是一个比较隐蔽的特性:因为你的代码并没有显式的赋值或者初始化,所以可能会给人造成迷惑:比如上面例子中为什么一个字符串能被赋值为 Person?你的同事在阅读代码的时候可能不得不去寻找这些负责字面量表达的代码进行查看 (而如果代码库很大的话,这不是一件容易的事情,因为你没有办法对字面量赋值进行 Cmd + 单击跳转)。

来自: 王巍 (onevcat). “Swifter - Swift 必备 Tips (第四版)。”

你可能感兴趣的:(《Swift 开发者必备 Tips》 阅读笔记(一))