【譯】哥們兒,個人方法哪兒去了?

原文連接:Dude, Where's my Call?
譯文原鏈:【譯】哥們兒,個人方法哪兒去了?git

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

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

import CoreGraphics

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

而後一個衝擊波打來:objective-c

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

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

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 循環swift

有一些重命名至關輕微,好比 NSView 中的這個:api

// 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:app

// 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* 方法須要一個 CGColorRefide

經過一些編譯器的魔法,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

相關文章
相關標籤/搜索