结构体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中是不存在的,因为我们会对方法的参数进行命名,所以可以随意地放置位置。
然而限制还是有的:
在同一个方法中只能有一个参数是可变的;
可变参数都必须是同一种类型的。
初始化方法顺序
子类初始化顺序是:
- 给子类自己的所有成员变量初始化
- 调用父类相应的初始化方法
- 操作父类资源
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
方法非常不安全:
- 没有人能保证
init
只被调用一次 - 没有人能保证在初始化方法调用以后,实例的各个变量都完成了初始化
- 如果在初始化里使用属性进行设置的话,可能会造成各种问题,虽然Apple明确说明不应该在init中使用属性来访问,但编译器没有做强制,实际还会有很多开发者犯这种错误。
所以Swift有超级严格的初始化方法。
- designated修饰的初始化方法确保所有非Optional实例变量都被赋值初始化。
- 子类会强制(显式或隐式)调用super的designated初始化方法。
convenience
初始化方法是“二等公民”,只作为补充和提供使用上的方便。
-
convenience
方法是不能被子类重写或者从子为中以super
的方式被调用的。 - 如果子类想继承父类的
convenience
方法,需要重写这个方法所需要的init
方法。
原则:
- 初始化路径必须保证对象完全初始化,这可以通过调用本类型的designated初始化方法来得到保证;
- 子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。
对初始化方法添加required
关键字,在某些时候非常有必要:
- 保证依赖于某个designated初始化方法的convenience一直可以被使用
- 给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提供了两个属性观察方法,分别是willSet
和didSet
。
在willSet
中可以通过newValue
获取即将要设定的值。
在didSet
中可以通过oldValue
获取旧值。
willSet
和didSet
是存储属性特有的,而get
和set
计算属性特有的,它们不能同时存在。
子类可以重写父类的计算属性为存储属性,从而实现willSet或didSet。
lazy方法
Swift标准库有一组lazy
方法,或者说是计算属性,可以把map
和filter
这类接受闭包运行的方法实现延时运行。对于运行代价很大的情况,它可以起到不小的性能提升作用。
本来不使用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