Perspective icon indicating copy to clipboard operation
Perspective copied to clipboard

Swift 中的 @autoclosure

Open kingcos opened this issue 6 years ago • 6 comments

Date Notes Swift Xcode Source Code
2018-04-05 更新并明确源代码所用版本 4.1 9.3 Swift 4.1 Release
2018-01-13 首次提交 4.0.3 9.2 -

@autoclosure

What

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

The Swift Programming Language (Swift 4.1)

闭包(Closure)在 Swift 等许多语言中普遍存在。熟悉 Objective-C 的同学一定对 Block 不陌生。两者其实是比较类似的,相较于 Block,闭包的写法简化了许多,也十分灵活。

在 Swift 中,@ 开头通常代表着属性(Attribute)。@autoclosure 属于类型属性(Type Attribute),意味着其可以对类型(Type)作出一些限定。

How

自动(Auto-)

  • @autoclosure 名称中即明确了这是一种「自动」闭包,即可以让返回该参数类型的闭包作为参数;
  • 其只可以修饰作为参数的闭包类型,但该闭包不能有参数,否则会报错: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

延迟调用(Delay Evaluation)

  • Swift 中的闭包会被延迟调用,即只有在真正被调用时,才被执行;
  • 延迟调用特性有利于非必须执行且运算开销较大的代码;
  • 该特性非 @autoclosure 独有,但通常搭配使用。
var array = [1, 2, 3, 4, 5]

array.removeLast()
print(array.count)

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

closureVar()
print(array.count)

// OUTPUT:
// 4
// 4
// 3

@escaping

  • 当闭包的真正执行时机可能要在其所在函数返回(Return)之后时,通常使用 @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

  • inout@autoclosure 不兼容,且没有实际意义;
  • @autoclosure 不适用于函数的可变参数(Variadic Parameters)。

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 使得运算符右边可以为闭包,再凭借 Delay Evaluation 特性保证了「短路」。
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

kingcos avatar May 05 '18 17:05 kingcos

用JavaScript多了,看Swift的Closure看得费解地吃力,因为JavaScript里也有一个叫Closure的东西,两者虽叫同样的名字,却太不一样。

我来借地盘稍稍比较一下:

Swift中闭包就是一段可以以后call的代码,和其它语言里的匿名函数很像,然后@ escaping和@ nonescaping就是指明一个传进来的闭包是在调用函数返回后还是返回前执行。如果是返回后才会调用的话,调用函数的变量什么的不能被清了。

JavaScript的闭包的话就是一个function和它的所谓lexical environment。

差别不要太大 🤷‍♂️

gnijuohz avatar Jul 26 '18 07:07 gnijuohz

@gnijuohz 感谢您的评论~

您说的很对~两门语言的一些语法确实有些类似,但 JS 作为脚本语言,灵活性非常强,又是弱类型,而 Swift 作为强调类型安全的编译语言,确实差别很大。

kingcos avatar Jul 26 '18 13:07 kingcos

@kingcos 恩,我再读了下苹果的Closure文档,发现我之前的理解还是有误,苹果的文档这么说的,

Closures can capture and store references to any constants and variables from the context in which they are defined.

然后,

Global and nested functions, as introduced in Functions, are actually special cases of closures. Closures take one of three forms:

Global functions are closures that have a name and do not capture any values.

Nested functions are closures that have a name and can capture values from their enclosing function.

Closure expressions are unnamed closures written in a lightweight syntax that can capture values from their surrounding context.

这样看来,其实两者几乎就是一样的。

gnijuohz avatar Jul 26 '18 17:07 gnijuohz

@gnijuohz 嗯嗯,不过其实我个人确实一直没有怎么去深入或者比较多得使用过 JS 这门语言😂。

kingcos avatar Jul 27 '18 16:07 kingcos

@kingcos 我也是工作之后才深入研究js的哈哈。借你地方总结不好意思了 😅

gnijuohz avatar Jul 27 '18 16:07 gnijuohz

@gnijuohz 没事儿~欢迎交流哈😀

kingcos avatar Jul 28 '18 03:07 kingcos