Swift 中的 @autoclosure

萌面大道發表於2019-03-04

由於種種原因,掘金等第三方平臺部落格不再保證能夠同步更新,歡迎移步 GitHub:github.com/kingcos/Per…。謝謝!

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 -

您可以在 github.com/kingcos/Per… 中看到更多更新的博文,謝謝您的關注。

@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 代表其為編譯器的內建函式。

也歡迎您關注我的微博 @萌面大道V

Reference

相關文章