Swift 中的 @autoclosure

由于種種原因妒挎,簡書等第三方平臺博客不再保證能夠同步更新,歡迎移步 GitHub:https://github.com/kingcos/Perspective/。謝謝纳账!

Date Notes Swift Xcode
2018-01-13 首次提交 4.0.3 9.2
@autoclosure

Perspective吱涉,即透視刹泄。筆者希望可以盡力將一些不是那么透徹的點透過 Demo 和 Source Code 而看到其本質外里。由于國內軟件開發(fā)仍很大程度依賴國外的語言、知識特石,所以該系列文章中的術語將使用英文表述盅蝗,除非一些特別統(tǒng)一的詞匯或整段翻譯時將使用中文,但也會在首次提及時標注英文姆蘸。筆者英文水平有限墩莫,這樣的目的也是盡可能減少歧義,但在其中不免有所錯誤逞敷,遺漏狂秦,還請大家多多批評、指正推捐。

本文也會同步在筆者的 GitHub 的 Perspective 倉庫:https://github.com/kingcos/Perspective裂问,歡迎 Star ??。

Kingcos

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.0.3)

Closure 在 Swift 等許多語言中普遍存在牛柒。熟悉 Objective-C 的同學一定對 Block 不陌生堪簿。兩者其實是比較類似的,相較于 Block皮壁,Closure 的寫法簡化了許多椭更,也十分靈活。

在 Swift 中闪彼,@ 開頭通常代表著 Attribute甜孤。@autoclosure 屬于 Type Attribute,意味著其可以對類型(Type)作出一些限定畏腕。

How

自動(Auto-)

  • @autoclosure 名稱中即明確了這是一種「自動」的 Closure缴川,即可以讓表達式(Expression)的類型轉換為相應的 Closure 的類型,即在調用原本的 func 時描馅,可以省略 Closure 參數的大括號把夸;
  • 其只可以修飾作為參數的 Closure,但該 Closure 必須為無參铭污,返回值可有可無恋日。
func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print("True")
    }
}

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

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

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

Closure 的 Delay Evaluation

  • Swift 中的 Closure 調用將會被延遲(Delay),即該 Closure 只有在真正被調用時嘹狞,才被執(zhí)行岂膳;
  • Delay Evaluation 有利于有副作用或運算開銷較大的代碼;
  • Delay Evaluation 非 @autoclosure 獨有磅网,但通常搭配使用谈截。
var array = [1, 2, 3, 4, 5]

array.removeLast()
print(array.count)

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

closure()
print(array.count)

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

@escaping

  • 當 Closure 的真正執(zhí)行時機可能要在其所在 func 返回(Return)之后時,通常使用 @esacping,可以用于處理一些耗時操作的回調簸喂。
  • @autoclosure@escaping 是可以兼容的毙死,順序可以顛倒。
func foo(_ bar: @autoclosure @escaping () -> Void) {
    DispatchQueue.main.async {
        bar()
    }
}

測試用例

  • swift/test/attr/attr_autoclosure.swift
  • Swift 作為完全開源的一門編程語言喻鳄,這就意味著可以隨時去查看其內部的實現的機制扼倘,而根據相應的測試用例,也能將正確和錯誤的用法一探究竟除呵。

inout

  • autoclosure + inout doesn't make sense.
  • inout@autoclosure 沒有意義再菊,不兼容。
  • 下面是一個簡單的 inout Closure 的 Demo竿奏,其實并沒有什么意義袄简。一般來說也很少會去將一個 func 進行 inout,更多的其實是用在值類型(Value Type)的變量(Variable)中泛啸。
var demo: () -> () = {
    print("func - demo")
}

func foo(_ closure: @escaping () -> ()) {
    var closure = closure // Ignored the warning
    closure = {
        print("func - escaping closure")
    }
}

foo(demo)
demo()
// OUTPUT:
// func - demo

func bar(_ closure: inout () -> ()) {
    closure = {
        print("func - inout closure")
    }
}

bar(&demo)
demo()
// OUTPUT:
// func - inout closure

可變參數(Variadic Parameters)

  • @autoclosure 不適用于 func 可變參數绿语。
// ERROR
func variadicAutoclosure(_ fn: @autoclosure () -> ()...) {
    for _ in fn {}
}

源代碼用例

  • swift/stdlib/public/core/Bool.swift
  • 在一些其他語言中,&&|| 屬于短路(Short Circuit)運算符候址,在 Swift 中也不例外吕粹,恰好就利用了 Closure 的 Delay Evaluation 特性。
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()
  }
}
  • swift/stdlib/public/core/Optional.swift
  • 在 Swift 中岗仑,?? 也屬于短路運算符匹耕,這里兩個實現的唯一不同是第二個 funcdefaultValue 參數會再次返回可選(Optional)型,使得 ?? 可以鏈式使用荠雕。
@_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/stdlib/public/core/AssertCommon.swift
  • COMPILER_INTRINSIC 代表該 func 為編譯器的內置函數:
    • swift/stdlib/public/core/StringSwitch.swift 中提到 The compiler intrinsic which is called to lookup a string in a table of static string case values.(筆者譯:編譯器內置稳其,即在一個靜態(tài)字符串值表中查找一個字符串。)炸卑;
    • WikiPedia 中解釋: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.(筆者譯:在計算機軟件領域既鞠,編譯器理論中,內置函數(或稱內建函數)是在給定編程語言中可以被編譯器所專門處理的的函數(子程序)盖文。通常嘱蛋,它可以用一系列自動生成的指令代替原來的函數調用,類似于內聯函數五续。與內聯函數不同的是洒敏,編譯器更加了解內置函數,因此可以更好地整合和優(yōu)化特定情況疙驾。)凶伙。
  • _assertionFailure():斷言(Assert)失敗,返回類型為 Never它碎;
  • func 的返回值類型為范型 T镊靴,主要是為了類型推斷铣卡,但 _assertionFailure() 執(zhí)行后程序就會報錯并停止執(zhí)行,類似 fatalError()偏竟,所以并無實際返回值。
// FIXME(ABI)#21 (Type Checker): rename to something descriptive.
@_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)
}
  • swift/stdlib/public/SDK/Dispatch/Dispatch.swift
  • 這里的 Closure 的返回值 DispatchPredicate敞峭,本質其實是枚舉類型踊谋,可以直接填入該類型的值,也可以傳入 Closure旋讹,大大地提高了靈活性殖蚕。
@_transparent
@available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
public func dispatchPrecondition(condition: @autoclosure () -> DispatchPredicate) {
    // precondition is able to determine release-vs-debug asserts where the overlay
    // cannot, so formulating this into a call that we can call with precondition()
    precondition(_dispatchPreconditionTest(condition()), "dispatchPrecondition failure")
}
  • swift/stdlib/public/core/Assert.swift
  • 斷言相關的方法很多的參數選為了 Closure,并標注了 @autoclosure沉迹,一是可以直接將表達式直接作為參數睦疫,而不需要參數,二是當 Release 模式時鞭呕,Closure 沒有必要執(zhí)行蛤育,即可節(jié)省開銷。
@_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.
  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
}

Why

  • Q: 總結一下為什么要使用 @autoclosure 呢葫松?
  • A: 通過上述官方源代碼的用例可以得出:當開發(fā)者需要的 func 的參數可能需要額外執(zhí)行一些開銷較大的操作的時候瓦糕,可以使用。
  • 因為如果開銷不大腋么,完全可以直接將參數類型設置為返回值的類型咕娄,只是此時無論是否參數后續(xù)被用到,得到的過程必然是會被調用的珊擂。
  • 而如果不需要執(zhí)行多個操作圣勒,也可以不使用 @autoclosure,而是直接傳入 func摧扇,無非是括號的區(qū)分圣贸。

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.0.3)

  • 官網文檔其實指出了開發(fā)者應當盡量不要濫用 @autoclosure,如果必須使用扳剿,也需要做到明確旁趟、清晰,否則可能會讓他人感到疑惑庇绽。

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

Reference

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末锡搜,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子瞧掺,更是在濱河造成了極大的恐慌耕餐,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辟狈,死亡現場離奇詭異肠缔,居然都是意外死亡夏跷,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門明未,熙熙樓的掌柜王于貴愁眉苦臉地迎上來槽华,“玉大人,你說我怎么就攤上這事趟妥∶ㄌ” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵披摄,是天一觀的道長亲雪。 經常有香客問我,道長疚膊,這世上最難降的妖魔是什么义辕? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮寓盗,結果婚禮上灌砖,老公的妹妹穿的比我還像新娘。我一直安慰自己贞让,他們只是感情好周崭,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喳张,像睡著了一般续镇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上销部,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天摸航,我揣著相機與錄音,去河邊找鬼舅桩。 笑死酱虎,一個胖子當著我的面吹牛,可吹牛的內容都是我干的擂涛。 我是一名探鬼主播读串,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼撒妈!你這毒婦竟也來了恢暖?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤狰右,失蹤者是張志新(化名)和其女友劉穎杰捂,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體棋蚌,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嫁佳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年挨队,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒿往。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡盛垦,死狀恐怖,靈堂內的尸體忽然破棺而出熄浓,到底是詐尸還是另有隱情情臭,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布赌蔑,位于F島的核電站,受9級特大地震影響竟秫,放射性物質發(fā)生泄漏娃惯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一肥败、第九天 我趴在偏房一處隱蔽的房頂上張望趾浅。 院中可真熱鬧,春花似錦馒稍、人聲如沸皿哨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽证膨。三九已至,卻和暖如春鼓黔,著一層夾襖步出監(jiān)牢的瞬間央勒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工澳化, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留崔步,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓缎谷,卻偏偏與公主長得像井濒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子列林,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345