6. 闭包表达式与闭包
闭包表达式(Closure Expression)
闭包表达式是一种在简短行内就能写完闭包的语法
也就是,闭包表达式,只是一种简洁、快速实现闭包的语法
Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法。
常见的优化包括:
- 利用上下文推断形式参数和返回值的类型;
- 单表达式的闭包可以隐式返回;
- 简写实际参数名;
- 尾随闭包语法。
函数的定义
- 可以通过func定义一个函数
func sum(_ v1: Int, _ v2: Int) -> Int
{v1 + v2
}
可以理解,就是前面说的,函数也是闭包
闭包就是可以捕获上下文中的常量或变量
- 也可以通过闭包表达式定义一个函数
闭包表达式有如下的一般形式:
{ (parameters) -> (return type) instatements
}
等价于:
{(参数列表) -> 返回值类型 in函数体代码
}
闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。
可变形式参数也能使用,但需要在形式参数列表的最后面使用。
元组也可被用来作为形式参数和返回类型。
闭包的函数整体部分由关键字 in 导入,这个关键字表示:
闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始。
通常我们说的闭包更多指的是闭包表达式,也就是没有函数名称的代码块,因此也叫做匿名闭包。
var fn = {(v1: Int, v2: Int) -> Int inreturn v1 + v2
}
调用:fn(10, 20)或者
{(v1: Int, v2: Int) -> Int inreturn v1 + v2
}(10, 20)
闭包表达式的简写
函数定义:
func exec(v1: Int, v2: Int, fn:(Int, Int) -> Int){print(fn(v1, v2))
}调用方法一:
exec(v1: 10, v2: 20, fn:{(v1: Int, v2: Int) -> Int inreturn v1 + v2
})
由于类型可以被推断出来,因此,类型可以不写。
因为类型可以不写,返回值类型不写,那么->也可以省略
简写为:
调用方法二:
exec(v1: 10, v2: 20, fn:{v1, v2 inreturn v1 + v2
})
单个表达式闭包能够通过从它们的声明中删掉 return 关键字来隐式返回它们单个表达式的结果
调用方法三:
exec(v1: 10, v2: 20, fn:{v1, v2 inv1 + v2
})
在闭包里面,可以使用 $0、$1分别代表闭包表达式里面第1个参数和第2个参数,因此,可以简化为:
调用方法四:
exec(v1: 10, v2: 20, fn:{ $0 + $1 })
其实这次是省略了
v1, v2 in
。里面没有了参数列表,也就不需要in做区分,因此,前面可以省略
更简化的方法:
exec(v1: 10, v2: 20, fn: + )
简直不是人。。。
其他优化例子
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
reversedNames = names.sorted(by: { $0 > $1 } )
这里, $0 和 $1 分别是闭包的第一个和第二个 String 实际参数。
尾随闭包
如果函数的最后一个参数为闭包,则该闭包也被称为尾随闭包
如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数且闭包表达式很长,使用尾随闭包将增强函数的可读性。
尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式,但它仍然是这个函数的实际参数。
func someFunctionThatTakesAClosure(closure:() -> Void){}someFunctionThatTakesAClosure({})someFunctionThatTakesAClosure() {}
还是上面的例子:
可简写为:
reversedNames = names.sorted() { $0 > $1 }
如果闭包表达式作为函数的唯一实际参数传入,而你又使用了尾随闭包的语法,那你就不需要在函数名后边写圆括号了:
reversedNames = names.sorted { $0 > $1 }
真是简写的没法了
闭包(Closure)
定义: 一个函数和它所捕获的变量\常量环境组合起来,称为闭包
函数的定义有两种方式:func和闭包表达式
- 一个函数,一般指的是:定义在函数内部的函数(嵌套函数)
- 一般它捕获的是外层函数的局部变量\常量
//定义Fn式一个函数:参数Int,返回值Int
typealias Fn = (Int) -> Int//定义一个函数:函数名:getFn,参数:空,返回值类型Fn
func getFn() -> Fn{//局部变量var num = 0//内部方法func plus(_ i: Int) -> Int {num += ireturn num}//getFn函数的返回值return plus
}//返回的plus和num形成了闭包(也就是plus+num就称为闭包)var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
打印结果:
1
3
6
10
按说var fn = getFn()
执行完毕后,局部变量num就被释放了。后续fn(1)此时再使用num,应该报错
但,其实是没错的,因为num已经被存储在堆空间了,因此没有被释放
并且,四次调用,访问的都是同一块堆上的内存空间num,因此,num的值被保留下来了(可以连加)
问:为啥num被存储到堆上面呢?
因为,返回出去的函数plus,里面用到了上层函数(getFn)的局部变量,就会分配堆空间,捕获该局部变量
并且,调用一次getFn()函数,就会分配一个单独的堆空间去存储num
是将num的值0,存储在堆空间。栈空间的num=0已经被回收了
var fn1 = getFn()
var fn2 = getFn()
print(fn1(1))
print(fn2(2))
print(fn1(3))
print(fn2(4))
打印:
1
2
4
6
可以把闭包想象成是一个类的实例对象
- 内存在堆空间
- 捕获的局部变量\常量就是对象的成员(存储属性)
- 组成闭包的函数就是类内部定义的方法
//类
class Closure{var num = 0func plus(_ i: Int) -> Int {num += ireturn num}
}var cs1 = Closure()
var cs2 = Closure()
print(cs1.plus(1))
print(cs1.plus(2))
print(cs2.plus(3))
print(cs2.plus(4))
打印:
1
3
3
7
闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”
全局和内嵌函数,实际上是特殊的闭包。
闭包符合如下三种形式中的一种:
- 全局函数是一个有名字但不会捕获任何值的闭包;(全局函数是一个闭包)
- 内嵌函数是一个有名字且能从其上层函数捕获值的闭包;(内嵌函数也是一个闭包)
- 闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包。
捕获值
一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。
函数和闭包都是引用类型
无论你什么时候赋值一个函数或者闭包给常量或者变量,你实际上都是将常量和变量设置为对函数和闭包的引用
自动闭包
有一个如下的比较函数:如果v1>0,则返回v1,否则返回v2
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {return v1 > 0 ? v1 : v2
}print(getFirstPositive(1, 2))
print(getFirstPositive(-1, 2))
打印:
1
2
当v2传入的是一个函数(参数为空,返回值为Int)的时候,即使v1>0,第二个参数也被调用了一次
func getNumber() -> Int {let a = 10let b = 11print("----")return a + b
}
print(getFirstPositive(1, getNumber()))
打印:
----
1
已经判断出v1 > 0了,其实后面的函数没必要执行
为了提高性能,可以将函数getFirstPositive的第二个参数修改为函数:
func getFirstPositive2(_ v1: Int, _ v2: () -> Int) -> Int {return v1 > 0 ? v1 : v2()
}print(getFirstPositive2(1, {print("1111")return 10
}))
print(getFirstPositive2(-1, {print("22222")return 10
}))
打印:
1
22222
10
可以看出,当v1>0的时候,不会执行第二个参数里面的函数
调用的时候,可以简写调用:
print(getFirstPositive2(10, {20}))
print(getFirstPositive2(-10){20})
打印:
10
20
看起来不易读
因此,swift提供了 自动闭包
func getFirstPositive3(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {return v1 > 0 ? v1 : v2()
}print(getFirstPositive3(1, 20))
print(getFirstPositive3(-1, 10))
打印:
1
10
如上所示,调用的时候,第二个参数闭包,传入的是一个Int值
其实,内部是将第二个参数20,修改为了{20}
语法糖
关于@autoclosure的注意点:
- @autoclosure仅支持
() -> T
格式的参数(无参,有返回值) - @autoclosure并非只支持最后一个参数
- 空合并运算符
??
使用了@autoclosure技术 - 有@autoclosure与无@autoclosure,也可以构成函数的重载
逃逸闭包
在 Swift 中,闭包是引用类型,当它被作为一个函数参数传递给一个函数时,这个函数会持有这个闭包的引用。大多数情况下,函数会立即执行闭包并返回,也就是说闭包的生命周期与函数的生命周期一致,我们称这样的闭包为非逃逸闭包。
但有一些情形下,函数在返回之后,闭包仍然被保留而未被执行,例如将闭包储存为函数外部的属性,或者在另外一个异步执行的闭包中被调用,我们称这样的闭包为逃逸闭包。
逃逸闭包必须在闭包参数名前标注 @escaping
关键字。这在使用闭包时会使你更清晰地理解闭包是如何和函数进行交互的。
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {someArray.append(completionHandler)
}
在 this example example中, completionHandler
闭包参数有 @escaping
注解,意味着这个闭包是可逃逸的。因为闭包被加入到 someArray
数组中,并不会立即被执行,而是会在函数返回之后的某个时刻执行。
逃逸闭包在某些场景下非常有用,比如异步调用,延时调用,还有存储为全局变量或类的实例变量等场景。不过需要注意的一点是,由于逃逸闭包的生命周期可能超过函数本身,可能会引起 strong reference cycle 强引用环,不恰当的使用可能导致内存问题。为了解决这个问题,你需要在闭包中显式地使用 [unowned self]
或者 [weak self]
来避免强引用环。
当闭包作为一个实际参数传递给一个函数,并且在函数返回后才被调用的时候,我们就说这个闭包从函数中逃逸了
在 Swift 中,我们需要在参数前加上 @escaping 关键字声明闭包是逃逸的。
如常见的异步操作就需要使用逃逸闭包。
swift中闭包与oc中的block有什么区别与相同点?
闭包(Closure)在 Swift 中,与 Objective-C 中的 blocks 形式上有相似处,但在实现、语法以及使用上有很多不同之处。
相似点:
-
匿名函数块:都是一种能捕获和存储上下文环境的匿名函数块。可以当作参数在函数之间传递,并且可以在需要的时候调用。
-
变量捕获:两者都能在其内部访问和修改外部变量。
区别:
- 语法:Swift 的闭包语法更为简洁清晰,通过对参数和返回类型的尾随闭包语法,类型推断等特性,使得闭包的定义及使用变得更为简洁,提高了代码的可读性。
Objective-C 的 block 语法要求声明返回类型,而 Swift 的闭包则不需要声明(但可以声明,这取决于具体用途)。
例如,在 Swift 中,你可以这样声明一个闭包:
let swiftClosure = { (number: Int) -> String inreturn "Swift closure: \(number)"
}
在 Objective-C 中,你需要这样声明一个 block:
NSString *(^objcBlock)(NSInteger) = ^(NSInteger number) {return [NSString stringWithFormat:@"Objective-C block: %ld", number];
};
-
内存管理:Objective-C 的 block 在捕获外部变量时需要注意 __block 、 __weak 标识符的使用,否则很容易造成循环引用。而 Swift 的闭包提供了 [unowned self] 和 [weak self] 语法来避免循环引用。
-
可访问性:Swift 的闭包能访问其作用域内的所有常量和变量,这包括其他函数中的参数和变量,这点在 Objective-C 的 block 中不完全支持。
-
可修改可逃逸性:在 Swift 中,闭包默认为不可逃逸,必要时可以使用
@escaping
关键字修改。Objective-C 的 block 默认为可逃逸,而且不能修改。 -
运行时处理:Objective-C 的 blocks 在运行时声明,Swift 的闭包在编译时声明。
总结来说,虽然 Swift 中的闭包和 Objective-C 中的 blocks 在面向函数编程的功能上有诸多相似之处,但在语法清晰度、内存管理、作用域访问性、可逃逸性等方面,Swift 的闭包具有较大优势。这些优点使得闭包适应更多的编程场景,提高了代码的可读性和可维护性。
8. 属性
Swift中,跟实例相关的属性可以分为两大类:存储属性、计算属性
存储属性(Stored Property)
类似于成员变量
存储在实例的内存中
结构体、类可以定义存储属性
枚举不可以定义存储属性
在创建结构体或类的实例时,必须为所有的存储属性设置一个合适的初值
可以在初始化器里设置,也可以为属性分配一个默认值
struct Circle{//存储属性var radius: Double//计算属性var diameter: Double{set{radius = newValue / 2}get{radius * 2}}
}var circle = Circle(radius: 5)
print(circle.radius)//5
print(circle.diameter)//10
//只需要设置radius的值,就可以通过计算属性中get方法确定diameter的值circle.diameter = 12
print(circle.radius)//6
print(circle.diameter)//12
//通过修改计算属性diameter的set方法,修改了radius的方法
对存储属性的懒加载
lazy只能用于var变量,不能用于let常量
lazy的两种用法:
方法一:
lazy var str: String = "Hello"
方法二:使用闭包
lazy var names: NSArray = {let names = NSArray()print("只在首次访问输出")return names
}()
疑问:为何使用闭包可以做到懒加载?
计算属性(Computed Property)
计算属性不是直接存储值,而是提供get和set方法
get:用来取值,封装取值的过程
set:用来设值,封装设值得过程
计算属性是用来间接修改其他属性值的
本质就是方法(函数)
不占用实例的内存
结构体、枚举、类都可以定义计算属性
上例中,set方法传入的新值默认叫做newValue
也可以自定义:
set(newName){radius = newName / 2
}
只读计算属性:只有get,没有set
struct Circle{//存储属性var radius: Double//计算属性var diameter: Double{get{radius * 2}}
}可以简写:struct Circle{//存储属性var radius: Double//计算属性var diameter: Double{radius * 2}
}
定义计算属性只能使用var,不能使用let
原因是,计算属性的值可能会发生变化(即使是只读计算属性)
属性监听器
计算属性本身就有监听,不需要考虑为其添加监听的事
对于存储属性,可以通过willSet和didSet来监听属性的改变
举个例子:
需要注意的是: willSet和didSet在初始化过程中是不会被调用的
也就是,在上面例子中,第11行初始化的时候不会调用willSet和didSet
9. 方法(Method)
定义在类、结构体、枚举内部的函数,被称为方法
方法又分为:实例方法和类型方法
- 实例方法(Instance Methods):通过实例调用
- 类型方法(Class Methods):通过类型调用。用
static
或class
关键字定义
在类型方法中调用self,self就代表类
在实例方法中调用self,self就代表实例
下标(subscrip)
使用subscrip
可以给任意类型(枚举、结构体、类)增加下标功能
subscrip的语法类似实例方法、计算属性,其本质就是方法(函数)
举一个下标的例子:
class Point {var x = 0.0, y = 0.0//类似方法定义,只是没有func关键字,没有方法名subscript(index: Int) -> Double {//类似计算属性set {if(index == 0){x = newValue}else if(index == 1){y = newValue}}get {if index == 0{return x}else if(index == 1){return y}return 0}}
}var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x)
print(p.y)
print(p[0])
print(p[1])
打印:
11.1
22.2
11.1
22.2
使用subscrip后,就可以直接对对象像数组一样操作,取p[0]、p[1]了
subscrip定义的返回值类型,决定了get方法的返回值类型,也决定了newValue的值
首先,返回值类型与get出去的类型,肯定是保持一致的。
subscrip可以没有set方法,但是必须有get方法
取值嘛
subscrip如果只有get方法,则get可以省略(语法糖)
上述下标是对象方法
下标也可以写成类方法:
class Sum {static subscript(v1: Int, v2: Int) -> Int{return v1 + v2}
}
//下标访问,使用[],传两个参数,用,隔开即可
print(Sum[10, 20])//30
在 Swift 中,静态下标是一种语法糖,它让我们可以用中括号 [] 访问或设置类型的特定值,而不需要调用一个方法或访问一个属性。
继承(Inheritance)
-
值类型不支持继承,只有类支持继承
-
在oc中,NSObject、NSProxy是基类,其余都继承NSObject
-
在swift中,只要是不继承其他类的,都称为基类
-
swift是单继承
有继承就有重写:
重写
子类可以重写父类的:下标、方法、属性,重写必须调用override
方法重写很正常,OC中常用的
下标的本质就是方法,因此也可以重写
属性,有计算属性,本质也是方法,因此,也可以重写
子类重写父类的方法,当使用子类调用该方法的时候,调用子类的方法
如果在执行子类方法的同时,还想让父类方法也执行,那么,需要调用super.methd()
实例方法、下标重写
class Animal {func speak(){print("Animal speak")}subscript(index: Int) -> Int {return index}
}class Cat: Animal {override func speak() {super.speak()print("Cat speak")}//加overrideoverride subscript(index: Int) -> Int {//调用父类的下标调用方法super[index]return super[index] + 1}
}//多态:父类类型指向子类类型
//animal虽然是父,但其实子类Cat里面东西多(继承animal的东西+自己的东西)
let animal = Cat()
animal.speak()
print(animal[6])
打印:
Animal speak
Cat speak
7
类方法、下标重写
- 被
class
修饰的类方法、下标,可以重写 - 被
static
修饰的类方法、下标,不允许重写
这也是被class修饰的类方法、下标与被static修饰的类方法、下标的区别
属性重写
- 子类可以将父类的属性(不管是存储属性,还是计算属性),重写为计算属性
- 子类不可以将父类重写为存储属性
- 只能重写
var
属性,不能重写let
属性 - 重写时,属性名、类型要一致
- 子类重写后的属性权限 不能小于 父类属性的权限
父类只读 --> 重写后的子类 可以为读写属性
父类读写 --> 重写后的子类 必须为读写属性
不可以将一个继承来的读写属性重写为一个只读属性
存储属性、计算属性都可以重写
属性重写提供了:get、set、willSet、didSet
四个关键字对属性进行重写
一个重写计算属性的例子:
import Foundationclass Car {var speed:Int {//计算属性set {print("car-set")}get {return 10}}
}class Tank: Car {override var speed:Int {//计算属性set {print("tank-set")}get {return 1}}
}let tank = Tank()
print(tank.speed)结果:1
Tank继承Car,本来car的speed有一个初值10,现在tank的初值变为了1
看完计算属性的重写,我们再看下存储属性的重写:
报错,那么加个override是否就可以了呢?
依然报错
那么,究竟怎么重写父类的存储属性呢???
一个重写存储属性的例子:
import Foundationclass Car {var speed:Int = 10
}class Tank: Car {override var speed:Int {get {return super.speed}set {print("tank - set")}}
}let tank = Tank()
print(tank.speed)结果: 10
从上面可以看出,重写属性,不管是计算属性还是存储属性,都需要借助get、set
也可以得出:存储属性重写为计算属性
例子2:
以上都是重新对象属性
对于类型属性:
- 被
class
修饰的计算类型属性,可以被子类重写
class不允许修饰存储属性,所以,上面只能是计算类型属性
- 被
static
修饰的类型属性(存储、计算),不可以被子类重写
属性观察器
可以在子类中,为父类属性(除了只读计算属性、let属性)增加属性观察器(也就是willSet、didSet)
那么willSet、didSet如何使用呢?
final
如果方法、属性、下标被final修饰,则不允许被子类重写
如果类被final修饰,则不允许被继承
10. 多态原理、初始化、可选链
多态
let animal = Animal()
//父类指针,指向子类对象,这种称为多态
//animal虽然是父,但其实子类Cat里面东西多(继承animal的东西+自己的东西)
animal = Cat()
struck与class创建的值,调用方法时的区别
从汇编打断点来看:
struct
建立的值,调用对应方法的时候,在编译期
已经决定调用哪一个方法,是直接调用
,调用性能快
class
建立的值,调用对应方法的时候,在运行期
才决定要调用哪一个方法,是动态调用
,调用性能相比struct要弱一点
class对象(堆空间)里面存储的内容:
前8个字节存放:类型信息(类似OC中的isa),其本身是一个指针,指向的是另外一处堆空间,再这个堆空间里面,存有很多类型相关的信息:比如,要调用的方法地址
再8个字节存放:引用计数(retainCount)
剩余的存放:属性信息
遵循8的倍数原则
var dog1 = Dog()
var dog2 = Dog()
dog1与dog2的前8个字节,内存地址是一样的,也就是类型信息是一样的(只有一份)
从函数调用上,引申出一个问题:
Swift的派发机制
Swift 具有丰富的方法派发机制,这涉及到如何在运行时确定调用哪个方法。主要有以下几种机制:
-
直接派发(Direct Dispatch):此类方法调用发生在编译阶段,编译器会直接在调用处"内联"这个函数的实现。这种派发方式最快最高效,因为它避免了函数调用的开销。常见于结构体和枚举的方法派发。
-
虚表派发(Table Dispatch,常见于 v-table):这是面向对象语言中最常见的派发方式,Swift 的类方法默认使用这种方式。在这种机制中,如果有一个类 Animal,我们创建了一个子类 Dog 并重写了某个方法,例如
bark()
,在运行时如果子类调用bark()
,此时会通过虚表找到子类 Dog 中bark()
方法的地址去调用。 -
消息派发(Message Dispatch):在 Swift 中,这主要发生在和 Objective-C 交互的部分,例如使用了
dynamic
关键字或者@objc
注解的方法。这种机制的弹性最大,但也是最慢的。例如:
@objc dynamic func myMethod() {print("This method uses Objective-C message dispatch.")
}
根据这些派发机制,有几个重要的注意点:
-
尽可能使用直接派发。这是最快的,并且可以让编译器优化你的代码。只有当你需要使用类的多态特性时,才需要其他的派发机制。
-
虚表派发能够提供面向对象编程的多态性,但是也增加了一定的开销。如果类或方法是
final
的,那么它们就可以使用直接派发,因为这些类和方法不能被重写。 -
消息派发是最灵活的,但也是最慢的。一般来说,应避免使用,除非你需要和 Objective-C 的运行时进行交互,或使用一些动态特性。
举个具体的例子,让以上的派发机制更加形象化:
struct MyStruct {func method() {} // 直接派发,因为是值类型
}class MyClass {func method() {} // 默认虚表派发,因为是类
}final class MyFinalClass {func method() {} // 直接派发,因为类是final
}class MyDynamicClass {@objc dynamic func method() {} // 消息派发,因为 @objc dynamic
}
初始化器
类、结构体、枚举都可以定义初始化器
类有两种初始化器:
- 指定初始化器(designated initializer)
- 便捷初始化器(convenience initializer)
其他主要知识点:
- 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
- 默认初始化器:类的指定初始化器
- 类偏向于少量指定初始化器,一个类通常只有一个指定初始化器
- 默认初始化器,指定初始化器。当自定义了带参数的指定初始化器后,编译器生成的不带参的初始化器就没了
- 便捷初始化器,要最终调用指定初始化器。自定义便捷初始化器后,默认初始化器还在