阅读 1831

Swift必备Tips重点笔记

本章内容来自于喵神的Swift必备Tips,有兴趣的同学可以阅读原书,更加详细!

本章内容来自于喵神的Swift必备Tips,有兴趣的同学可以阅读原书,更加详细!

本章内容来自于喵神的Swift必备Tips,有兴趣的同学可以阅读原书,更加详细!

@autoclosure 和 ??

@autoclosure 做的事情就是把一句表达式自动地封装成一个闭包 (closure),这样有时候在语法上看起来就会非常漂亮。

func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print("True")
    }
}

logIfTrue({ return 2 > 1 })
// 简化,可以去掉return
logIfTrue({2 > 1})
// 简化,尾随闭包,省略括号
logIfTrue{2 > 1}
// 但是不管哪种方式,要么是书写起来十分麻烦,要么是表达上不太清晰

func logIfTrue2(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("True")
    }
}
// Swift 将会把 2 > 1 这个表达式自动转换为 () -> Bool。这样我们就得到了一个写法简单,表意清楚的式子。
logIfTrue2(2 > 1)
复制代码

空合并运算符 ?? 可以用来快速地对 nil 进行条件判断。这个操作符可以判断输入并在当左侧的值是非 nil 的 Optional 值时返回其 value,当左侧是 nil 时返回右侧的值。

  • a ?? b
  • a 是可选项
  • b 可以是可选项,也可以不是可选项
  • b 跟 a 的存储类型必须相同
  • 如果 b 不是可选项,返回 a 时就会自动解包

我们点进去 ?? 的定义可以看到两个版本:

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

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T
复制代码

由此也能证明,返回的类型是否是可选类型是由 b 来决定的

猜测一下 ?? 的实现:

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    switch optional {
        case .Some(let value):
            return value
        case .None:
            return defaultValue()
        }
}
复制代码

可能你会有疑问,为什么这里要使用 autoclosure,直接接受 T 作为参数并返回不行么,为何要用 () -> T 这样的形式包装一遍,岂不是画蛇添足?其实这正是 autoclosure 的一个最值得称赞的地方。如果我们直接使用 T,那么就意味着在 ?? 操作符真正取值之前,我们就必须准备好一个默认值传入到这个方法中,一般来说这不会有很大问题,但是如果这个默认值是通过一系列复杂计算得到的话,可能会成为浪费 -- 因为其实如果 optional 不是 nil 的话,我们实际上是完全没有用到这个默认值,而会直接返回 optional 解包后的值的。这样的开销是完全可以避免的,方法就是将默认值的计算推迟到 optional 判定为 nil 之后。

就这样,我们可以巧妙地绕过条件判断和强制转换,以很优雅的写法处理对 Optional 及默认值的取值了。最后要提一句的是,@autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能使用这个特性进行简化。

weak 和 unowned

如果您是一直写 Objective-C 过来的,那么从表面的行为上来说 unowned 更像以前的 unsafe_unretained,而 weak 就是以前的 weak。用通俗的话说,就是 unowned 设置以后即使它原来引用的内容已经被释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果你尝试调用这个引用的方法或者访问成员属性的话,程序就会崩溃。而 weak 则友好一些,在引用的内容被释放后,标记为 weak 的成员将会自动地变成 nil (因此被标记为 @weak 的变量一定需要是 Optional 值)。

关于两者使用的选择,Apple 给我们的建议是如果能够确定在访问时不会已被释放的话,尽量使用 unowned,如果存在被释放的可能,那就选择用 weak

日常工作中一般使用弱引用的最常见的场景有两个:

  • 设置 delegate 时
  • 在 self 属性存储为闭包时,其中拥有对 self 引用时

值类型和引用类型

Swift 的类型分为值类型和引用类型两种,值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个 "指向"。Swift 中的所有的内建类型都是值类型,不仅包括了传统意义像 Int,Bool 这些,甚至连 String,Array 以及 Dictionary 都是值类型的。这在程序设计上绝对算得上一个震撼的改动,因为据我所知现在流行的编程语言中,像数组和字典这样的类型,几乎清一色都是引用类型。

那么使用值类型有什么好处呢?相较于传统的引用类型来说,一个很显而易见的优势就是减少了堆上内存分配和回收的次数。首先我们需要知道,Swift 的值类型,特别是数组和字典这样的容器,在内存管理上经过了精心的设计。值类型的一个特点是在传递和赋值时进行复制,每次复制肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候,值类型的复制都是不会发生的。也就是说,简单的赋值,参数的传递等等普通操作,虽然我们可能用不同的名字来回设置和传递值类型,但是在内存上它们都是同一块内容。

值类型在复制时,会将存储在其中的值类型一并进行复制,而对于其中的引用类型的话,则只复制一份引用。

虽然将数组和字典设计为值类型最大的考虑是为了线程安全,但是这样的设计在存储的元素或条目数量较少时,给我们带来了另一个优点,那就是非常高效,因为 "一旦赋值就不太会变化" 这种使用情景在 Cocoa 框架中是占有绝大多数的,这有效减少了内存的分配和回收。但是在少数情况下,我们显然也可能会在数组或者字典中存储非常多的东西,并且还要对其中的内容进行添加或者删除。在这时,Swift 内建的值类型的容器类型在每次操作时都需要复制一遍,即使是存储的都是引用类型,在复制时我们还是需要存储大量的引用,这个开销就变得不容忽视了。幸好我们还有 Cocoa 中的引用类型的容器类来对应这种情况,那就是 NSMutableArray 和 NSMutableDictionary。

所以,在使用数组和字典时的最佳实践应该是,按照具体的数据规模和操作特点来决定到时是使用值类型的容器还是引用类型的容器:在需要处理大量数据并且频繁操作 (增减) 其中元素时,选择 NSMutableArray 和 NSMutableDictionary 会更好,而对于容器内条目小而容器本身数目多的情况,应该使用 Swift 语言内建的 Array 和 Dictionary。

重载和自定义操作符

对于Swift中已经存在的操作符,我们可以通过变换参数进行重载

struct Vector2D {
    var x = 0.0
    var y = 0.0
}

func +(left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y + right.y)
}

let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let v3 = v1 + v2
复制代码

对于我们自定义的操作符,需要先对其进行声明,告诉编译器这个符号其实是一个操作符

/// 声明操作符
// 定义优先级组
precedencegroup DotProductPrecedence {
    associativity: none										// 结合律方向:left, right or none
    higherThan: MultiplicationPrecedence	// 优先级,比加法运算高
  	assignment: false											// true=赋值运算符,false=非赋值运算符
}
// infix 表示要定义的是一个中位操作符,即前后都是输入;其他的修饰子还包括 prefix 和 postfix
infix operator +*: DotProductPrecedence

/// 重载操作符
func +* (left: Vector2D, right: Vector2D) -> Double {
    return left.x * right.x + left.y * right.y
}

struct Vector2D {
    var x = 0.0
    var y = 0.0
}

let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let result = v1 +* v2
print(result) // 14.0
复制代码
  • Swift 的操作符是不能定义在局部域中的,因为至少会希望在能在全局范围使用你的操作符,否则操作符也就失去意义了。
  • 来自不同 module 的操作符是有可能冲突的,这对于库开发者来说是需要特别注意的地方。如果库中的操作符冲突的话,使用者是无法像解决类型名冲突那样通过指定库名字来进行调用的。因此在重载或者自定义操作符时,应当尽量将其作为其他某个方法的 "简便写法",而避免在其中实现大量逻辑或者提供独一无二的功能。这样即使出现了冲突,使用者也还可以通过方法名调用的方式使用你的库。
  • 运算符的命名也应当尽量明了,避免歧义和可能的误解。因为一个不被公认的操作符是存在冲突风险和理解难度的,所以我们不应该滥用这个特性。

inout 函数参数修饰符

  • 可以用 inout 定义一个输入输出参数:可以在函数内部修改外部实参的值。
  • 可变参数不能标记为inout
  • inout参数不能有默认值
  • inout参数只能传入可以被多次赋值的
  • inout 的本质是地址传递(引用传递)
var number = 10
func add(_ num: inout Int) {
		num = 20
}
add(&number)

func swapValues(_ v1: inout Int, _ v2: inout Int) {
  	let tmp = v1
  	v1 = v2
  	v2 = tmp
}
var num1 = 10; var num2 = 20
swapValues(&num1, &num2)

func swapValues(_ v1: inout Int, _ v2: inout Int) {
  	(v1, v2) = (v2, v1)
}
复制代码

typealias 类型别名

typealias 是用来为已经存在的类型重新定义名字的,通过命名,可以使代码变得更加清晰。

func distance(from point: CGPoint, to anotherPoint: CGPoint) -> Double {
    let dx = Double(anotherPoint.x - point.x)
    let dy = Double(anotherPoint.y - point.y)
    return sqrt(dx * dx + dy * dy)
}

let origin: CGPoint = CGPoint(x: 0, y: 0)
let point: CGPoint = CGPoint(x: 1, y: 1)

let d: Double =  distance(from: origin, to: point)
复制代码

虽然在数学上和最后的程序运行上都没什么问题,但是阅读和维护的时候总是觉得有哪里不对。因为我们没有将数学抽象和实际问题结合起来,使得在阅读代码时我们还需要在大脑中进行一次额外的转换:CGPoint 代表一个点,而这个点就是我们在定义的坐标系里的位置;Double 是一个数字,它代表两个点之间的距离。

如果我们使用 typealias,就可以将这种转换直接写在代码里,从而减轻阅读和维护的负担:

import UIKit

typealias Location = CGPoint
typealias Distance = Double

func distance(from location: Location,
    to anotherLocation: Location) -> Distance {
        let dx = Distance(location.x - anotherLocation.x)
        let dy = Distance(location.y - anotherLocation.y)
        return sqrt(dx * dx + dy * dy)
}

let origin: Location = Location(x: 0, y: 0)
let point: Location = Location(x: 1, y: 1)

let d: Distance = distance(from: origin, to: point)
复制代码

在别名中引入泛型

typealias Worker<T> = Person<T>

class Person<T> {}
typealias WorkId = String
typealias Worker = Person<WorkId>
复制代码

associatedtype 关联类型

关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。

protocol Food { }
struct Meat: Food { }
struct Grass: Food { }

protocol Animal {
    associatedtype F: Food
    func eat(_ food: F)
}

struct Tiger: Animal {
		// 只要实现了正确类型的eat,F的类型就可以被推断出来,所以我们也不需要显式地写明F
		// typealias F = Meat
    func eat(_ food: Meat) {
        print("eat \(meat)")
    }
}

struct Sheep: Animal {
    func eat(_ food: Grass) {
        print("eat \(food)")
    }
}
复制代码

不过在添加 associatedtype 后,Animal 协议就不能被当作独立的类型使用了。

这是因为 Swift 需要在编译时确定所有类型,这里因为 Animal 包含了一个不确定的类型,所以随着 Animal 本身类型的变化,其中的 F 将无法确定 (试想一下如果在这个函数内部调用 eat 的情形,你将无法指定 eat 参数的类型)。在一个协议加入了像是 associatedtype 或者 Self 的约束后,它将只能被用为泛型约束,而不能作为独立类型的占位使用,也失去了动态派发的特性。也就是说,这种情况下,我们需要将函数改写为泛型:

func isDangerous<T: Animal>(animal: T) -> Bool {
    if animal is Tiger {
        return true
    } else {
        return false
    }
}

isDangerous(animal: Tiger()) // true
isDangerous(animal: Sheep()) // false
复制代码

Associated Object 关联对象

得益于 Objective-C 的运行时和 Key-Value Coding 的特性,我们可以在运行时向一个对象添加值存储。而在使用 Category 扩展现有的类的功能的时候,直接添加实例变量这种行为是不被允许的,这时候一般就使用 property 配合 Associated Object 的方式,将一个对象 “关联” 到已有的要扩展的对象上。进行关联后,在对这个目标对象访问的时候,从外界看来,就似乎是直接在通过属性访问对象的实例变量一样,可以非常方便。

在 Swift 中这样的方法依旧有效,只不过在写法上可能有些不同。

// MyClass.swift
class MyClass {
}

// MyClassExtension.swift
private var key: Void?

extension MyClass {
    var title: String? {
        get {
            return objc_getAssociatedObject(self, &key) as? String
        }

        set {
            objc_setAssociatedObject(self,
                &key, newValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}


// 测试
func printTitle(_ input: MyClass) {
    if let title = input.title {
        print("Title: \(title)")
    } else {
    		print("没有设置")
    }
}

let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)
复制代码

初始化方法顺序

与 Objective-C 不同,Swift 的初始化方法需要保证类型的所有属性都被初始化。所以初始化方法的调用顺序就很有讲究。在某个类的子类中,初始化方法里语句的顺序并不是随意的,我们需要保证在当前子类实例的成员初始化完成后才能调用父类的初始化方法。

协议和类方法中的 Self

protocol IntervalType {
    func clamp(intervalToClamp: Self) -> Self
}
复制代码

上面这个 IntervalType 的协议定义了一个方法,接受实现该协议的自身的类型,并返回一个同样的类型。

这么定义是因为协议其实本身是没有自己的上下文类型信息的,在声明协议的时候,我们并不知道最后究竟会是什么样的类型来实现这个协议,Swift 中也不能在协议中定义泛型进行限制。而在声明协议时,我们希望在协议中使用的类型就是实现这个协议本身的类型的话,就需要使用 Self 进行指代。

在这种情况下,Self 不仅指代的是实现该协议的类型本身,也包括了这个类型的子类。

实际实现的时候需要稍微转一个弯,我们需要通过一个和当前上下文无关的,又能够指代当前类型的方式进行初始化。我们可以使用 type(of:) 来获取对象类型,通过它也可以进一步进行初始化,以保证方法与当前类型上下文无关

protocol Copyable {
    func copy() -> Self
}
func copy() -> Self {
    let result = type(of: self).init()
    result.num = num
    return result
}
复制代码

单单是这样还是无法通过编译,编译器提示我们如果想要构建一个 Self 类型的对象的话,需要有 required 关键字修饰的初始化方法,这是因为 Swift 必须保证当前类和其子类都能响应这个 init 方法。另一个解决的方案是在当前类类的声明前添加 final 关键字,告诉编译器我们不再会有子类来继承这个类型

class MyClass: Copyable {

    var num = 1

    func copy() -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
    }

    required init() {

    }
}
复制代码

单例

class MyManager  {
    static let shared = MyManager()
    private init() {}
}
复制代码

这种写法不仅简洁,而且保证了单例的独一无二。在初始化类变量的时候,Apple 将会把这个初始化包装在一次 swift_once_block_invoke 中,以保证它的唯一性。不仅如此,对于所有的全局变量,Apple 都会在底层使用这个类似 dispatch_once 的方式来确保只以 lazy 的方式初始化一次。

dispatch_once 在 swift3 被移除了

另外,我们在这个类型中加入了一个私有的初始化方法,来覆盖默认的公开初始化方法,这让项目中的其他地方不能够通过 init 来生成自己的 MyManager 实例,也保证了类型单例的唯一性。如果你需要的是类似 default 的形式的单例 (也就是说这个类的使用者可以创建自己的实例) 的话,可以去掉这个私有的 init 方法。

swift 单例的实现,是如何保证线程安全的?

let 定义的属性本身就是线程安全的,同时 static 定义的是一个 class constant,拥有全局作用域和懒加载特性。

String 和 NSString 的区别和联系

String 和 NSString 在使用的时候是可以无缝转换的,但是也是有一定区别的,本质上,String 是值类型,NSString 是引用类型。

在 Swift 中尽可能的话还是使用原生的 String 类型。

  • 因为现在所有的 Cocoa 框架都能接收和也能返回 String 类型,所以没有必要特地转换
  • “因为在 Swift 中 String 是 struct,相比起 NSObject 的 NSString 类来说,更切合字符串的 "不变" 这一特性。通过配合常量赋值 (let) ,这种不变性在多线程编程时就非常重要了,它从原理上将程序员从内存访问和操作顺序的担忧中解放出来。另外,在不触及 NSString 特有操作和动态特性的时候,使用 String 的方法,在性能上也会有所提升。
  • 因为 String 实现了 Collection 这样的协议,因此有些 Swift 的语法特性只有 String 才能使用,而 NSString 是没有的。
  • String 字符串之间的拼接比 NSString 方便
  • String 独有的字符串插值功能

使用 NSString 的情况:

  • 通过 Range 截取字符串的时候,使用 NSString 更加方便一点

条件编译

#if <condition>

#elseif <condition>

#else

#endif
复制代码

这几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码

方法可选参数
os()macOS, iOS, tvOS, watchOS, Linux
arch()x84_64, arm, arm64, i386
swift()>= 某个版本

另外对于 arch() 的参数需要说明的是 arm 和 arm64 两项分别对应 32 位 CPU 和 64 位 CPU 的真机情况,而对于模拟器,相应地 32 位设备的模拟器和 64 位设备的模拟器所对应的分别是 i386 和 x86_64,它们也是需要分开对待的。

另一种方式是对自定义的符号进行条件编译,比如我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法:

@IBAction func someButtonPressed(sender: AnyObject!) {
    #if FREE_VERSION
        // 弹出购买提示,导航至商店等
    #else
        // 实际功能
    #endif
}
复制代码

在这里我们用 FREE_VERSION 这个编译符号来代表免费版本。为了使之有效,我们需要在项目的编译选项中进行设置,在项目的 Build Settings 中,找到 Swift Compiler - Custom Flags,并在其中的 Other Swift Flags 加上 -D FREE_VERSION 就可以了。

GCD延时调用

import Foundation

typealias Task = (_ cancel : Bool) -> Void

func delay(_ time: TimeInterval, task: @escaping ()->()) ->  Task? {

    func dispatch_later(block: @escaping ()->()) {
        let t = DispatchTime.now() + time
        DispatchQueue.main.asyncAfter(deadline: t, execute: block)
    }

    var closure: (()->Void)? = task
    var result: Task?

    let delayedClosure: Task = {
        cancel in
        if let internalClosure = closure {
            if (cancel == false) {
                DispatchQueue.main.async(execute: internalClosure)
            }
        }
        closure = nil
        result = nil
    }

    result = delayedClosure

    dispatch_later {
        if let delayedClosure = result {
            delayedClosure(false)
        }
    }

    return result;
}

func cancel(_ task: Task?) {
    task?(true)
}

let task = delay(5) { print("拨打 110") }

// 仔细想一想..
// 还是取消为妙..
cancel(task)
复制代码

KeyPath 和 KVO

在 Swift 中我们也是可以使用 KVO 的,而且在 Swift 4 中,结合 KeyPath,Apple 为我们提供了非常漂亮的一套新的 API。不过 KVO 仅限于在 NSObject 的子类中,这是可以理解的,因为 KVO 是基于 KVC (Key-Value Coding) 以及动态派发技术实现的,而这些东西都是 Objective-C 运行时的概念。另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic@objc

Swift4之前:

class MyClass: NSObject {
    @objc dynamic var date = Date()
}

private var myContext = 0

class Class: NSObject {

    var myObject: MyClass!

    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 MyClass,当前日期: \(myObject.date)")
        myObject.addObserver(self,
            forKeyPath: "date",
            options: .new,
            context: &myContext)

        delay(3) {
						self.myObject.date = Date()
        }
    }

    override func observeValue(forKeyPath keyPath: String?,
                            of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                              context: UnsafeMutableRawPointer?)
    {
        if let change = change, context == &myContext {
            if let newDate = change[.newKey] as? Date {
                print("MyClass 日期发生变化 \(newDate)")
            }
        }
    }
}

let obj = Class()
复制代码

Swift4中引入了新的 KeyPath 的表达方式,现在,对于类型 Foo 中的变量 bar: Bar,对应的 KeyPath 可以写为 \Foo.bar。在这种表达方式下,KeyPath 将通过泛型的方式带有类型信息,比如上的 KeyPath 的类型为 KeyPath<Foo, Bar>

class MyClass: NSObject {
    @objc dynamic var date = Date()
}

class AnotherClass: NSObject {
    var myObject: MyClass!
    var observation: NSKeyValueObservation?
    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 AnotherClass,当前日期: \(myObject.date)")

        observation = myObject.observe(\MyClass.date, options: [.new]) { (_, change) in
            if let newDate = change.newValue {
                print("AnotherClass 日期发生变化 \(newDate)")
            }
        }

        delay(1) { self.myObject.date = Date() }
    }
}
复制代码

相较于原来 Objective-C 方式的处理,使用 Swift 4 KeyPath 的好处显而易见

  1. 首先,设定观察和处理观察的代码被放在了一起,让代码维护难度降低很多;
  2. 其次在处理时我们得到的是类型安全的结果,而不是从字典中取值;
  3. 最后,我们不再需要使用 context 来区分是哪一个观察量发生了变化,而且使用 observation 来持有观察者,这可以让我们从麻烦的内存管理中解放出来,观察者的生命周期将随着 AnotherClass 的释放而结束。对比一下在 Class 中,我们还需要在实例完成任务时找好时机停止观察,否则将造成内存泄漏。

不过在 Swift 中使用 KVO 还是有有两个显而易见的问题

  1. 显然 Swift 的 KVO 需要依赖的东西比原来多。在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic 和 @objc 进行修饰。大多数情况下,我们想要观察的类不包含这两个修饰,并且有时候我们很可能也无法修改想要观察的类的源码。遇到这样的情况的话,一个可能可行的方案是继承这个类并且将需要观察的属性使用 dynamic 和 @objc 进行重写。

  2. 对于那些非 NSObject 的 Swift 类型怎么办。因为 Swift 类型并没有通过 KVC 进行实现,所以更不用谈什么对属性进行 KVO 了。对于 Swift 类型,语言中现在暂时还没有原生的类似 KVO 的观察机制。我们可能只能通过属性观察来实现一套自己的类似替代了。

尾递归

一般对于递归,解决栈溢出的一个好方法是采用尾递归的写法。顾名思义,尾递归就是让函数里的最后一个动作是一个函数调用的形式,这个调用的返回值将直接被当前函数返回,从而避免在栈上保存状态。这样一来程序就可以更新最后的栈帧,而不是新建一个,来避免栈溢出的发生。

func tailSum(_ n: UInt) -> UInt {
    func sumInternal(_ n: UInt, current: UInt) -> UInt {
        if n == 0 {
            return current
        } else {
            return sumInternal(n - 1, current: current + n)
        }
    }

    return sumInternal(n, current: 0)
}

tailSum(1000000)
复制代码

但是如果你在项目中直接尝试运行这段代码的话还是会报错,因为在 Debug 模式下 Swift 编译器并不会对尾递归进行优化。我们可以在 scheme 设置中将 Run 的配置从 Debug 改为 Release,这段代码就能正确运行了。

JSON 和 Codable

// jsonString
{"menu": {
    "id": "file",
    "value": "File",
    "popup": {
        "menuitem": [
            {"value": "New", "onclick": "CreateNewDoc()"},
            {"value": "Open", "onclick": "OpenDoc()"},
            {"value": "Close", "onclick": "CloseDoc()"}
        ]
    }
}}
复制代码

Swift 4 中新加入了 Codable 协议,用来处理数据的序列化和反序列化。利用内置的 JSONEncoder 和 JSONDecoder,在对象实例和 JSON 表现之间进行转换变得非常简单。要处理上面的 JSON,我们可以创建一系列对应的类型,并声明它们实现 Codable:

struct Obj: Codable {
    let menu: Menu
    struct Menu: Codable {
        let id: String
        let value: String
        let popup: Popup
    }

    struct Popup: Codable {
        let menuItem: [MenuItem]
        enum CodingKeys: String, CodingKey {
            case menuItem = "menuitem"
        }
    }

    struct MenuItem: Codable {
        let value: String
        let onClick: String

        enum CodingKeys: String, CodingKey {
            case value
            case onClick = "onclick"
        }
    }
}
复制代码

只要一个类型中所有的成员都实现了 Codable,那么这个类型也就可以自动满足 Codable 的要求。

如果 JSON 中的 key 和类型中的变量名不一致的话 (这很常见,因为 JSON 中往往使用下划线命名 key 值,而 Swift 中的命名规则一般是驼峰式),我们还需要在对应类中声明 CodingKeys 枚举,并用合适的键值覆盖对应的默认值,上例中 Popup 和 MenuItem 都属于这种情况。

属性访问控制

Swift 中由低至高提供了 private,fileprivate,internal,public 和 open 五种访问控制的权限。默认的 internal 在绝大部分时候是适用的,另外由于它是 Swift 中的默认的控制级,因此它也是最为方便的。

  • private:让代码只能在当前作用域或者同一文件中同一类型的作用域中被使用
  • fileprivate:表示代码可以在当前文件中被访问,而不做类型限定
  • public:让代码在 target 外部也可以被调用
  • open:让代码可以被继承或者重写

如果我们希望在别的 module 中能访问一个属性,同时又保持只在当前作用域可以设置的话,我们需要将 get 的访问权限提高为 public。属性的访问控制可以通过两次的访问权限指定来实现,具体来说:

public class MyClass {
    public private(set) var name: String?
}
复制代码

错误和异常处理

在 Objective-C 开发中,异常往往是由程序员的错误导致的 app 无法继续运行,比如我们向一个无法响应某个消息的 NSObject 对象发送了这个消息,会得到 NSInvalidArgumentException 的异常,并告诉我们 "unrecognized selector sent to instance";比如我们使用一个超过数组元素数量的下标来试图访问 NSArray 的元素时,会得到 NSRangeException。类似由于这样所导致的程序无法运行的问题应该在开发阶段就被全部解决,而不应当出现在实际的产品中。相对来说,由 NSError 代表的错误更多地是指那些“合理的”,在用户使用 app 中可能遇到的情况:比如登陆时用户名密码验证不匹配,或者试图从某个文件中读取数据生成 NSData 对象时发生了问题 (比如文件被意外修改了) 等等。

但是 NSError 的使用方式其实变相在鼓励开发者忽略错误,所以很多工程师在开发时为了省事和简单,就会将输入的 error 设为 nil,也就是不关心错误,导致当错误真正发生时,你几乎无从下手调试。

在 Swift 2.0 中,Apple 为这门语言引入了异常机制。现在,这类带有 NSError 指针作为参数的 API 都被改为了可以抛出异常的形式。

open func write(toFile path: String, 
    options writeOptionsMask: NSData.WritingOptions) throws

do {
    try d.write(toFile: "Hello", options: [])
} catch let error as NSError {
    print ("Error: \(error.domain)")
}
复制代码

如果你不使用 try 的话,是无法调用 write(toFile:options:) 方法的,它会产生一个编译错误,这让我们无法有意无意地忽视掉这些错误。

在上面的示例中 catch 将抛出的异常 (这里就是个 NSError) 用 let 进行了类型转换,这其实主要是针对 Cocoa 现有的 API 的,是对历史的一种妥协。

对于我们用 Swift 新写的可抛出异常的 API,我们应当抛出一个实现了 Error 协议的类型,比如 enum:

enum LoginError: Error {
    case UserNotFound, UserPasswordNotMatch
}

func login(user: String, password: String) throws {
    //users 是 [String: String],存储[用户名:密码]

    if !users.keys.contains(user) {
        throw LoginError.UserNotFound
    }

    if users[user] != password {
        throw LoginError.UserPasswordNotMatch
    }

    print("Login successfully.")
}

do {
    try login(user: "onevcat", password: "123")
} catch LoginError.UserNotFound {
    print("UserNotFound")
} catch LoginError.UserPasswordNotMatch {
    print("UserPasswordNotMatch")
}
复制代码

这样的 ErrorType 可以非常明确地指出问题所在。在调用时,catch 语句实质上是在进行模式匹配。

可以看出,在 Swift 中,我们虽然把这块内容叫做“异常”,但是实质上它更多的还是“错误”而非真正意义上的异常。

当然,Swift 现在的异常机制也并不是十全十美的。最大的问题是类型安全,不借助于文档的话,我们现在是无法从代码中直接得知所抛出的异常的类型的。比如上面的 login 方法,光看方法定义我们并不知道 LoginError 会被抛出。一个理想中的异常 API 可能应该是这样的:

func login(user: String, password: String) throws LoginError
复制代码

另一个限制是对于非同步的 API 来说,抛出异常是不可用的 -- 异常只是一个同步方法专用的处理机制

Cocoa 框架里对于异步 API 出错时,保留了原来的 Error 机制,比如很常用的 URLSession 中的 dataTask API:

func dataTask(with: URLRequest, 
    completionHandler: (Data?, URLResponse?, Error?) -> Void)
复制代码

对于异步API,一种现在比较常用的方式就是借助于 enum。枚举 (enum) 类型可以与其他的实例进行绑定,我们可以让方法返回枚举类型,然后在枚举中定义成功和错误的状态,并分别将合适的对象与枚举值进行关联:

enum Result {
    case Success(String)
    case Error(NSError)
}

func doSomethingParam(param:AnyObject) -> Result {
    //...做某些操作,成功结果放在 success 中
    if success {
        return Result.Success("成功完成")
    } else {
        let error = NSError(domain: "errorDomain", code: 1, userInfo: nil)
        return Result.Error(error)
    }
}

let result = doSomethingParam(path)

switch result {
    case let .Success(ok):
    let serverResponse = ok
case let .Error(error):
    let serverResponse = error.description
}
复制代码

在 Swift 2.0 中,我们甚至可以在 enum 中指定泛型,这样就使结果统一化了。

enum Result<T> {
    case Success(T)
    case Failure(NSError)
}
复制代码

我们只需要在返回结果时指明 T 的类型,就可以使用同样的 Result 枚举来代表不同的返回结果了。这么做可以减少代码复杂度和可能的状态,同时不是优雅地解决了类型安全的问题,可谓一举两得。

因此,在 Swift 中的错误处理,现在一般的最佳实践是对于同步 API 使用异常机制,对于异步 API 使用泛型枚举

关于 try 和 throws:

  • try!,强制执行

  • try?,尝试执行,常与 if let 搭配使用,如果你用了 try? 的话,就意味着你无视了错误的具体类型

  • 在一个可以 throw 的方法里,我们永远不应该返回一个 Optional 的值。因为结合 try? 使用的话,这个 Optional 的返回值将被再次包装一层 Optional,使用这种双重 Optional 的值非常容易产生错误,也十分让人迷惑

  • rethrows 和 throws 做的事情并没有太多不同,它们都是标记了一个方法应该抛出错误。但是 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中,来表示它既可以接受普通函数,也可以接受一个能 throw 的函数作为参数。也就是像是下面这样的方法,我们可以在外层用 rethrows 进行标注:

    func methodThrows(num: Int) throws {
        if num < 0 {
            print("Throwing!")
            throw E.Negative
        }
        print("Executed!")
    }
    
    func methodRethrows(num: Int, f: Int throws -> ()) rethrows {
        try f(num)
    }
    
    do {
        try methodRethrows(num: 1, f: methodThrows)
    } catch _ {
    
    }
    复制代码
  • 你在要 throws 另一个 throws 时,应该将前者改为 rethrows。

Log输出

在 Swift 中,编译器为我们准备了几个很有用的编译符号

符号类型描述
#fileString包含这个符号的文件的路径
#lineInt符号出现的行号
#columnInt符号出现的列
#functionString包含这个符号的方法名字

我们可以通过使用这些符号来写一个好一些的 Log 输出方法:

func printLog<T>(_ message: T,
                    file: String = #file,
                  method: String = #function,
                    line: Int = #line)
{
    #if DEBUG
    print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
    #endif
}
复制代码
文章分类
iOS
文章标签