本文由CocoaChina译者 星夜暮晨 翻译
原文: Help Yourself to Some Swift
作为一名软件工程师,好处之一就是如果我们对手上的工具不甚满意的话,我们可以自行对这个工具进行完善。Swift 让这个优化过程变得更为轻松,它提供了许多特性从而允许我们能够自然而然地扩展和自定义这门语言。
在本篇文章中,我打算为大家分享一系列 Swift 小贴士,以表述 Swift 是如何让我的生活更加美好的。我希望本文能够抛砖引玉,让您对这门语言有更深甚至更好的想法,并付诸行动(切记要三思而后行!)。
您可能已经熟悉了 Objective-C 中的一个使用惯例:那就是枚举值以及其字符常量通常都有长得吓人的描述名:
label.textAlignment = NSTextAlignmentCenter;
(这让我想起了从中学课上学习到的一条准则:在答案中要复述问题,简称 RQIA。 问:在这个例子中是哪一个文本对齐方式呢?答:在这个例子中是 居中文本对齐方式 。 这对老师批阅试卷来说是非常有效的一种做法,因为老师很可能记不住问题,但是在其他情况下这种做法会显得十分繁杂。)
Swift 减少了冗余度,因为枚举值可以在类型名之后加上点语法进行访问,即使你省略掉了类型姓名它仍然能够推断出来:
label.textAlignment = NSTextAlignment.Center // 更为简洁: label.textAlignment = .Center
然而,很多时候我们很可能不会用到枚举,遇上的往往是这样很长很长的一个构造器:
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
代码中会有多少个“timingFunction”?很可能多到无法想象。
有一个不为人知的小技巧,就是缩略形式的点语法对于 所有类型 的静态成员来说都是有效的。通过在扩展中增加自定义的属性就可以应用上这个技巧了……
extension CAMediaTimingFunction { // 这个属性是懒加载属性,第一次被访问时才会被初始化。 // (@nonobjc 标记是必须的,这可以阻止编译器试图为一个 // 静态属性创建动态访问器(也就是令其不可继承)。 @nonobjc static let EaseInEaseOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) // 另一个方法就是使用计算性属性,这也同样有效, // 但是*每次*访问它的时候都将重新计算,可能带来性能问题: static var EaseInEaseOut: CAMediaTimingFunction { // .init is short for self.init return .init(name: kCAMediaTimingFunctionEaseInEaseOut) } }
这样我们就可以很方便地简化这个操作了:
animation.timingFunction = .EaseInEaseOut
处理 Core Graphics 上下文、色区之类的代码同样也会非常非常长:
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), CGColorCreate(CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), [0.792, 0.792, 0.816, 1]))
我们仍然使用 万能的 扩展:
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]))
看起来要简单不少。当然,还有 很多方法 可以扩展 Core Graphics,从而让它符合您的需求。
这是不是似曾相识?
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
非常难以阅读,是吧?苹果意识到这是一个很常见的问题,因此重新设计了新的 NSLayoutAnchor API(可以在 iOS 9 以及 OS X 10.11 上使用)来简化自动布局的构造:
spaceConstraint = label.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 20) widthConstraint = label.widthAnchor.constraintLessThanOrEqualToConstant(200) spaceConstraint.active = true widthConstraint.active = true
不过,我觉得我们可以做得更好。在我的设想当中,下面这行代码比内置的 API 更容易阅读和使用:
spaceConstraint = label.constrain(.Leading, .Equal, to: button, .Trailing, plus: 20) widthConstraint = label.constrain(.Width, .LessThanOrEqual, to: 200) // "让标签的左边缘和按钮的右边缘建立关联,距离为 20" // "让标签的宽度小于或等于 200"
我们可以对 UIView 或者 NSView 建立一对扩展,从而让这个设想成为可能。这些辅助方法可能看起来又臭又长,但是它们使用起来却十分方便,并且在可维护性上也是十分优越的。(这里我实际上包含了一些带有默认值的额外参数——比如 multiplier
、 priority
以及 identifier
,因此你可以更好地对约束进行构建)。
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 } }
在我们使用自定义运算符之前,我必须要告诫大家: 切记要三思而后行 。运算符使用起来很简单,但是很可能最后会搞得一团糟。一步一个脚印,对自己代码不要过分自信,我确信您最终就可以找到自定义运算符的真正用处。
如果您要让一个元素可以拖动的话,那么很可能就要写如下所示的代码:
// 开始触摸 / 鼠标摁下: let touchPos = touch.locationInView(container) objectOffset = CGPoint(x: object.center.x - touchPos.x, y: object.center.y - touchPos.y) // 手指拖动 / 鼠标移动: let touchPos = touch.locationInView(container) object.center = CGPoint(x: touchPos.x + objectOffset.x, y: touchPos.y + objectOffset.y)
这里我们仅仅只是做了很简单的加减法,但是由于 CGPoint 由 x
和 y
两个元素 组成 ,因此我们必须要再次写下每个表达式。这里使用了某些便利函数来实现。
objectOffset
代表了触摸位置和对象位置之间的距离。表述这个距离的最好方式实际上并不是 CGPoint,而是使用不常见的 CGVector ,它使用的是 dx
和 dy
来表示距离,也就是所谓的“变量增量”。
因此,两点之间的减法操作会生成一个矢量是符合逻辑的,为了实现这个功能,我们需要重载 -
运算符:
/// - Returns: 由 `rhs` 指向 `lhs` 的一个向量 func -(lhs: CGPoint, rhs: CGPoint) -> CGVector { return CGVector(dx: lhs.x - rhs.x, dy: lhs.y - rhs.y) }
然后反过来,我们向一个点上加上矢量将会生成另一个点:
/// - Returns: 距离 `lhs` 点 `rhs` 矢距的新点 func +(lhs: CGPoint, rhs: CGVector) -> CGPoint { return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy) }
现在那堆代码就变得更加易懂了!
// 触控开始: objectOffset = object.center - touch.locationInView(container) // 手指拖动: object.center = touch.locationInView(container) + objectOffset
练习:想想对于点和矢量来说还有哪些运算符可以用?找出并实现它们。建议: -(CGPoint, CGVector)
、 *(CGVector, CGFloat)
以及 -(CGVector)
。
还有一些东西更有创造性。Swift 提供了一系列 复合赋值运算符(compound assignment operators) ,同时执行算术操作以及赋值:
a += b // 等同于 "a = a + b" a %= b // 等同于 "a = a % b"
但是仍然有很多运算符没有内置的复合赋值形式。一个最常见的例子就是 ??
,空合(nil-coalescing)运算符。就如同 Ruby 的 ||=
只有当变量为空(或者为 false )的时候才开始赋值。这对于 Swift 可选值来说是一个非常好的特性,并且添加起来十分简单:
infix operator ??= { associativity right precedence 90 assignment } // 匹配其他的赋值运算符 /// 如果 `lhs` 为 `nil`, 那么就用 `rhs` 的值对其进行赋值 func ??=(inout lhs: T?, @autoclosure rhs: () -> T) { lhs = lhs ?? rhs() }
这可能让人望而却步——我们一点一点来进行分析:
infix operator
声明告诉 Swift 将 ??=
视为一个运算符。
通过
为函数添加泛型,这样可以处理所有类型的值。
inout
允许它修改左操作数。
@autoclosure
将开启短路求值(short-circuit evaluation)功能,如果可以的话只对右操作数进行求值(这个同样也依赖于 ??
本身的短路求值功能)。
最后,结果如我所愿,简洁明了,易于使用:
a ??= b // 相当于 "a = a ?? b"
注意:关于如何让 Swift 恰当地使用 GCD(Grand Central Dispatch) 可能需要为其专门写一篇文章,但是这里我会尽可能将基础部分说完。你可以在这个 Gist 上找到更多的想法。
Swift 2 引入了 协议扩展 ,因此之前许多全局标准库函数变成了准成员函数,比如说 map(seq, transform)
现在变成了 seq.map(transform)
, join(separator, seq)
现在变成了 seq.joinWithSeparator(separator)
等等。因此,这些 理论上 不是实例方法的函数仍然可以使用点语法来进行访问,减少一堆括号导致代码散乱无章的现象发生。
然而,所有的付出并没有完全收获,对于 Swift 标准库之外的自由函数,比如说 dispatch_async()
以及 UIImageJPEGRepresentation()
。这些函数用起来十分笨重,如果您的代码中大量应用了这些函数的话,不妨来看看 Swift 是如何减轻您的负担的。我们以一些 GCD 的例子开始。
sync
与 async
这十分简单,我们直接上代码:
extension dispatch_queue_t { final func async(block: dispatch_block_t) { dispatch_async(self, block) } // `block` 这里应该被标记为 @noescape,然而我们无法这么做 final func sync(block: dispatch_block_t) { dispatch_sync(self, block) } }
这两个便利调用方法可以直接放入正常的队列处理函数,并且允许我们使用点语法进行访问,在此之前我们是没办法这么做的。
注意:由于 GCD 对象转换至 Swift 的方法十分古怪, dispatch_queue_t
实际上是一个协议,虽然它也可以作为类正常工作。我这里用 final
给函数做了标记以表明我们的意图,也就是不能在动态队列中使用。虽然我的理解是这里本质上是协议扩展,在与此类似的情况下, 千万不要使用它 。
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.") } }
关于 sync
有一个更为优化的版本,我们从 Swift 标准库函数中的 with*
族获取到了灵感,让我们在闭包中返回所计算出的值:
extension dispatch_queue_t { final func sync(block: () -> Result) -> Result { var result: Result? dispatch_sync(self) { result = block() } return result! } } // Grab some data while on the serial queue let currentItems = mySerialQueue.sync { print("I’m on the queue!") return mutableItems.copy() }
还有两个简单的扩展,通过 dispatch groups 可以轻松地实现:
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) } }
现在这个额外的 group
参数就可以被包含进 async
的常规调用当中了。
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)
一样简单。哪一个更好完全取决于个人爱好,您甚至可以两个都实现。
如果您的项目中使用了 Objective-C 以及 Swift 两种语言,您或许会遇到这样一个尴尬的局面:Obj-C 的 API 和 Swift 的 风格 相差太大。这时候就需要 NS_REFINED_FOR_SWIFT
来救场了。
通过这个宏指令( Xcode 7 新出的 )所标记的函数、方法和变量在 Obj-C 代码中可以正常使用,但当它们桥接到 Swift 的时候,名称前面就会加上“__”。
@interface MyClass : NSObject /// @return 指定 @c 的索引,如果未找到的话则返回 NSNotFound - (NSUInteger)indexOfThing:(id)thing NS_REFINED_FOR_SWIFT; @end // 当桥接到 Swift 的时候,这个方法变为: public class MyClass: NSObject { public func __indexOfThing(thing: AnyObject) -> UInt }
通过 Objc-C 方法,您就可以使用相同的名称来提供一个对 Swift 更友好的 API(通常在现有加下划线的原始版本的基础上来实现)。
extension MyClass { /// - Returns: 指定 `thing` 的索引,如果未找到的话返回 `nil` func indexOfThing(thing: AnyObject) -> Int? { let idx = Int(__indexOfThing(thing)) // 调用原始方法 if idx == NSNotFound { return nil } return idx } }
现在我们就可以使用 if let
来使用这段代码了!
Swift 是一门年轻的语言,其中每个代码库(codebase)都是不同的。大量微型函数库正在涌现,每位作者对于运算符、辅助方法以及命名规范都有着各自的标准。这个情况就需要苹果团队中需要谨慎采纳依赖库和标准库设置。
采用本文所述的技术不是为了写最酷、最时髦的 Swift 代码。诚然,维护您任务的某人——也是将来的您自己——可能会以一份全新的方式来思考这些代码。他们的目的只是为了更好地阅读代码,因此不要因为怎样简单怎样来,而是怎样明了怎样来。
本文中的所有译文仅用于学习和交流目的,转载请注明文章译者、出处、和本文链接。
感谢 博文视点 为本期翻译活动提供赞助