[譯]要不要來點(diǎn)Swift

2018-06-19 14:56 更新

猛戳查看最終稿@SwiftGG 做程序員有一點(diǎn)優(yōu)勢:如果工具不好用,你自己就可以對它進(jìn)行優(yōu)化。而 Swift 讓這一點(diǎn)變得尤其簡單,它包含的幾個(gè)特性可以讓你以一種自然的方式對這門語言進(jìn)行擴(kuò)展和自定義。

在本文中,我將分享 Swift 給我編程體驗(yàn)帶來提升的幾個(gè)例子。我希望在讀了本文之后,你可以認(rèn)識到使用這門語言時(shí)你自己的痛點(diǎn)在哪,并付諸實(shí)踐。(當(dāng)然需要先思考?。?/p>

存在爭議的重復(fù)標(biāo)識符

下面是你在 Objective-C 中很熟悉的一種情況:枚舉值和字符串常量會(huì)有很長的描述詳細(xì)的名字:

label.textAlignment = NSTextAlignmentCenter;

(這讓我想起了中學(xué)科學(xué)課的格言:在作答時(shí)重復(fù)一下問題,或者 RQIA,文字是怎么對齊的?文字是居中對齊的。 這在作答方式在超出上下文環(huán)境的時(shí)候很有用,但是其他情況下就顯得比較冗余了。)

Swift 減少了這種冗余,因?yàn)槊杜e值可以通過類型名+點(diǎn)符號來訪問,而且如果你省略了類型名,它仍然可以被自動(dòng)推斷出來:

label.textAlignment = NSTextAlignment.Center


// 更簡明的:
label.textAlignment = .Center

但有時(shí)候你用的不是枚舉,而是被一個(gè)又臭又長的構(gòu)造器給困住了。

animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

有多少“timingfunction“呢?太多了好嘛。

一個(gè)不那么為人所知的事實(shí)是,縮寫點(diǎn)符號對任何類型的任何 static 成員都有效。結(jié)合在 extension 中添加自定義 property 的能力,我們得到如下代碼…

extension CAMediaTimingFunction
{
    // 這個(gè)屬性會(huì)在第一次被訪問時(shí)初始化。
    // (需要添加 @nonobjc 來防止編譯器
    //  給 static(或者 final)屬性生成動(dòng)態(tài)存取器。)
    @nonobjc static let EaseInEaseOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)


    // 另外一個(gè)選擇就是使用計(jì)算屬性, 它同樣很有效,
    // 但 *每次* 被訪問時(shí)都會(huì)重新求值:
    static var EaseInEaseOut: CAMediaTimingFunction {
        // .init 是 self.init 的簡寫
        return .init(name: kCAMediaTimingFunctionEaseInEaseOut)
    }
}

現(xiàn)在我們得到了一個(gè)優(yōu)雅的簡寫:

animation.timingFunction = .EaseInEaseOut

Context 中的重復(fù)標(biāo)識符

用來處理 Core Graphics Context、顏色空間等的代碼往往也是冗長的。

CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(),
    CGColorCreate(CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), [0.792, 0.792, 0.816, 1]))

再次使用棒棒的 extension

extension CGContext
{
    static func currentContext() -> CGContext? {
        return UIGraphicsGetCurrentContext()
    }
}


extension CGColorSpace
{
    static let GenericRGB = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)
}


CGContextSetFillColorWithColor(.currentContext(),
    CGColorCreate(.GenericRGB, [0.792, 0.792, 0.816, 1]))

更簡單了是不?而且顯然會(huì)有更多的方式Core Graphics 類型進(jìn)行擴(kuò)展,以使其適應(yīng)你的需求。

Auto Layout 中的重復(fù)標(biāo)識符

下面的代碼看起來熟悉么?

spaceConstraint = NSLayoutConstraint(
    item: label,
    attribute: .Leading,
    relatedBy: .Equal,
    toItem: button,
    attribute: .Trailing,
    multiplier: 1, constant: 20)
widthConstraint = NSLayoutConstraint(
    item: label,
    attribute: .Width,
    relatedBy: .LessThanOrEqual,
    toItem: nil,
    attribute: .NotAnAttribute,
    multiplier: 0, constant: 200)


spaceConstraint.active = true
widthConstraint.active = true

理解起來相當(dāng)困難,是么?Apple 認(rèn)識到這是個(gè)普遍存在的問題,所以重新設(shè)計(jì)了新的 NSLayoutAnchor API(在 iOS9 和 OS X 10.11 上適用)來處理這個(gè)問題:

spaceConstraint = label.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 20)
widthConstraint = label.widthAnchor.constraintLessThanOrEqualToConstant(200)
spaceConstraint.active = true
widthConstraint.active = true

然而,我認(rèn)為還可以做的更好。在我看來,下面的代碼比內(nèi)置的接口更容易閱讀和使用:

spaceConstraint = label.constrain(.Leading, .Equal, to: button, .Trailing, plus: 20)
widthConstraint = label.constrain(.Width, .LessThanOrEqual, to: 200)


// "設(shè)置 label 的 leading edge 和 button 的 trailing edge 相距 20"
// "設(shè)置 label 的 width 小于等于 200。"

上面的代碼是通過給 UIView 或者 NSView 添加一些 extension 來實(shí)現(xiàn)的。這些輔助函數(shù)看起來可能會(huì)有些拙劣,但是用起來會(huì)特別方便,而且很容易維護(hù)。(這里我已經(jīng)提供了另外一些包含默認(rèn)值的參數(shù)——一個(gè) multiplier,priorityidentifier——所以你可以選擇更進(jìn)一步滴進(jìn)行自定義約束。)

extension UIView
{
    func constrain(
        attribute: NSLayoutAttribute,
        _ relation: NSLayoutRelation,
        to otherView: UIView,
        _ otherAttribute: NSLayoutAttribute,
        times multiplier: CGFloat = 1,
        plus constant: CGFloat = 0,
        atPriority priority: UILayoutPriority = UILayoutPriorityRequired,
        identifier: String? = nil)
        -> NSLayoutConstraint
    {
        let constraint = NSLayoutConstraint(
            item: self,
            attribute: attribute,
            relatedBy: relation,
            toItem: otherView,
            attribute: otherAttribute,
            multiplier: multiplier,
            constant: constant)
        constraint.priority = priority
        constraint.identifier = identifier
        constraint.active = true
        return constraint
    }

    
    func constrain(
        attribute: NSLayoutAttribute,
        _ relation: NSLayoutRelation,
        to constant: CGFloat,
        atPriority priority: UILayoutPriority = UILayoutPriorityRequired,
        identifier: String? = nil)
        -> NSLayoutConstraint
    {
        let constraint = NSLayoutConstraint(
            item: self,
            attribute: attribute,
            relatedBy: relation,
            toItem: nil,
            attribute: .NotAnAttribute,
            multiplier: 0,
            constant: constant)
        constraint.priority = priority
        constraint.identifier = identifier
        constraint.active = true
        return constraint
    }
}

你好~操作符

首先我必須提醒一下:如果要使用自定義操作符,一定要三思而后行。使用它們很簡單,但最終可能會(huì)得到一堆像屎一樣的代碼。一定要對代碼的健壯性持懷疑態(tài)度,然后你會(huì)發(fā)現(xiàn)在某些場景下,自定義操作符確實(shí)是很有用的。

重載它們

如果你有開發(fā)過拖動(dòng)手勢相關(guān)的功能,你可能會(huì)寫過類似下面的代碼:

// 觸摸開始 / 鼠標(biāo)按下:


let touchPos = touch.locationInView(container)
objectOffset = CGPoint(x: object.center.x - touchPos.x, y: object.center.y - touchPos.y)


// 觸摸移動(dòng) / 鼠標(biāo)拖動(dòng):


let touchPos = touch.locationInView(container)
object.center = CGPoint(x: touchPos.x + objectOffset.x, y: touchPos.y + objectOffset.y)

在這段代碼里面我們只做了簡單的加法和減法,但因?yàn)?CGPoint 包含 xy,所以每個(gè)表達(dá)式我們都要寫兩次。所以我們需要一些簡化操作的函數(shù)。

objectOffset 代表觸摸位置和對象位置的距離。描述這種距離最好的方式并不是 CGPoint,而是不那么為人所知的 CGVector, 它不使用 xy,而是用 dxdy 來表示距離或者 “deltas“。

這里寫圖片描述

所以兩個(gè)點(diǎn)相減得到一個(gè)向量就比較符合邏輯了,這樣一來我們就得到了 - 操作符的一個(gè)重載:

/// - 返回: 從 `rhs` 到 `lhs`的向量。
func -(lhs: CGPoint, rhs: CGPoint) -> CGVector
{
    return CGVector(dx: lhs.x - rhs.x, dy: lhs.y - rhs.y)
}

然后,相反滴,把一個(gè)向量和一個(gè)點(diǎn)相加得到另外一個(gè)點(diǎn):

// - 返回: `lhs` 偏移`rhs` 之后得到的一個(gè)點(diǎn)
func +(lhs: CGPoint, rhs: CGVector) -> CGPoint
{
    return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy)
}

現(xiàn)在下面的代碼看起來就感覺很好了!

// 觸摸開始:
objectOffset = object.center - touch.locationInView(container)


// 觸摸移動(dòng):
object.center = touch.locationInView(container) + objectOffset

練習(xí):想一想其它可以用在點(diǎn)和向量上的操作符,并對它們進(jìn)行重載。
建議:-(CGPoint, CGVector)、*(CGVector, CGFloat)-(CGVector)。

獨(dú)門絕技

下面是一些更有創(chuàng)造性的內(nèi)容。Swift 提供了一些復(fù)合賦值操作符,這些操作符在執(zhí)行某個(gè)操作的同時(shí)進(jìn)行賦值:

a += b   // 等價(jià)于 "a = a + b"
a %= b   // 等價(jià)于 "a = a % b"

但是仍然存在其它不包含內(nèi)置復(fù)合賦值形式的操作符。最常見的例子就是 ??,空合并運(yùn)算符。也就是 Ruby 中的 ||=,例如,首先實(shí)現(xiàn)只有在變量是 nil (或者不是)的情況下才賦值的版本。這對 Swift 中的可選值意義非凡,而且實(shí)現(xiàn)起來也很簡單:

infix operator ??= { associativity right precedence 90 assignment } // 匹配其它的賦值操作符


/// 如果 `lhs` 為 `nil`, 把 `rhs` 的值賦給它
func ??=<T>(inout lhs: T?, @autoclosure rhs: () -> T)
{
    lhs = lhs ?? rhs()
}

這段代碼看起來可能很復(fù)雜——這里我們做了下面幾件事情。

  • infix operator 聲明用來告訴 Swift 把 ??= 當(dāng)作一個(gè)操作符。
  • 使用 <T> 將函數(shù)泛型化,從而使其可以支持任何類型的值。
  • inout 表示允許修改左側(cè)的運(yùn)算數(shù)
  • @autoclosure 用來支持短路賦值,有需要的話可以只對右側(cè)做賦值操作。(著也依賴于 ?? 本身對短路的支持。)

但在我看來,上述代碼實(shí)現(xiàn)的功能是非常清晰而且易用的:

a ??= b   // 等價(jià)于 "a = a ?? b"

調(diào)度

關(guān)于如何在 Swift 中使用 GCD,最好的方式是閱讀官方文檔,但在本文中我仍然會(huì)介紹一些基礎(chǔ)知識點(diǎn)。如果想了解更多,請參照這份概要

Swift 2 引入了協(xié)議擴(kuò)展,因此,很多之前的全局標(biāo)準(zhǔn)庫函數(shù)變成了準(zhǔn)成員函數(shù):如 map(seq, transform) 變成了現(xiàn)在的 seq.map(transform),join(separator, seq) 變成了現(xiàn)在的 seq.joinWithSeparator(separator) 等等。這樣的話,那些嚴(yán)格說來不屬于類實(shí)例方法的函數(shù)仍然可以用 . 符號訪問,而且還減少了逗號(PS:原文為parentheses,可能是作者筆誤)的數(shù)目,從而不會(huì)把代碼弄得太亂。

然而這種變化并沒有應(yīng)用到 Swift 標(biāo)準(zhǔn)庫外的自由函數(shù),比如 dispatch_async()UIImageJPEGRepresentation()。這些函數(shù)仍然很難用,如果你經(jīng)常使用它們,還是很值得思考一下如何利用 Swift 來幫你改造一下它們。下面是一些入門的 GCD 例子。

sync 或者非 sync

這些都很簡單;我們馬上開始:

extension dispatch_queue_t
{
    final func async(block: dispatch_block_t) {
        dispatch_async(self, block)
    }

    
    // 這里的 `block` 需要是 @noescape 的, 但不能是鏈接中這樣的: <http://openradar.me/19770770>
    final func sync(block: dispatch_block_t) {
        dispatch_sync(self, block)
    }
}

上面簡化的兩個(gè)函數(shù)直接調(diào)用了普通的調(diào)度函數(shù),但可以讓我們通過 . 符號調(diào)用它們,這是我們之前做不到的。

注:GCD 對象是以一種古怪的方式導(dǎo)出到 Swift 的,盡管以類的方式也可以實(shí)現(xiàn),但實(shí)際上 dispatch_queue_t 只是一個(gè)協(xié)議而已。在這里我把兩個(gè)函數(shù)都標(biāo)注了 final 來表明我們的意圖:我們不希望在這里使用動(dòng)態(tài)調(diào)度,盡管在我看來這種情況下使用協(xié)議擴(kuò)展是很好的,但是不要在哪都用。

mySerialQueue.sync {
    print("I’m on the queue!")
    threadsafeNum++
}


dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0).async {
    expensivelyReticulateSplines()
    print("Done!")

    
    dispatch_get_main_queue().async {
        print("Back on the main queue.")
    }
}

更進(jìn)一步的 sync 的版本是從 Swift 標(biāo)準(zhǔn)庫函數(shù)中的 with* 家族獲取到的靈感,我們要做的事情是返回一個(gè)在閉包中計(jì)算得到的結(jié)果:

extension dispatch_queue_t
{
    final func sync<Result>(block: () -> Result) -> Result {
        var result: Result?
        dispatch_sync(self) {
            result = block()
        }
        return result!
    }
}


// 在串行隊(duì)列上抓取一些數(shù)據(jù)
let currentItems = mySerialQueue.sync {
    print("I’m on the queue!")
    return mutableItems.copy()
}

群體思維

另外兩個(gè)簡單的擴(kuò)展可以讓我們很好的使用 dispatch group

extension dispatch_queue_t
{
    final func async(group: dispatch_group_t, _ block: dispatch_block_t) {
        dispatch_group_async(group, self, block)
    }
}


extension dispatch_group_t
{
    final func waitForever() {
        dispatch_group_wait(self, DISPATCH_TIME_FOREVER)
    }
}

現(xiàn)在調(diào)用 async 的時(shí)候就可以包含一個(gè)額外的 group 參數(shù)了。

let group = dispatch_group_create()


concurrentQueue.async(group) {
    print("I’m part of the group")
}


concurrentQueue.async(group) {
    print("I’m independent, but part of the same group")
}


group.waitForever()
print("Everything in the group has now executed")

注:我們可以很簡單滴選擇 group.async(queue) 或者 queue.async(group)。具體用哪個(gè)全看你自己——或者你甚至可以兩個(gè)都實(shí)現(xiàn)。

優(yōu)雅的重定義

如果你的項(xiàng)目同時(shí)包含 Objective-C 和 Swift,你可能會(huì)碰到這種讓人頭大的情況:Obj-C 的 API 看起來不是那么 Swift 化。需要 NS_REFINED_FOR_SWIFT 來拯救我們了。

在 Obj-C 中使用標(biāo)記了 (new in Xcode 7) 宏的函數(shù)、方法和變量是正常的,但是導(dǎo)出到 Swift 之后,它們會(huì)包含一個(gè) “__“前綴。

@interface MyClass : NSObject


/// @返回 @c 東西的下標(biāo), 如果沒有提供就返回 NSNotFound。
- (NSUInteger)indexOfThing:(id)thing NS_REFINED_FOR_SWIFT;


@end


// 當(dāng)導(dǎo)出到 Swift 時(shí), 它就變成了:


public class MyClass: NSObject
{
    public func __indexOfThing(thing: AnyObject) -> UInt
}

現(xiàn)在把 Obj-C 的方法放到一邊,你可以重用同樣的名字來提供一個(gè)更友好的 Swift 版本 API(實(shí)現(xiàn)方式通常是調(diào)用帶“__“前綴的原始版本):

extension MyClass
{
    /// - 返回: 給定 `thing` 的下標(biāo), 如果沒有就返回 `nil`。
    func indexOfThing(thing: AnyObject) -> Int?
    {
        let idx = Int(__indexOfThing(thing)) // 調(diào)用原始方法
        if idx == NSNotFound { return nil }
        return idx
    }
}

現(xiàn)在你可以心滿意足滴 “if let“了!

更進(jìn)一步

Swift 還很年輕,它各個(gè)代碼庫的風(fēng)格也都是不同的。而大量的第三方微型庫也涌現(xiàn)出來,這些庫的代碼體現(xiàn)了作者在操作符,輔助函數(shù)和命名規(guī)范上所持的不同觀點(diǎn)。這種情況也就需要在處理依賴關(guān)系以及在團(tuán)隊(duì)中建立規(guī)范時(shí)更挑剔。

使用本文中技術(shù)最重要的原因不是寫最新潮和最酷炫的 Swift 代碼。當(dāng)然,負(fù)責(zé)維護(hù)你代碼的人——也許是未來的你——可能會(huì)持不同的觀點(diǎn)。為了他們,親愛的讀者,你需要在為了讓代碼變的清晰合理的情況下擴(kuò)展 Swift,而不是為了讓代碼變的簡單而去擴(kuò)展它。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號