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 而看到其本质。由于国内软件开发仍很大程度依赖国外的语言、知识,所以该系列文章中的术语将使用英文表述,除非一些特别统一的词汇或整段翻译时将使用中文,但也会在首次提及时标注英文。笔者英文水平有限,这样的目的也是尽可能减少歧义,但在其中不免有所错误,遗漏,还请大家多多批评、指正。

本文也会同步在笔者的 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 只有在真正被调用时,才被执行;
  • 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 的真正执行时机可能要在其所在 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.(笔者译:编译器内置,即在一个静态字符串值表中查找一个字符串。);
    • 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.(笔者译:在计算机软件领域,编译器理论中,内置函数(或称内建函数)是在给定编程语言中可以被编译器所专门处理的的函数(子程序)。通常,它可以用一系列自动生成的指令代替原来的函数调用,类似于内联函数。与内联函数不同的是,编译器更加了解内置函数,因此可以更好地整合和优化特定情况。)。
  • _assertionFailure():断言(Assert)失败,返回类型为 Never
  • func 的返回值类型为范型 T,主要是为了类型推断,但 _assertionFailure() 执行后程序就会报错并停止执行,类似 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 没有必要执行,即可节省开销。
@_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: 通过上述官方源代码的用例可以得出:当开发者需要的 func 的参数可能需要额外执行一些开销较大的操作的时候,可以使用。
  • 因为如果开销不大,完全可以直接将参数类型设置为返回值的类型,只是此时无论是否参数后续被用到,得到的过程必然是会被调用的。
  • 而如果不需要执行多个操作,也可以不使用 @autoclosure,而是直接传入 func,无非是括号的区分。

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)

  • 官网文档其实指出了开发者应当尽量不要滥用 @autoclosure,如果必须使用,也需要做到明确、清晰,否则可能会让他人感到疑惑。

也欢迎您关注我的微博 @萌面大道V

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342