swift

Swift基础6(闭包)

2019-11-18  本文已影响0人  SunshineBrother

闭包是可以在你的代码中被传递和引用的功能性独立代码块。

闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。

闭包表达式

闭包表达式语法有如下的一般形式:

{ (parameters) -> (return type) in
    statements
}

闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。可变形式参数也能使用,但需要在形式参数列表的最后面使用。元组也可被用来作为形式参数和返回类型。

在swift中,可以使用func定义一个函数,也可以通过闭包表达式定义一个函数

//函数
func sum(_ v1:Int,_ v2:Int) -> Int {
    v1 + v2
}
//闭包表达式
var fn = {
    (v1:Int,v2:Int) in
    return v1 + v2
}
 
print(sum(1, 2)) //3
print(fn(1,2))     //3

闭包表达式的简写

第一种写法
Swift 的标准库提供了一个叫做sorted(by:)的方法,会根据你提供的排序闭包将已知类型的数组的值进行排序。一旦它排序完成, sorted(by:) 方法会返回与原数组类型大小完全相同的一个新数组,该数组的元素是已排序好的。原始数组不会被 sorted(by:)方法修改。

let names = ["Chris","Alex","Ewa","Barry","Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
print(reversedNames) //["Ewa", "Daniella", "Chris", "Barry", "Alex"]

我们这里来使用一个闭包表达式

reversedNames = names.sorted(by: {(s1:String,s2:String) -> Bool in
    return s1 > s2
})

第二种写法:从语境中推断类型

由于排序闭包为实际参数来传递给方法,Swift 就能推断它的形式参数类型和返回类型。 sorted(by:)方法是在字符串数组上调用的,所以它的形式参数必须是一个(String, String) -> Bool 类型的函数。这意味着 (String, String)Bool类型不需要写成闭包表达式定义中的一部分。因为所有的类型都能被推断,返回箭头( ->)和围绕在形式参数名周围的括号也能被省略

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

第三种写法:从单表达式闭包隐式返回

单表达式闭包能够通过从它们的声明中删掉return关键字来隐式返回它们单个表达式的结果,前面的栗子可以写作:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

第四种写法:简写的实际参数名

Swift 自动对行内闭包提供简写实际参数名,你也可以通过 0 ,1 , $2 等名字来引用闭包的实际参数值。

如果你在闭包表达式中使用这些简写实际参数名,那么你可以在闭包的实际参数列表中忽略对其的定义,并且简写实际参数名的数字和类型将会从期望的函数类型中推断出来。 in 关键字也能被省略,因为闭包表达式完全由它的函数体组成:

reversedNames = names.sorted(by: { $0 > $1 } )

这里, 0 和1 分别是闭包的第一个和第二个 String 实际参数。

第五种:运算符函数

实际上还有一种更简短的方式来撰写上述闭包表达式。Swift 的 String 类型定义了关于大于号( >)的特定字符串实现,让其作为一个有两个 String 类型形式参数的函数并返回一个 Bool 类型的值。这正好与 sorted(by:) 方法的第二个形式参数需要的函数相匹配。因此,你能简单地传递一个大于号,并且 Swift 将推断你想使用大于号特殊字符串函数实现:

reversedNames = names.sorted(by: >)

尾随闭包

func exec(v1:Int,v2:Int,fn:(Int,Int) -> Int) {
    print(fn(v1,v2))
}
 
//调用
exec(v1: 10, v2: 20){
    $0 + $1
}

如果闭包表达式作为函数的唯一实际参数传入,而你又使用了尾随闭包的语法,那你就不需要在函数名后边写圆括号了

func exec(fn:(Int,Int) -> Int) {
    print(fn(1,2))
}

exec(fn: {$0 + $1})
exec(){$0 + $1}
exec{$0 + $1}

闭包

一个函数和它所捕获的变量或者常量环境组合起来,称为闭包

typealias Fn = (Int) -> Int
func getFn() -> Fn {
    
    var num = 0
    func plus (_ i:Int) -> Int{
        num += i
        return num
    }
    
    return plus
}

var fn = getFn()
fn(1)
fn(2)
fn(3)
fn(4)

对于这样的函数,打印结果值是什么呢?我们尝试打印一下1 3 6 10,有没有超出我们的预料。

捕获值

一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。

想要了解上面的本质,我们可以简单的看一下汇编代码

typealias Fn = (Int) -> Int
func getFn() -> Fn {

    var num = 0
    func plus (_ i:Int) -> Int{
//        num += i
        return 0
    }
    return plus
}

var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))

return plus打一个断点,想要看汇编代码工具栏 --> Debug --> Debug Workflow --> Always Show Disassembly

typealias Fn = (Int) -> Int
func getFn() -> Fn {

    var num = 0
    func plus (_ i:Int) -> Int{
        num += i
        return num
    }
    return plus
}

var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))

然后我们在看一下这个汇编

swift_allocObject会在堆中开辟一段新的空间。

理解

我们可以把闭包想象成一个类的实例对象

我们可以把上面方法想象成这样

class Closure {
    var num = 0
    func plus (_ i:Int) -> Int{
        num += i
        return num
    }
   
}
 
var cs = Closure()
cs.plus(1)
cs.plus(2)
cs.plus(3)
cs.plus(4)

【注意】
1、作为一种优化,如果一个值没有改变或者在闭包的外面,Swift 可能会使用这个值的拷贝而不是捕获。
2、如果你分配了一个闭包给类实例的属性,并且闭包通过引用该实例或者它的成员来捕获实例,你将在闭包和实例间建立一个强引用环。
3、闭包是引用类型

练习2

typealias Fn = (Int) -> (Int,Int)
func getFn() -> (Fn,Fn) {

    var num1 = 0
    var num2 = 0
    
    func plus (_ i:Int) -> (Int,Int){
        num1 += i
        num2 += i * 2
        return (num1,num2)
    }
    
    func minus (_ i:Int) -> (Int,Int){
        num1 -= i
        num2 -= i * 2
        return (num1,num2)
    }
    
    return (plus,minus)
}

let (p,m) = getFn()
print(p(5))   //(5, 10)
print(m(4))  //(1, 2)
print(p(3))   //(4, 8)
print(m(2))  //(2, 4)

在一个函数里面,闭包捕获值的地址是相同的

我是使用类对比

class Closure {
    var num1 = 0
    var num2 = 0
    
    func plus (_ i:Int) -> (Int,Int){
        num1 += i
        num2 += i * 2
        return (num1,num2)
    }
    
    func minus (_ i:Int) -> (Int,Int){
        num1 -= i
        num2 -= i * 2
        return (num1,num2)
    }
}

var cs = Closure()
print(cs.plus(5))     //(5, 10)
print(cs.minus(4))  //(1, 2)
print(cs.plus(3))     //(4, 8)
print(cs.minus(2))  //(2, 4)

练习2

var functions:[() -> Int] = []
for i in 1...3{
    functions.append{i}
}

for f in functions {
    print(f())
}
//1
//2
//3

对比类

class Closure {
    var i:Int
    init(_ i:Int) {
        self.i = i
    }
    
    func get() -> Int {
        return i
    }
}

var cs:[Closure] = []
for i in 1...3{
    cs.append(Closure(i))
}

for cls in cs{
    print(cls.get())
}

逃逸闭包(@escaping )与非逃逸闭包(@noescaping)

逃逸闭包(@escaping )

当闭包作为一个实际参数传递给一个函数的时候,我们就说这个闭包逃逸了,因为它是在函数返回之后调用的。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。

闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用

例如:当网络请求结束后调用的闭包。发起请求后过了一段时间后这个闭包才执行,并不一定是在函数作用域内执行的

override func viewDidLoad() {
        super.viewDidLoad()
         
        getData { (data) in
            print("闭包返回结果:\(data)")
        }
    }

    func getData(closure:@escaping (Any) -> Void) {
        print("函数开始执行--\(Thread.current)")
        DispatchQueue.global().async {
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2, execute: {
                print("执行了闭包---\(Thread.current)")
                closure("345")
            })
        }
        print("函数执行结束---\(Thread.current)")
    }

从结果可以看出,逃逸闭包的生命周期是长于函数的。

逃逸闭包的生命周期:

即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放。

非逃逸闭包(@noescaping)

一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用。

    override func viewDidLoad() {
        super.viewDidLoad()
         
        handleData { (data) in
            print("闭包返回结果:\(data)")
        }
    }

    func handleData(closure:(Any) -> Void) {
        print("函数开始执行--\(Thread.current)")
        print("执行了闭包---\(Thread.current)")
        closure("123")
        print("函数执行结束---\(Thread.current)")
    }

为什么要分逃逸闭包和非逃逸闭包

为了管理内存,闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用,这样闭包会持有当前对象,容易导致循环引用。

非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。

自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值

这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。

func getFirstPositive1(_ v1:Int, _ v2:Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive1(1, 2)


func getFirstPositive2(_ v1:Int, _ v2:() -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive2(1, 2) //这个报错
getFirstPositive2(1, {2})

func getFirstPositive3(_ v1:Int, _ v2:@autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive3(1, 2)
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
上一篇下一篇

猜你喜欢

热点阅读