Swift 闭包(一)

·  阅读 95
Swift 闭包(一)

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

函数介绍

在我们分析闭包之前,先来分析一下函数。

函数类型

之前在代码的书写过程中,我们已经或多或少的接触过函数,函数本身也有自己的类型,它由形 式参数类型,返回类型组成。下面我们看几个案例。

  • 案例 1
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

var a = addTwoInts

a(10, 20)
复制代码

这里定义了一个 addTwoInts 函数, 将 addTwoInts 赋值给 a 这个变量,然后就可以通过 a 这个变量来执行这个函数,这里 addTwoInts 代表的就是这个函数的类型。

  • 案例 2
// 函数 1
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

// 函数 2
func addTwoInts(_ a: Double, _ b: Double) -> Double {
    return a + b
}

var a: (Double, Double) -> Double = addTwoInts

a(10.0, 20.0)

var b = a

b(20.0, 30.0)
复制代码

swift 中如果出现同名函数的时候,如果要讲函数赋值给一个变量,需要指明变量的函数类型((Double, Double) -> Double),这时候 a 的值就是函数 2,分别执行 a(10.0, 20.0) 或者 b(20.0, 30.0) 代码的时候其实就是在调用函数 2 。 这里需要明确的一点是函数也是引用类型。

函数的 Metadata

对于函数来说,也有对应的 Metadata,下面我们在源码中来看一下。

struct TargetFunctionTypeMetadata : public TargetMetadata<Runtime> {
  using StoredSize = typename Runtime::StoredSize;
  using Parameter = ConstTargetMetadataPointer<Runtime, swift::TargetMetadata>;

  TargetFunctionTypeFlags<StoredSize> Flags;

  /// 返回值类型
  ConstTargetMetadataPointer<Runtime, swift::TargetMetadata> ResultType;

  /// 参数类型,这里其实是一个连续的内存空间  
  Parameter *getParameters() { return reinterpret_cast<Parameter *>(this + 1); }

  const Parameter *getParameters() const {
    return reinterpret_cast<const Parameter *>(this + 1);
  }

  Parameter getParameter(unsigned index) const {
    assert(index < getNumParameters());
    /// 这里读取的时候是通过 index 获取
    return getParameters()[index];
  }
}
复制代码
class TargetFunctionTypeFlags {
  enum : int_type {
    // 参数掩码 
    NumParametersMask      = 0x0000FFFFU,
    ConventionMask         = 0x00FF0000U,
    ConventionShift        = 16U,
    ThrowsMask             = 0x01000000U,
    ParamFlagsMask         = 0x02000000U,
    EscapingMask           = 0x04000000U,
    DifferentiableMask     = 0x08000000U,
    GlobalActorMask        = 0x10000000U,
    AsyncMask              = 0x20000000U,
    // 判断是否是逃逸类型 
    SendableMask           = 0x40000000U,
  };
复制代码

在以上源码中我们可以看到 TargetFunctionTypeMetadata 继承于 TargetMetadata,所以包含 kind 属性,除此之外 Flags 中可以看到参数掩码、SendableMask(是逃逸类型)等参数,ResultType 代表返回值类型,根据源码中的这些信息我们可以还原如下代码:

func addTwoInts(_ a: Double, _ b: Double) -> Double {
    return a + b
}

struct TargetFunctionTypeMetadata{
    var kind: Int
    var flags: Int
    var arguments: ArgumentsBuffer<Any.Type>

    func numberArguments() -> Int{
        return self.flags & 0x0000FFFF
    }
}

struct ArgumentsBuffer<Element>{
    var element: Element

    mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
        return withUnsafePointer(to: &self) {
            let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
                return start
            }
            return UnsafeBufferPointer(start: ptr, count: n)
        }
    }

    mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}


let value = type(of: addTwoInts)

let functionType = unsafeBitCast(value as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
//输出参数的个数
print(functionType.pointee.numberArguments())
复制代码

闭包

什么是闭包

闭包是一个捕获了上下文的常量或者是变量的函数。

func makeIncrementer() -> () -> Int { 
  var runningTotal = 10
  func incrementer() -> Int {
    runningTotal += 1
    return runningTotal 
  }
  return incrementer 
}

var a = makeIncrementer
复制代码

这里可以看到,内部函数 incrementer 的生命周期明显比外部函数 makeIncrementer 的生命周期长,当执行 return incrementer 外部函数的生命周期就已经结束了,而内部函数被返回出去,并赋值给了变量 a,所以内部函数可以延时执行,而内部函数中用到了外部函数的变量 runningTotal,所以要对 runningTotal 进行捕获,这里 incrementer 就是闭包。

  • 闭包表达式
{ (param) -> (returnType) in 
  //do something
}
复制代码

按照我们之前的知识积累,OC 中的 Block 其实是一个匿名函数,所以这个表达式要具备以下几个条件

  • 作用域(也就是大括号)
  • 参数和返回值
  • 函数体(in)之后的代码

Swift 中的闭包即可以当做变量,也可以当做参数传递,这里我们来看一下下面的例子熟悉一 下:

var closure : (Int) -> Int = { (age: Int) in 
  return age
}
复制代码

同样的我们也可以把我们的闭包声明一个可选类型:

//错误的写法
var closure : (Int) -> Int? closure = nil
//正确的写法
var closure : ((Int) -> Int)? closure = nil
复制代码

还可以通过 let 关键字将闭包声明位一个常量(也就意味着一旦赋值之后就不能改变了)

let closure: (Int) -> Int

closure = {(age: Int) in
 return age
}

closure = {(age: Int) in
 return age
}
复制代码

同时也可以作为函数的参数

func test(param : () -> Int){ 
  print(param())
}
var age = 10
test { () -> Int in
  age += 1
  return age 
}
复制代码

尾随闭包

当我们把闭包表达式作为函数的最后一个参数,如果当前的闭包表达式很⻓,我们可以通过尾随 闭包的书写方式来提高代码的可读性。

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool {
    return by(a, b, c)
}

test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
    return (item1 + item2 < item3)
})
复制代码

以上是一个正常的闭包,通过尾随闭包的方式书写如下:

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool {
    return by(a, b, c)
}

test(10, 20, 30) {item1,item2,item3 in
    return (item1 + item2 < item3)
}
复制代码

通过对比可以看到,以闭包的形式书写,代码会简洁很多。

闭包表达式是 Swift 语法。使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:

  • 利用上下文推断参数和返回值类型
  • 单表达式可以隐士返回,既省略 return 关键字
  • 参数名称的简写(比如我们的 $0)
  • 尾随闭包表达式
var array = [1, 2, 3]

array.sort(by:{(item1:Int, item2:Int)-> Bool in return item1 < item2 })

array.sort(by:{(item1,item2)-> Bool in return item1 < item2 })

array.sort(by:{(item1,item2)in return item1 < item2 })

array.sort{(item1,item2)in item1 < item2}

array.sort{ return $0 < $1 }

array.sort{ $0 < $1 }

array.sort(by:<)
复制代码

虽然代码可以像上面一步步的进行省略简写,但是我们在书写代码的时候也要注意代码的可读性,省略的太多的话也会造成代码可读性变差。

捕获值

在讲闭包捕获值的时候,我们先来回顾一下 Block 捕获值的情形

- (void)testBlock {
    NSInteger i = 1;
    void(^block)(void) = ^{
        NSLog(@"block %ld:", i);
    };
    i += 1;
    NSLog(@"before block %ld:", i); 
    block();
    NSLog(@"after block %ld:", i);
}
复制代码

这里当我们对 i 的值进行加 1之后,再执行 block,可以看到 block 内部 i 的值并没有变化,那么如果我们想要外部的修改能够影响当前 block 内部捕获的值,我们只需要对当前的 i 添加 __block 修饰符就行。

- (void)testBlock {
    __block NSInteger i = 1;
    void(^block)(void) = ^{
        NSLog(@"block %ld:", i);
    };
    i += 1;
    NSLog(@"before block %ld:", i);
    block();
    NSLog(@"after block %ld:", i);
}
复制代码

那么我将以上代码用 swift 来书写,看一下:

image.png

这里可以看到,用 swift 来写的话闭包中 i 的值是修改过的,那么我们将 swift 转成 SIL 代码来看一下。

image.png

image.png

SIL 代码中可以看到,闭包执行的时候是直接获取全局变量 i 的地址,所以在外部修改 i 的值,在闭包中打印 i 的值也是修改过的。

下面我们将以上代码放到函数中再转成 SIL 代码来看一下。

func test() {
    var i = 1
    let closure = {
        print("closure:\(i)")
    }

    i += 1

    print("before closure:\(i)")
    closure()
    print("after closure:\(i)")
}

test();
复制代码

image.png

image.png

image.png

通过 SIL 代码可以看到,将闭包执行放到 test 函数中之后,变量 i 会被捕获到堆区。

OCblock 分为全局 block 、堆区 block 、栈区 block 这三种情况,但是在 Swift 中闭包并没有这些概念,闭包本质上来讲也是一个函数。

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改