[Swift]闭包

1. Swift中闭包的概念非常广泛,但是我们这里主要讲解闭包表达式:

    1) 闭包按照表面意思就是指一段封闭的代码包,而Swift中的闭包则包括之前讲过的函数、方法、内嵌函数,当然最重要的就是闭包表达式了;

    2) 闭包表达式:

         i) 即一段用{ }包住的代码,可以实现跟函数一样的功能;

         ii) 闭包表达式的运算结果为一个函数类型,也就是说用{ }包住的一段闭包表达式的值为一个函数;

         iii) 用以实现类似C++的匿名函数的功能(Lambda函数);

    3) 要支持闭包的两个前提条件:支持函数类型(闭包的返回值就是函数类型)可以将函数作为参数和返回值进行传递,还有一个就是必须支持函数嵌套(因为闭包表达式就是一种抽象的函数,可以在任意地方使用,因此必须包括函数内部);


2. 闭包表达式的定义:

    1) 可以和函数定义进行比对:

func 函数名(参数列表) -> 返回值类型 { 语句组 } // 函数定义

{ (参数列表) -> 返回值类型 in 语句组 } // 闭包表达式的定义
可以看到除了语法形式上不同外,功能上和函数一模一样,这里说明几点最大不同:

i) 闭包表达式形式和函数很像,但是没有一个名字,也就说是匿名的!由于是匿名的,用户就很难通过”函数名“调用的方式来调用闭包了!

ii) 函数可以返回任何类型数据,但是闭包表达式只能返回函数类型,比如:

var add = { (a: Int, b: Int) -> Int in return a + b }
println(add(15, 20)) // 35
因此用户想要在代码中显示调用这种匿名的闭包只有通过如下方式才行:

println({ (a: Int, b: Int) -> Int in return a + b }(15, 20))
就是直接在{}后面加上(实参列表)的形式,因为{}返回的就是一个函数名(就是函数类型),但一般很少这样用,闭包用就用在它的匿名上,匿名才是它的精华所在!

    2) 匿名的闭包能最主要用在什么地方呢?——作为回调函数的参数:

        i) 什么是回调函数:就是不能又程序员显示调用的函数,而是由操作系统调用的函数,而显示调用的意思就是程序员可以直接在代码中通过“函数名(实参列表)”的方式调用的函数,但是回调函数很常见,比如Win32中的窗口消息响应函数以及C++中的sort(array, &fcmp)中的fcmp函数,这些函数都不能由程序员在代码中通过“函数名(实参列表)”的方式显示地在代码中调用,他们的调用都是隐式的,背后都由操作系统自动调用,并且有一个重要的共同点就是,他们的实参都是根据上下文确定的(专业地讲操作系统根据上下文捕获它们的实参并传给它们),比如窗口详细响应函数程序员并没有指定而是操作系统根据MSG结构体中的字段捕获给WndProc的,而fcmp的参数是操作系统根据array中给定的两个元素自动捕获给fcmp的;

        ii) 如果回调函数直接可以在传参的位置定义不是可以让代码更加清晰易懂吗?这样也可以避免为了回调而定义一大堆这样的毁掉函数,并且很多回调函数完成的任务很相似(因此函数类型往往相同),如果要定义他们则特别希望取相同的名字(但是函数类型往往相似而不能取同样的名字去重载,因此只能为每个回到函数取不同的名字,这样很不方便),但是如果直接用闭包表达式在传参时定义这些函数的功能则会非常完美地解决这个问题,因为它是匿名的!并且在传参的位置直接定义其中的代码,可以说时一目了然啊!

sorted(arr_Int, { (a: Int, b: Int) -> Bool in return a < b })
但是这看上去函数参数非常冗长不好看,但是没关系,Swift的闭包表达式有两个非常强大的特性,其一就是闭包表达式有很多简化的方法,比如上式可以简化成:

sorted(arr_Int, {$0 < $1})
其二就是当闭包作为函数的最后一个参数时闭包可以写在函数调用的外面(即函数的最后一个参数是一个函数类型时,而那个函数类型的参数在传参时可以传闭包):

sorted(arr_Int) { (a: Int, b: Int) -> Bool in return a < b }
这不就跟函数定义很像嘛!但是这不是函数定义,其实这也就是为什么闭包的最外侧是花括号{}的原因了,这就是Swift最唯美的尾随闭包了!特别是在闭包的内容特别复杂的时候使用尾随闭包就会让代码看上去非常好看,就跟写函数定义一样,而不用冗长地挤在一行!

记住!尾随闭包能使用的前提是闭包必须是函数的最后一个参数,如果不是则不能使用尾随闭包!

func test1(a: Int, b: Int, f: (Int, Int) -> Int) -> Int
{
    return f(a, b)
}

println(test1(12, 5){ $0 + $1 }) // 17,可以使用尾随闭包

func test2(a: Int, f: (Int, Int) -> Int, b: Int) -> Int
{
    return f(a, b)
}

// test2(12, 5) { $0 + $1 } // Bad! 闭包不是函数的最后一个参数,不得使用尾随闭包
println(test2(12, +, 22)) // Good! 34,闭包只能写在对应的参数位置,这里+不是闭包,其实是Swift的运算符函数
// 用C++的角度去理解就是operator+(Int, Int)函数,+在Swift中可以理解为该函数函数名的简写
   3) 闭包的简单应用(最为内嵌函数的应用):前面其实也都是闭包的应用,记住!包整个闭包表达式看成一个函数名就行了!该函数的参数、内容、返回值都在{}中定义

func cal(opr: String) -> (Int, Int) -> Int
{
    switch opr
    {
    case "+": return { (a: Int, b: Int) -> Int in return a + b }
    case "-": return { (a: Int, b: Int) -> Int in return a - b }
    default: return { (a: Int, b: Int) -> Int in return 0 }
    }
}

println(cal("+")(15, 20)) // 35
println(cal("-")(15, 20)) // -5
println(cal("*")(15, 20)) // 0

3. 闭包表达式的化简:

    1) 化简的内容有:参数类型、参数名、in可以省略、如果闭包定义的函数不是返回Void的则return也可以省略;

    2) 简化的依据:如果可以从上下文环境中推断出(这个推断的步骤由编译器自动完成)以上所说的简化信息,则这些内容都可以进行简化或省略;

    3) 简化示例:

func cal(opr: String) -> (Int, Int) -> Int
{ // Swift可以根据上下文捕获的信息推断出闭包的参数类型等各种信息
  // 这里可以通过cal的返回值类型推断出闭包的所有信息
    
    switch opr
    {
    // 通过返回值可以推断出闭包的参数都是Int型的,因此可以省略类型
    case "+": return { (a, b) -> Int in return a + b }
    // 当然也可以省略括号
    case "-": return { a, b -> Int in return a - b }
    // 可以通过返回值推断出闭包函数的返回类型为Int,因此返回类型“-> 返回类型”也可以省略
    case "*": return { a, b in return a * b }
    // 如果return语句时闭包语句组中的唯一一条语句则可以省略return关键字
    case "/": return { a, b in a / b } // 必须是唯一的一条语句而不是最后一条语句,这样就不行:in a + b; a - b
    // 如果以上所有信息均能获得则还能继续简化
    // 和Shell一样$0表示第一个参数,$1表示第二个参数,一次类推下去
    // 由于参数类型、返回值都已经推断出,因此可以直接利用$X构造表达式作为语句组
    // 同样,如果返回语句是唯一的语句则可以不加return,如果是多个语句就得加return
    case "%": return { $0 % $1 }
    case "&": return { println("operator &"); return $0 & $1 } // 必须加return,否则报错
    // 一般用$X就表示已经推断出参数类型了,因此无需再在多此一举地在闭包中声明参数的类型
    
    // 注意!闭包中必须出现参数!不管是这里的a、b还是$0、$1,因此这里不能直接写return { 0 },这回报错的
    // Swift要求闭包中必须出现参数,因此需要这样写才行
    default: return { a, b in 0 }
    }
}

    4) 有时对于特定的回调函数可以用符号名来代替符号函数达到最简的效果:比如sorted(arr_Int, <),这里<并不是闭包,而是operator<(Int, Int) -> Bool符号函数的缩写,因此可以将<符号看做一种函数类型,当然这种情况是比较少的;


4. 一个尾随闭包的示例:前面已经讲过尾随闭包了,前提就是闭包必须是函数的最后一个参数,这里的例子是Array类型的map方法,该函数将数组元素映射到另一组对应的值,然后将映射数组返回,比如[1, 2, 3].map(一个定义映射方法的函数(或闭包))返回成["one", "two", "three"],后者是映射后的值,如果用尾随闭包则形式是[1, 2, 3].map { 闭包定义 },可以看得出来该回调函数可以捕获数组中的元素,并交由该函数处理返回成映射值:

let dtos = [ // 定义映射表
    0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]

let nums = [16, 58, 510] // 映射对象

let strs: [String] = nums.map { (var num) in // num类型可以推断出是Int
// 返回值根据[String]推断出是String
// 但是使用var修饰参数时必须得加(),否则会报错!
    
    var ret = ""
    while num != 0
    {
        ret  = dtos[num % 10]! + ret
        num /= 10
    }
    return ret
}

println(strs) // [OneSix, FiveEight, FiveOneZero]


5. 闭包的内存包含自己的数据区!——存放外界捕获值

    1) 传统语言如C语言等函数的代码段只有操作语句,而函数中使用的数据都有栈区或堆区统一管理,函数代码段本身没有数据区;

    2) 但是Swift的闭包彻底颠覆了函数的内存模型,闭包在Swift中成为了一种资源,该资源中可以包含自己的数据,就跟Win32的窗口资源类似,不仅有代码,也有数据!

    3) 但是闭包中定义的变量和常量仍然是按照传统方式进行管理的(即存放在统一的栈区和堆区),那么闭包的数据区放的都是什么数据呢?答案是外界捕获值!

    4) 这些捕获值定义在闭包的外面,然后在闭包中直接访问这些值,但这里跟C语言的作用域以及static全局作用域的概念完全不同,在内部访问外部作用域更大的数据其实是直接访问这些数据的空间,但是闭包在这里是捕获而不是直接访问这些数据的空间,具体地将就是将捕获的数据拷贝到自己的数据区中称为自己的资源的一部分,并从此跟原数据没有任何关系,但是这样做有什么好处呢?还记不记得可以通过返回内嵌函数的方式在内嵌函数过了自己作用域仍然可以使用的例子了,如果在函数内部定义一个数据,然后在内嵌函数中捕获该值,最后再返回该内嵌函数,那么不就可以在外界(即过了该数据的作用域)继续通过返回的内嵌函数访问该数据了吗?这种技巧经常用来对程序中的有关信息进行计数,请看下面的例子(内嵌函数也是一种闭包,但这里我们直接使用闭包了):

func makeIncrementor(forIncrement amount: Int) -> () -> Int
{
    var currentCount = 0
    
    return {
        currentCount += amount
        return currentCount
    } // 在内存中创建一块闭包资源
    // 该闭包资源中的cuurentCount并不和外界定义的currentCount共享内存
    // 而是将其备份到闭包自己的内存数据区中,并且用0去初始化该数据
    // 将闭包返回到外界则这片闭包资源区就不会被释放(即使过了作用域)
}

let incrementByTen = makeIncrementor(forIncrement: 10) // 创建了一个闭包并返回
// 接下来使用的都是同一片闭包内存资源
println(incrementByTen()) // 10
println(incrementByTen()) // 20
println(incrementByTen()) // 30

let incrementBySix = makeIncrementor(forIncrement: 6) // 又创建了一个闭包资源
println(incrementBySix()) // 6
println(incrementBySix()) // 12
println(incrementBySix()) // 18

println(incrementByTen()) // 40,闭包就当一个变量或常量之类的数据使用,其中数据区的内容可以一直保存
     5) 闭包是引用类型数据!

         i) 闭包的返回值前面提到过是函数类型,而这个函数类型的数据类似于Win32中HWND等资源指针(在Swift中叫引用);

         ii) 如果将该引用赋来赋去,则这些引用访问的都是同一片闭包内存空间,比如接着上面的代码往下写:

let incrementByTen2 = incrementByTen
println(incrementByTen2()) // 50
只有通过闭包表达式才能创建一片新的闭包内存!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值