【譯】哥們兒,我的方法哪兒去了?

CHENGKANG發表於2017-05-26

原文連結:Dude, Where`s my Call?
譯文原鏈:【譯】哥們兒,我的方法哪兒去了?

想象有一天你正在給 Swift 編譯器喂一些看起來無害的程式碼。

// xcrun -sdk macosx swiftc -emit-executable cg.swift

import CoreGraphics

let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 0.0, 23.0)

然後一個衝擊波打來:

cg.swift:7:12: error: `CGPathCreateMutable()` has been replaced by `CGMutablePath.init()`
<unknown>:0: note: `CGPathCreateMutable()` has been explicitly marked unavailable here
cg.swift:8:1: error: `CGPathMoveToPoint` has been replaced by instance method `CGMutablePath.moveTo(_:x:y:)`
<unknown>:0: note: `CGPathMoveToPoint` has been explicitly marked unavailable here

它們哪兒去了?被重新命名了。

Swift 3 一個重大的特性就是由 Swift-Evolution 提議 SE-0005 (Better Translation of Objective-C APIs Into Swift)SE-0006 (Apply API Guidelines to the Standard Library) 帶來的”超級重新命名“,這次超級重新命名重新命名了 C 和 Objective-C API 中的一些方法以給它們一種更 Swift 的感覺。Xcode 裡面有一個移植器會將你的 Swift 2 程式碼轉換成新的風格。它會執行很多機械的改變,給你留一些由於其他語言改變需要掃尾的工作,例如移除 C 的 for 迴圈

有一些重新命名相當輕微,比如 NSView 中的這個:

// Swift 2
let localPoint = someView.convertPoint(event.locationInWindow, fromView: nil)

// Swift 3
let localPoint = someView.convert(event.locationInWindow, from: nil)

在這裡 Point 從方法名裡移除了。你知道自己正在處理一個 point,所以沒必要重複這一事實。fromView 重新命名為了 from 因為 View 只是提供了冗餘的型別資訊,並沒有讓這個呼叫更清楚。

其他的改變更大一些,比如 Core Graphics:

// Swift 2 / (Objective-C)
let path = CGPathCreateMutable()
CGPathMoveToPoint (path, nil, points[i].x, points[i].y)
CGPathAddLineToPoint (path, nil, points[i + 1].x, points[i + 1].y)
CGContextAddPath (context, path)
CGContextStrokePath (context)

// Swift 3
let path = CGMutablePath()
path.move (to: points[i])
path.addLine (to: points[i + 1])

context.addPath (path)
context.strokePath ()

喔噢。這變化太大了。這個 API 現在看起來就是讓人喜歡的 Swift 風格 API 而不是舊式的 C API。Apple 在 Swift 裡面完全改變了 Core Graphics API (還有 GCD)以讓它們更好用。你在 Swift 3 裡不能再用老式的 CG C 風格的 API,因此你需要開始習慣新的風格。我已經將 GrafDemo (我這些 Core Graphics 博文的示例程式) 在自動翻譯器中跑過(兩次)了。你可以在這個 pull 請求中看到 Swift 3 第一個版本前後的變化,在這個 pull 請求中看到 Xcode8b6 的 Swift 3 版本前後變化。

他們幹什麼了?

Core Graphics API 就是一堆全域性變數和全域性自由方法。就是說,方法並不是直接和某些比如說類或者結構體這樣的例項繫結的。用 CGContextAddArcToPoint 來操作 CGContext 僅僅是一個傳統,不過你傳進去一個 CGColor 也不會有人攔著你。無非就是會在執行時爆炸而已。只是在 C 風格的物件導向你才有一個隱晦型別作為第一個引數傳過去,作為某種神奇餅乾。CGContext* 方法需要一個 CGContextRefCGColor* 方法需要一個 CGColorRef

通過一些編譯器的魔法,Apple 將這些隱晦引用轉成了類,並且新增了一些方法給這些類以將其對映到 C API。當編譯器看到類似這樣的東西時:

let path = CGMutablePath()
path.addLines(between: self.points)
context.addPath(path)
context.strokePath()

實際上,在背後,正在發出這一系列呼叫:

let path = CGPathCreateMutable()
CGPathAddLines(path, nil, self.points, self.points.count)
CGContextAddPath(context, path)
CGContextStrokePath(context)

“新的”類

以下是已經接受 Swift 3.0 治療的常見的隱晦型別 (忽略了一些專用的型別比如 CGDisplayMode 或者 CGEvent),還有一兩個作為代表的方法:

  • CGAffineTransform - translateBy(x:30, y:50), rotate(by: CGFloat.pi / 2.0)

  • CGPath / CGMutablePath - contains(point, using: evenOdd), .addRelativeArc(center: x, radius: r, startAngle: sa, delta: deltaAngle)

  • CGContext - context.addPath(path), context.clip(to: cgrectArray)

  • CGBitmapContext (folded in to CGContext) - let c = CGContext(data: bytes, width: 30, height: 30, bitsPerComponent: 8, bytesPerRow: 120, space: colorspace, bitmapInfo: 0)

  • CGColor - let color = CGColor(red: 1.0, green: 0.5, blue: 0.333, alpha: 1.0)

  • CGFont - let font = CGFont("Helvetica"), font.fullName

  • CGImage - image.masking(imageMask), image.cropping(to: rect)

  • CGLayer - let layer = GCLayer(context, size: size, auxilaryInfo: aux), layer.size

  • CGPDFContext (folded in to CGContext) / CGPDFDocument - context.beginPDFPage(pageInfo)

CGRectCGPoint 在 Swift 3 之前早已有了一些很不錯的擴充套件。

怎麼做到的?

編譯器有一個內建的語法轉換器,它將 Objective-C 的明明風格轉換成更 Swift 些的形式。去掉重複的單詞和那些僅僅是重複型別資訊的單詞。還去掉了一些之前是在方法呼叫左括號之前的單詞並將它們移到括號裡面作為引數標籤。通過這樣自動清理了一大堆呼叫方法。

當然,人類喜歡搞一些微妙複雜的言辭,因此在 Swift 編譯器裡有一個允許手動重寫自動翻譯器翻譯的部分的機制。這是具體的實現了(別在輸出產品時依靠他們),不過他們提供了深入瞭解用於讓現存 API 出現在 Swift 中所做的那些工作的機會。

其中一個涉及到的機制是 ”overlay“,它是當你引入一個框架或者 C 庫時編譯器引用的第二個庫。Swift Lexicon 將 overlay 形容為”當庫在系統中不發被修改時在系統中增強和擴大這個庫“。一些一直都存在很棒的 CGRectCGPoint 擴充套件,例如someRect.divide(30.0, fromEdge: .MinXEdge),怎麼來的?他們來自 overlay。工具鏈想啊”噢,我看到你在連結 Core Graphics。讓我再加點方便方法吧。“

還有另外一個機制,apinotes,特別是 CoreGraphics.apinotes,一字一詞地控制著 Core Graphics 中地命名和可見性。

例如,在 Swift 中像 CGRectMake 這樣用來初始化基礎結構體的呼叫沒有作用,因為已經有它們的初始化方法了。所以就讓這些呼叫方法不可用了:

# The below are inline functions that are irrelevant due to memberwise inits
- Name: CGPointMake
  Availability: nonswift
- Name: CGSizeMake
  Availability: nonswift
- Name: CGVectorMake
  Availability: nonswift
- Name: CGRectMake
  Availability: nonswift

然後還有其他的對映——如果你在 Swift 中看到這個,那就呼叫那個方法:

# The below are fixups that inference didn`t quite do what we wanted, and are
# pulled over from what used to be in the overlays
- Name: CGRectIsNull
  SwiftName: "getter:CGRect.isNull(self:)"
- Name: CGRectIsEmpty
  SwiftName: "getter:CGRect.isEmpty(self:)"

如果編譯器看到了比如 rect.isEmpty() 這樣的東西,它會傳送一個請求給 CGRectIsEmpty

以下還是一些方法和功能的重新命名:

# The below are attempts at providing better names than inference
- Name: CGPointApplyAffineTransform
  SwiftName: CGPoint.applying(self:_:)
- Name: CGSizeApplyAffineTransform
  SwiftName: CGSize.applying(self:_:)
- Name: CGRectApplyAffineTransform
  SwiftName: CGRect.applying(self:_:)

當編譯器看到 rect.applying(transform),它就知道呼叫 CGRectApplyAffineTransform

編譯器只能自動重新命名 Objective-C API,因為其遵循良好的系統命名法。C API (比如 Core Graphics)需要通過 overlay 和 apinote 來實現。

你能做什麼

你可以通過 NS_SWIFT_NAME 做一些類似 apinote 機制的事情。你可以用這個巨集來註釋 C/Objective-C 標頭檔案,表示在 Swift 裡要用那個名字。編譯器會對你的 NS_SWIFT_NAME 採用同樣的替換(”如果看到 X,就呼叫 Y“)。

例如,這是一個 Intents(Siri) 框架中的呼叫:

- (void)resolveWorkoutNameForEndWorkout:(INEndWorkoutIntent *)intent
                         withCompletion:(void (^)(INSpeakableStringResolutionResult *resolutionResult))completion
     NS_SWIFT_NAME(resolveWorkoutName(forEndWorkout:with:));

從 Objective-C 中呼叫它的話看起來是這樣:

NSObject<INEndWorkoutIntentHandling> *workout = ...;

[workout resolveWorkoutNameForEndWorkout: intent  withCompletion: ^(INSpeakableStringResolutionResult) {
     ...
}];

而在 Swift 中是這樣:

let workout: INEndWorkoutIntentHandling = ...
workout.resolveWorkoutName(forEndWorkout: workout) {
    response in
    ...
}

NS_SWIFT_NAME,和 Objective-C 中的輕量級泛型,nullability 註釋,以及 Swift 編譯器中的自動 Objective-C API 重新命名一起,可以讓你立刻有一種介面都回到 Swift 世界中的感覺。

使用自制的 overlay 和 apinote 是可以的,但那些原本是在 Swift 和 Apple 的 SDK 結合在一起時用的。你可以在你自己的框架中分發 apinote,但是 overlay 需要從 Swift 編譯器樹中編譯。

為了自己建立更 Swift 的 API,你必須儘可能地做好標頭檔案旁聽(比如新增 nullability 註釋和 NS_SWIFT_NAME),然後在你的專案中放一些 Swift 檔案來偽造 overlay 以覆蓋任何多餘情況。這些 ”overlay” 檔案在有 ABI 穩定性前都需要作為原始檔傳送。

輕掠過 iOS 10 標頭檔案,看起來新的 API 喜歡用 NS_SWIFT_NAME,而老一點的更久遠一些的 API 用 apinote。這樣有一些道理因為這些標頭檔案是在不同 Swift 版本中共享的,而給更久遠的標頭檔案可能新增新的 NS_SWIFT_NAME 可能會在編譯器未改變的情況下破壞當前的程式碼。而且,apinote 可以由編譯器團隊或者社群成員新增,而標頭檔案的改變需要擁有這個標頭檔案的團隊的注意。而那個團隊可能已經準備好正要釋出他們的功能了。

它好嗎?

Swift 3 版本的 Core Graphics 絕對是更優秀更加 Swift 化。老實說,我也想在 Objective-C 上這樣用。你可能因此失掉一些可 Google 性,並且需要當你在 Stack Overflow 的文章或者網上的教程中看到現有的 CG 程式碼時做一些腦內轉換。不過那也不必這些日子普通的 Swift 程式碼所需的腦力運動多多少。

有一些由於 CG 類似 OO 本質及其如何進入 Swift 中帶來的 API 的不協調。在這個 CoreGraphics.apinotes 中:

- Name: CGBitmapContextGetWidth
  SwiftName: getter:CGContext.width(self:)
- Name: CGPDFContextBeginPage
  SwiftName: CGContext.beginPDFPage(self:_:)

CGBitmapContextCGPDFContext 方法都被 CGContext 偷去了。這意味著你可以對任何 CGContext 要它的寬度,或者叫它開始一個 PDF 頁面。如果你找一個非點陣圖 context 要它的寬,你會得到這樣的執行時錯誤:

<Error>: CGBitmapContextGetWidth: invalid context 0x100e6c3c0.
If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.

因此即使這個 API 非常 Swift 化了,編譯器並不能捕獲某些型別的 API 錯用。Xcode 會高高興興地給你其實實際上不合適的方法補全。某種意義上來說,C API 更安全一點,因為 CGBitmapContextGetWidth 很清楚地告訴你它要的是一個點陣圖 context 即使第一個引數從技術上來說就還是一個 CGContextRef。我希望這僅僅是一個 bug (rdar://27626070)。

如果你想了解更多想超級重新命名以及像 NS_SWIFT_NAME 這樣的工具,看看這個吧 WWDC 2016 Session 403 – iOS API Design Guidelines

相關文章