Swift基础6(闭包)

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

闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,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)")
    }

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

逃逸闭包的生命周期:

  • 1、闭包作为参数传递给函数;
  • 2、退出函数;
  • 3、闭包被调用,闭包生命周期结束

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

非逃逸闭包(@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)
  • @autoclosure会自动的将2封装为{2}
  • @autoclosure只支持() -> T的格式参数
  • 其中??就是一个@autoclosure
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容