专注、坚持

Swift 中的 @autoclosure

2020.08.23 by kingcos
Date Notes Swift Xcode Source Code
2020-08-23 完善排版与表述 5.1 11.3 -
2018-04-05 更新并明确源代码所用版本 4.1 9.3 Swift 4.1 Release
2018-01-13 首次提交 4.0.3 9.2 -

@autoclosure

Preface

Swift 中的闭包(Closure)十分类似于 Obj-C 中的 Block,但不仅写法更加简洁,也带来了许多新的特性。@autoclosure 即自动闭包属于类型属性(Type Attribute)的一种,意味着其可以对类型作出一些限定,那么本文就来简单介绍一下。

How

自动闭包

@autoclosure 名称中即明确了这是一种自动闭包,即可以让返回该参数类型变量自动转换为闭包。但其只可以修饰无参闭包 () -> T,否则将会报错:error: argument type of @autoclosure parameter must be '()'

func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print(#function)
    }
}

// logIfTrue(predicate: () -> Bool)
logIfTrue { 1 < 2 }

func logIfTrueWithAutoclosure(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print(#function)
    }
}

// logIfTrueWithAutoclosure(predicate: Bool)
logIfTrueWithAutoclosure(1 < 2)

// OUTPUT:
// logIfTrue
// logIfTrueWithAutoclosure

延迟调用

在 Swift 中,由于函数是第一类型,我们可以声明变量为函数类型:

var array = [1, 2, 3, 4, 5]

array.removeLast()
print(array.count) // 4

var closureVar = { array.removeLast() }
print(array.count) // 4

_ = closureVar()
print(array.count) // 3

此时声明所使用的函数并不会被调用,只有当它们加上 () 真正被调用时,才会执行。

func returnBiggerThanZero(_ val1: Int, _ val2: Int) -> Int {
    return val1 > 0 ? val1 : val2
}

print(returnBiggerThanZero(10, {
    print("Run - 1")
    return (0..<10).reduce(0, +)
}()))

// OUTPUT:
// Run - 1
// 10

比如我们声明一个 returnBiggerThanZero(_ val1: Int, _ val2: Int) -> Int 的函数,当 val1 大于 0 时,我们传递闭包返回值作为的参数完全没有必要执行,那么此时就可以将第二个参数声明为闭包类型,从而避免没有必要的闭包执行:

func returnBiggerThanZero(_ val1: Int, _ val2: () -> Int) -> Int {
    return val1 > 0 ? val1 : val2()
}

print(returnBiggerThanZero(10, {
    print("Run - 1")
    return (0..<10).reduce(0, +)
}))

print(returnBiggerThanZero(-10, { -1 }))

// OUTPUT:
// 10
// -1

这时如果只是传递一个变量,则需要包装在闭包中,如上面的 { -1 },但又会显得很冗余,那么我们可以使用 @autoclosure 修饰该参数,即:

func returnBiggerThanZero(_ val1: Int, _ val2: @autoclosure () -> Int) -> Int {
    return val1 > 0 ? val1 : val2()
}

print(returnBiggerThanZero(10, {
    print("Run - 2")
    return (0..<10).reduce(0, +)
}()))
print(returnBiggerThanZero(10, (0..<10).reduce(0, +)))
print(returnBiggerThanZero(-10, -1))

// OUTPUT:
// 10
// 10
// -1

这样我们传递参数时,既支持了延迟调用,也避免了闭包类型的冗余。

@escaping

当闭包的真正执行时机可能要在其所在函数返回之后时,通常使用 @escaping,可以用于处理一些耗时操作的回调。而 @autoclosure@escaping 是可以同时使用的,且放置顺序可以颠倒:

func doWith(_ completion: () -> Void) {
    completion()
}

func doWithAutoclosureAndEscaping(_ escaper: @autoclosure @escaping () -> Void) {
    doWith {
        escaper()
    }
}

func doWithEscapingAndAutoclosure(_ escaper: @escaping @autoclosure () -> Void) {
    doWith {
        escaper()
    }
}

Source Code

Test Cases

$SWIFT_SOURCE_CODE_PATH/test/attr/attr_autoclosure.swift

@autoclosureinout 不兼容;而其也不适用于、可变参数。

Use Cases

$SWIFT_SOURCE_CODE_PATH/stdlib/public/core/

短路(Short Circuit)运算符

// Bool.swift
extension Bool {
  @_inlineable // FIXME(sil-serialize-all)
  @_transparent
  @inline(__always)
  public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows
      -> Bool {
    return lhs ? try rhs() : false
  }

  @_inlineable // FIXME(sil-serialize-all)
  @_transparent
  @inline(__always)
  public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows
      -> Bool {
    return lhs ? true : try rhs()
  }
}

// Optional.swift
@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

Swift 中的 &&|| 以及 ?? 属于短路运算符,即当表达式左边的结果已经可以决定整个运算符的返回值时(运算符的本质也是函数),右边便没有必要运算。利用了 @autoclosure 使得运算符右边可以为闭包,再凭借延迟调用特性保证了「短路」。

var flag = 0
var age: Int? = nil

func getAgeA() -> Int? {
    flag += 10
    return 20
}

func getAgeB() -> Int? {
    flag += 100
    return nil
}

age ?? getAgeA() ?? getAgeB()
print(flag)

// OUTPUT:
// 10

断言(Assert)

断言相关的方法将某些参数设置为闭包类型,并标注了 @autoclosure,一是可以直接将闭包直接作为参数;二是当 Release 模式时,Closure 没有必要执行,即可节省开销(XCTest 和 Dispatch 中的部分方法同理)。

// AssertCommon.swift
@_inlineable // FIXME(sil-serialize-all)
public // COMPILER_INTRINSIC
func _undefined<T>(
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) -> T {
  _assertionFailure("Fatal error", message(), file: file, line: line, flags: 0)
}

// Assert.swift
@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func assert(
  _ condition: @autoclosure () -> Bool,
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) {
  // Only assert in debug mode.
  // 在 Debug 模式且条件不成立,断言失败
  if _isDebugAssertConfiguration() {
    if !_branchHint(condition(), expected: true) {
      _assertionFailure("Assertion failed", message(), file: file, line: line,
        flags: _fatalErrorFlags())
    }
  }
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func precondition(
  _ condition: @autoclosure () -> Bool,
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) {
  // Only check in debug and release mode.  In release mode just trap.
  if _isDebugAssertConfiguration() {
    if !_branchHint(condition(), expected: true) {
      _assertionFailure("Precondition failed", message(), file: file, line: line,
        flags: _fatalErrorFlags())
    }
  } else if _isReleaseAssertConfiguration() {
    let error = !condition()
    Builtin.condfail(error._value)
  }
}

@_inlineable // FIXME(sil-serialize-all)
@inline(__always)
public func assertionFailure(
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) {
  if _isDebugAssertConfiguration() {
    _assertionFailure("Fatal error", message(), file: file, line: line,
      flags: _fatalErrorFlags())
  }
  else if _isFastAssertConfiguration() {
    _conditionallyUnreachable()
  }
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func preconditionFailure(
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) -> Never {
  // Only check in debug and release mode.  In release mode just trap.
  if _isDebugAssertConfiguration() {
    _assertionFailure("Fatal error", message(), file: file, line: line,
      flags: _fatalErrorFlags())
  } else if _isReleaseAssertConfiguration() {
    Builtin.int_trap()
  }
  _conditionallyUnreachable()
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func fatalError(
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file, line: UInt = #line
) -> Never {
  _assertionFailure("Fatal error", message(), file: file, line: line,
    flags: _fatalErrorFlags())
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func _precondition(
  _ condition: @autoclosure () -> Bool, _ message: StaticString = StaticString(),
  file: StaticString = #file, line: UInt = #line
) {
  // Only check in debug and release mode. In release mode just trap.
  if _isDebugAssertConfiguration() {
    if !_branchHint(condition(), expected: true) {
      _fatalErrorMessage("Fatal error", message, file: file, line: line,
        flags: _fatalErrorFlags())
    }
  } else if _isReleaseAssertConfiguration() {
    let error = !condition()
    Builtin.condfail(error._value)
  }
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func _debugPrecondition(
  _ condition: @autoclosure () -> Bool, _ message: StaticString = StaticString(),
  file: StaticString = #file, line: UInt = #line
) {
  // Only check in debug mode.
  if _isDebugAssertConfiguration() {
    if !_branchHint(condition(), expected: true) {
      _fatalErrorMessage("Fatal error", message, file: file, line: line,
        flags: _fatalErrorFlags())
    }
  }
}

@_inlineable // FIXME(sil-serialize-all)
@_transparent
public func _sanityCheck(
  _ condition: @autoclosure () -> Bool, _ message: StaticString = StaticString(),
  file: StaticString = #file, line: UInt = #line
) {
#if INTERNAL_CHECKS_ENABLED
  if !_branchHint(condition(), expected: true) {
    _fatalErrorMessage("Fatal error", message, file: file, line: line,
      flags: _fatalErrorFlags())
  }
#endif
}

Summary

It’s common to call functions that take autoclosures, but it’s not common to implement that kind of function.

NOTE

Overusing autoclosures can make your code hard to understand. The context and function name should make it clear that evaluation is being deferred.

The Swift Programming Language (Swift 4.1)

需要注意的是,官方并不建议我们在函数参数中使用 @autoclosure 类型属性,因为这会使得代码看起来模棱两可。

Extension

COMPILER_INTRINSIC

The compiler intrinsic which is called to lookup a string in a table of static string case values.

笔者译:编译器内置,即在一个静态字符串值表中查找一个字符串。

$SWIFT_SOURCE_CODE_PATH/stdlib/public/core/StringSwitch.swift

In computer software, in compiler theory, an intrinsic function (or builtin function) is a function (subroutine) available for use in a given programming language which implementation is handled specially by the compiler. Typically, it may substitute a sequence of automatically generated instructions for the original function call, similar to an inline function. Unlike an inline function, the compiler has an intimate knowledge of an intrinsic function and can thus better integrate and optimize it for a given situation.

笔者译:在计算机软件领域,编译器理论中,内置函数(或称内建函数)是在给定编程语言中可以被编译器所专门处理的的函数(子程序)。通常,它可以用一系列自动生成的指令代替原来的函数调用,类似于内联函数。与内联函数不同的是,编译器更加了解内置函数,因此可以更好地整合和优化特定情况。

WikiPedia

  • COMPILER_INTRINSIC 代表其为编译器的内置函数。

Reference