Swift Tips:一、Swift新元素

结构体mutating

结构体自身是不可变的,因为其整体是一个值类型。如果其内的方法想修改自己的属性,需要在方法前加关键字mutating来让自身变成另一个值。

protocol的mutating

如果protocol定义了属性,又定义了方法来改变那个属性,那么所定义的方法应使用mutating修饰。之所以这么做,是因为struct和enum都可以遵循protocol,它们在实现改变属性的方法时,要求这个方法是mutating的。

for...in

for...in可以用在所有实现了Sequence协议的类型上。除了swift原生的集合类型,还可以用在我们自己的类型上。

autoclosure

可以在方法的闭包参数前使用@autoclosure,这样在调用方法时,可以只写闭包体,不需要花括号。对这样的闭包有一个条件,就是要求它是无参的。

// 定义方法,接受无参闭包
func logIfTrue( _ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("True")
    }
}
// 调用
logIfTrue(2 > 1)

操作符??的实现也用到了@autoclosure,实际上??后的表达式会转换成闭包,在需要时才会执行所写表达式。

func的参数修饰

func接受的参数,如果没有var修饰,都是let的,默认不可对其重新赋值。
如果你想让func对参数的修改影响到外面,可以使用inout修饰这个参数。

swift是一门讨厌变化的语言,所有有可能的地方,都被默认认为是不可变的,也就是用let进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。

方法嵌套

Swift的方法是一等公民,它可被当作变量或者参数来使用,甚至可以在方法内定义方法。

有时候我们某个方法过长,会把它拆出几个子方法来调用,这样一般会让被拆出的方法与本来的方法同级,然而这些新增的方法并不应该有这么高的地位,它只需要被原方法调用就足够,这时候方法嵌套机制就很能发挥作用了。

命名空间

Swift的命名空间是基于module而不是在代码中显式地指明,每个module代表了Swift中的一个命名空间。而一个module对应的就是一个target,也就是说,同一个target里的类型还是不可以同名的。
如果同时用到了分处于不同target的同名类时,可以在类名前加上module名以限定。

MyFramework.MyClass.func()

如果想在同一个target里实现命名空间的效果,还是有折衷的办法的。可以使用类型嵌套,一般做法是把类型嵌套在struct内定义,而外层的struct仅作为命名空间的存在。

struct MyNamespace {
    class MyClass {
        // ...
    }
}

虽然这两种办法确实都不是我们所习惯的命名空间,但它们还是能发挥出命名空间的作用的,至少我们不需要给类名加上各种奇怪的前缀了。

Any和AnyObjct

这两个都是Swift中的妥协产物。
AnyObject可以代表任何class类型的实例
Any可以表示任意类型,包括struc,enum,和 func

现在的Swift最主要的用途依然是使用Cocoa框架进行app开发,因此为了与Cocoa架构协作,将原来id的概念使用了一个类似功能的AnyObject来替代。

AnyObject其实是一个接口:

protocol AnyObject {
    
}

所有的class都隐式地实现了这个接口。

对变量声明为AnyObject类型时,有时会发生Cocoa类型的自动转换,例如编译器会把Swift的原生Int类型转换成Cocoa的NSNumber类型,再存入AnyObject声明的变量中。

这里好一点的做法是使用Any,它能直接保存Swift原生类型。
我们应尽量使用Swift原生类型,这对性能的提升是有帮助的。

其实使用Any和AnyObject都不是令人愉悦的事情,这都是为妥协而存在的产物。如果在我们自己代码里需要大量经常地使用这两者的话,往往意味着代码可能在结构和设计上存在问题。最好避免依赖和使用这两者,应尝试明确指出类型。

typealias和泛型接口

typealias是有用来为已经存在的类型重新定义名字的,通过命名,可以使代码变得更加清晰。
Swift中是没有泛型接口的,但是可以使用typealias实现泛型的功能。
比如系统的IteratorProtocol接口

protocol IteratorProtocol {
    associatedtype Element
    public mutating func next() -> Self.Element?
}

然后实现此接口时,会要求实现对应的associatedtype,此时可使用typealias来实现。

class MyIterator: IteratorProtocol {
    typealias Element = Int
    
    mutating func next() -> Int? {
        return ...
    }
}

可变参数函数

在Swift中是很容易声明可变参数的函数的,只需要在可变参数类型后面加上...就可以了。
比如下面这个累加函数:

func sum(input: Int...) -> Int {
    return input.reduce(0, +)
}

input在函数体内部将被作为数组[Int]来使用。

在其他很多语言中,因为编译器和语言自身语法特性的限制,可变参数往往只能作为方法中的最后一个参数。这个限制在Swift中是不存在的,因为我们会对方法的参数进行命名,所以可以随意地放置位置。

然而限制还是有的:
在同一个方法中只能有一个参数是可变的;
可变参数都必须是同一种类型的。

初始化方法顺序

子类初始化顺序是:

  1. 给子类自己的所有成员变量初始化
  2. 调用父类相应的初始化方法
  3. 操作父类资源
class Cat {
    var name: String
    init() {
        name = "cat"
        print("cat init")
    }
}

class Tiger: Cat {
    let power: Int
    override init() {
        power = 10
        // 这里虽然没有显式写super.init()
        // 但由于是初始化的最后了,Swift可以替我们自动完成
    }
}

上面代码的Tiger类并没有第3步,这时可以把第2步代码省去,Swift自动帮我们完成。

Designated,Convenience 和 Required

Swift的初始化机制相比OC复杂得多,不妨想想Swift这么设计,是要达到一种怎样的目的。

其实就是安全。在OC中,init方法非常不安全:

  1. 没有人能保证init只被调用一次
  2. 没有人能保证在初始化方法调用以后,实例的各个变量都完成了初始化
  3. 如果在初始化里使用属性进行设置的话,可能会造成各种问题,虽然Apple明确说明不应该在init中使用属性来访问,但编译器没有做强制,实际还会有很多开发者犯这种错误。

所以Swift有超级严格的初始化方法。

  1. designated修饰的初始化方法确保所有非Optional实例变量都被赋值初始化。
  2. 子类会强制(显式或隐式)调用super的designated初始化方法。

convenience初始化方法是“二等公民”,只作为补充和提供使用上的方便。

  1. convenience方法是不能被子类重写或者从子为中以super的方式被调用的。
  2. 如果子类想继承父类的convenience方法,需要重写这个方法所需要的init方法。

原则:

  1. 初始化路径必须保证对象完全初始化,这可以通过调用本类型的designated初始化方法来得到保证;
  2. 子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。

对初始化方法添加required关键字,在某些时候非常有必要:

  1. 保证依赖于某个designated初始化方法的convenience一直可以被使用
  2. 给convenience方法加上required,以要求子类不要直接使用父类的convenience。

protocol组合

可以把多个protocol加起来合成一个新的协议,当然新的协议可以给个名字重新定义出来:

protocol A {
    func echo() -> Int
}

protocol B {
    func echo() -> String
}

protocol AB: A, B {}

也可以匿名创建,然后用typealias取别名,这种做法优于上面的空协议做法:

typealias AB = A & B

如果同时实现的两个协议中有同名方法,在调用时会有点尴尬,编译器也会直接报错,这时需要把实例转成其中一个协议类型,再调用。

class ABClass: AB {
    
    func echo() -> Int {
        return 1
    }
    
    func echo() -> String {
        return "1"
    }
}
// 调用
let ab = ABClass()
let intValue = (ab as A).echo()
let stringValue = (ab as B).echo()

多类型和容器

Swift的原生容器有三种,分别是Array、Dictionary和Set。
它们都是泛型的,也就是说在集合中只能放同一种类型的元素。

容器的泛型除了可以指定实体类型外,还可以指定为协议protocol
假如需要在容器中放不同类型的元素时,你应该找到它们的共同点,把共同点抽象为协议,让不同类型遵循协议,然后声明容器的泛型为这个共同的遵循的协议。

除了新建一个协议把不同类型的共同点抽象出来以外,还可以使用枚举enum,借助swift的枚举可以带有值的特点,把对象包起来再存进容器。

enum IntOrString {
    case intValue(Int)
    case stringValue(String)
}
let mixed = [IntOrString.intValue(1),
            IntOrString.stringValue("two"),
            IntOrString.intValue(3)]

模式匹配

在Swift中可以用~=符号来进行模式匹配,目前只支持最简单的相等和范围匹配。
~=的左边写模式,右边写被检验值。

// 范围匹配
let isInRange = 3...8 ~= 6
print("isInRange:\(isInRange)")

// 相同匹配
let isEqual = "Swift" ~= "swift"
print("isEqual:\(isEqual)")

// isInRange:true
// isEqual:false

Swift的switch就是使用了~=操作符进行模式匹配

AnyClass,元类型和.self

AnyClass是这么定义的:

typealias AnyClass = Swift.AnyObject.Type

通过AnyObject.Type这种方式所得到的是一个元类型(Meta)。
.Type表示取某个类型的元类型。
.self如果加在类型后面,表示取这个类型本身,如果加在实例后面,表示取这个实例本身。

class A {
    // ...
}
let typeA: A.Type = A.self

如果想取protocol的元类型,可以使用.Protocol来获取:

protocol P {
    // ..
}
let typeP: P.Protocol = P.self

接口和类方法中的Self

首字母大写的Self表示本类型,一般用于声明方法。除了指代本类型,还同时指代本类型的子类。
假如要实现一个复制协议,可以这样写:

protocol Copyable {
    func copy() -> Self
}

class MyClass {
    var num = 1
    
    required init() {
    }
    
    func copy() -> Self {
        let myType = type(of: self)
        let obj = myType.init()
        obj.num = num
        return obj
    }
}

class MySubClass: MyClass {

}
// 调用
let object = MyClass()
object.num = 100
        
let newObject = object.copy()
object.num = 1
        
print(object.num)
print(newObject.num)
        
let subObject = MySubClass()
subObject.num = 200
        
let newSubObject = subObject.copy()
subObject.num = 2
        
print(subObject.num)
print(newSubObject.num)

// 打印结果:
1
100
2
200

动态类型和多方法

Swift不能根据对象在动态时的类型进行合适的重载方法调用。
它在编译阶段就已经确定了最终的方法调用。
比如:

func printPet(pet: Pet) {
    print("Pet")
}

func printPet(pet: Cat) {
    print("Cat")
}

func printPet(pet: Dog) {
    print("Pet")
}

func printThem(pet: Pet, cat: Cat) {
    printPet(pet: pet)
    printPet(pet: cat)
}
// 调用
printThem(pet: Dog(), cat: Cat())
// 打印结果:
Pet
Cat

printThem方法的pet参数声明了接收一个Pet对象,那么其内的printPet方法就实实在在地调用了入参为Pet的方法,而不管实际运行时是否是Pet的子类。

属性观察

Swift提供了两个属性观察方法,分别是willSetdidSet
willSet中可以通过newValue获取即将要设定的值。
didSet中可以通过oldValue获取旧值。
willSetdidSet是存储属性特有的,而getset计算属性特有的,它们不能同时存在。
子类可以重写父类的计算属性为存储属性,从而实现willSet或didSet。

lazy方法

Swift标准库有一组lazy方法,或者说是计算属性,可以把mapfilter这类接受闭包运行的方法实现延时运行。对于运行代价很大的情况,它可以起到不小的性能提升作用。
本来不使用lazy是这样的:

let data = 1...3
let result = data.map { (i) -> Int in
    print("正在处理\(i)")
    return i * 2
}
print("准备访问结果")
        
for i in result {
    print("操作后的结果为\(i)")
}
print("操作完毕")

打印结果为:

正在处理1
正在处理2
正在处理3
准备访问结果
操作后的结果为2
操作后的结果为4
操作后的结果为6
操作完毕

在没有lazy时,map会按顺序直接运行闭包。
下面先取其lazy结果,后再map:

let data = 1...3
let result = data.lazy.map { (i) -> Int in
    print("正在处理\(i)")
    return i * 2
}
print("准备访问结果")
        
for i in result.reversed() {
    print("操作后的结果为\(i)")
}
print("操作完毕")

打印结果为:

准备访问结果
正在处理3
操作后的结果为6
正在处理2
操作后的结果为4
正在处理1
操作后的结果为2
操作完毕

lazy后,可以在实际访问序列元素时,才执行闭包运算。

Reflection和Mirror

现在的Swift虽然在反射方面相比Objective-C要弱得多,但还是存在一些和反射相关的内容的。
可以通过一个Mirror来获取某元素的一些信息,比如对象的所有属性。

struct Person {
    let name: String
    let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing)
print("xiamMing是\(r.displayStyle!)")

print("属性个数:\(r.children.count)")
for child in r.children {
    print("属性名:\(child.label!), 值:\(child.value)")
}

打印结果:

xiamMing是struct
属性个数:2
属性名:name, 值:XiaoMing
属性名:age, 值:16

也可以用dump打印其镜像信息:

dump(xiaoMing)
▿ Person
  - name: "XiaoMing"
  - age: 16

Optional Map

Optional实际上是一个枚举,?只是它的语法糖。

enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

现在假设我们有个需求,要将某个Int?乘2。一个合理的策略是如果这个Int?有值的话,就取出值进行乘2操作,如果是nil的话就直接将nil赋给结果。我们可以写出如下代码:

let num: Int? = 3
var result: Int?
if let realNum = num {
    result = realNum * 2
} else {
    result = nil
}

其实我们有更优雅简洁的方式,那就是使用Optional的map。它内部定义了map方法,当然也还有flatMap

public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

这样我们可以很方便地对一个Optional值做变化和操作,而不必手动解包。

let num: Int? = 3
let result = num.map {
    $0 * 2
}

Protocol Extension

对Protocol进行Extension扩展,可以给Protocol声明的方法做默认实现。
但是,在protocol的extension中实现了接口里没有定义的方法时,就比较有趣了。

extension PA {
    func method1() {
        print("protocol method1")
    }
    func method2() {
        print("protocol method2")
    }
}

struct SA: PA {
    func method1() {
        print("struct method1")
    }
    func method2() {
        print("struct method2")
    }
}
// 调用
let s = SA()
s.method1()
s.method2()
print("转换为Protocol后:")
let p = s as PA
p.method1()
p.method2()

打印结果:

struct method1
struct method2
转换为Protocol后:
struct method1
protocol method2

SA实现了PA协议,当变量作为struct使用时,编译器选择了调用struct的方法。
当变量作为protocol使用时,对于method1,编译器知道其协议实现者必定实现了method1方法,故使用动态派发,在运行时调用实现类型的方法;对于method2,protocol并没有声明这个方法,编译器不确定协议实现者有没有实现这个方法,所以它直接确定了调用extension的method2,而不管协议实现者。

where和模式匹配

where关键字可用在switch的case语句中,对条件进行限定:

let name = ["王小二","张三","李四","王二小"]
name.forEach {
    switch $0 {
    case let x where x.hasPrefix("王"):
        print("\(x)是笔者本家")
    default:
        print("你好,\($0)")
    }
}

但如果想在if语句中对条件进行限定,现在的swift版本要用逗号,代替where

let num: [Int?] = [48, 61, nil]
num.forEach {
    if let score = $0, score >= 60 {
        print("及格啦 - \(score)")
    } else {
        print("不及格 :(")
    }
}

where还广泛用在Protocol中,比如限定associatedtype:

public protocol Sequence {
    associatedtype Element where Self.Element == Self.Iterator.Element
}

以及限定扩展的适用条件:

extension Sequence where Self.Element : Comparable {
    // ...
}

参考

  • Swifter - 100个 Swift 必备 Tips
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容