Swift小贴士:语言的扩展和自定义
2015-12-28 10:00:06 | 来源:玩转帮会 | 投稿:佚名 | 编辑:小柯

原标题:Swift小贴士:语言的扩展和自定义

  • 本文由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”?很可能多到无法想象。

有一个不为人知的小技巧,就是缩略形式的点语法对于所有类型的静态成员来说都是有效的。通过在扩展中增加自定义的属性就可以应用上这个技巧了……

extensionCAMediaTimingFunction
{
//这个属性是懒加载属性,第一次被访问时才会被初始化。
//(@nonobjc标记是必须的,这可以阻止编译器试图为一个
//静态属性创建动态访问器(也就是令其不可继承)。
@nonobjcstaticletEaseInEaseOut=CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut)
//另一个方法就是使用计算性属性,这也同样有效,
//但是*每次*访问它的时候都将重新计算,可能带来性能问题:
staticvarEaseInEaseOut:CAMediaTimingFunction{
//.initisshortforself.init
return.init(name:kCAMediaTimingFunctionEaseInEaseOut)
}
}

这样我们就可以很方便地简化这个操作了:

animation.timingFunction=.EaseInEaseOut
上下文环境

处理 Core Graphics 上下文、色区之类的代码同样也会非常非常长:

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

我们仍然使用万能的扩展:

extensionCGContext
{
staticfunccurrentContext()->CGContext?{
returnUIGraphicsGetCurrentContext()
}
}
extensionCGColorSpace
{
staticletGenericRGB=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 建立一对扩展,从而让这个设想成为可能。这些辅助方法可能看起来又臭又长,但是它们使用起来却十分方便,并且在可维护性上也是十分优越的。(这里我实际上包含了一些带有默认值的额外参数——比如 multiplierpriority 以及 identifier,因此你可以更好地对约束进行构建)。

extensionUIView
{
funcconstrain(
attribute:NSLayoutAttribute,
_relation:NSLayoutRelation,
tootherView:UIView,
_otherAttribute:NSLayoutAttribute,
timesmultiplier:CGFloat=1,
plusconstant:CGFloat=0,
atPrioritypriority:UILayoutPriority=UILayoutPriorityRequired,
identifier:String?=nil)
->NSLayoutConstraint
{
letconstraint=NSLayoutConstraint(
item:self,
attribute:attribute,
relatedBy:relation,
toItem:otherView,
attribute:otherAttribute,
multiplier:multiplier,
constant:constant)
constraint.priority=priority
constraint.identifier=identifier
constraint.active=true
returnconstraint
}
funcconstrain(
attribute:NSLayoutAttribute,
_relation:NSLayoutRelation,
toconstant:CGFloat,
atPrioritypriority:UILayoutPriority=UILayoutPriorityRequired,
identifier:String?=nil)
->NSLayoutConstraint
{
letconstraint=NSLayoutConstraint(
item:self,
attribute:attribute,
relatedBy:relation,
toItem:nil,
attribute:.NotAnAttribute,
multiplier:0,
constant:constant)
constraint.priority=priority
constraint.identifier=identifier
constraint.active=true
returnconstraint
}
}
你好啊,运算符

在我们使用自定义运算符之前,我必须要告诫大家:切记要三思而后行。运算符使用起来很简单,但是很可能最后会搞得一团糟。一步一个脚印,对自己代码不要过分自信,我确信您最终就可以找到自定义运算符的真正用处。

重载运算符

如果您要让一个元素可以拖动的话,那么很可能就要写如下所示的代码:

//开始触摸/鼠标摁下:
lettouchPos=touch.locationInView(container)
objectOffset=CGPoint(x:object.center.x-touchPos.x,y:object.center.y-touchPos.y)
//手指拖动/鼠标移动:
lettouchPos=touch.locationInView(container)
object.center=CGPoint(x:touchPos.x+objectOffset.x,y:touchPos.y+objectOffset.y)

这里我们仅仅只是做了很简单的加减法,但是由于 CGPoint 由 xy 两个元素组成,因此我们必须要再次写下每个表达式。这里使用了某些便利函数来实现。

objectOffset 代表了触摸位置和对象位置之间的距离。表述这个距离的最好方式实际上并不是 CGPoint,而是使用不常见的 CGVector,它使用的是 dxdy 来表示距离,也就是所谓的“变量增量”。

因此,两点之间的减法操作会生成一个矢量是符合逻辑的,为了实现这个功能,我们需要重载 - 运算符:

///-Returns:由`rhs`指向`lhs`的一个向量
func-(lhs:CGPoint,rhs:CGPoint)->CGVector
{
returnCGVector(dx:lhs.x-rhs.x,dy:lhs.y-rhs.y)
}

然后反过来,我们向一个点上加上矢量将会生成另一个点:

///-Returns:距离`lhs`点`rhs`矢距的新点
func+(lhs:CGPoint,rhs:CGVector)->CGPoint
{
returnCGPoint(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 可选值来说是一个非常好的特性,并且添加起来十分简单:

infixoperator??={associativityrightprecedence90assignment}//匹配其他的赋值运算符
///如果`lhs`为`nil`,那么就用`rhs`的值对其进行赋值
func??=(inoutlhs:T?,@autoclosurerhs:()->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 的例子开始。

syncasync

这十分简单,我们直接上代码:

extensiondispatch_queue_t
{
finalfuncasync(block:dispatch_block_t){
dispatch_async(self,block)
}
//`block`这里应该被标记为@noescape,然而我们无法这么做finalfuncsync(block:dispatch_block_t){
dispatch_sync(self,block)
}
}

这两个便利调用方法可以直接放入正常的队列处理函数,并且允许我们使用点语法进行访问,在此之前我们是没办法这么做的。

注意:由于 GCD 对象转换至 Swift 的方法十分古怪,dispatch_queue_t 实际上是一个协议,虽然它也可以作为类正常工作。我这里用 final 给函数做了标记以表明我们的意图,也就是不能在动态队列中使用。虽然我的理解是这里本质上是协议扩展,在与此类似的情况下,千万不要使用它。

mySerialQueue.sync{
print("I’monthequeue!")
threadsafeNum++
}
dispatch_get_global_queue(QOS_CLASS_BACKGROUND,0).async{
expensivelyReticulateSplines()
print("Done!")
dispatch_get_main_queue().async{
print("Backonthemainqueue.")
}
}

关于 sync 有一个更为优化的版本,我们从 Swift 标准库函数中的 with* 族获取到了灵感,让我们在闭包中返回所计算出的值:

extensiondispatch_queue_t
{
finalfuncsync(block:()->Result)->Result{
varresult:Result?
dispatch_sync(self){
result=block()
}
returnresult!
}
}
//Grabsomedatawhileontheserialqueue
letcurrentItems=mySerialQueue.sync{
print("I’monthequeue!")
returnmutableItems.copy()
}
群策群力

还有两个简单的扩展,通过 dispatch groups可以轻松地实现:

extensiondispatch_queue_t
{
finalfuncasync(group:dispatch_group_t,_block:dispatch_block_t){
dispatch_group_async(group,self,block)
}
}
extensiondispatch_group_t
{
finalfuncwaitForever(){
dispatch_group_wait(self,DISPATCH_TIME_FOREVER)
}
}

现在这个额外的 group 参数就可以被包含进 async 的常规调用当中了。

letgroup=dispatch_group_create()
concurrentQueue.async(group){
print("I’mpartofthegroup")
}
concurrentQueue.async(group){
print("I’mindependent,butpartofthesamegroup")
}
group.waitForever()
print("Everythinginthegrouphasnowexecuted")

注意:我们可以使用 group.async(queue),就和 queue.async(group) 一样简单。哪一个更好完全取决于个人爱好,您甚至可以两个都实现。

精炼并简洁

如果您的项目中使用了 Objective-C 以及 Swift 两种语言,您或许会遇到这样一个尴尬的局面:Obj-C 的 API 和 Swift 的风格相差太大。这时候就需要 NS_REFINED_FOR_SWIFT 来救场了。

通过这个宏指令(Xcode 7 新出的)所标记的函数、方法和变量在 Obj-C 代码中可以正常使用,但当它们桥接到 Swift 的时候,名称前面就会加上“__”。

@interfaceMyClass:NSObject
///@return指定@c的索引,如果未找到的话则返回NSNotFound
-(NSUInteger)indexOfThing:(id)thingNS_REFINED_FOR_SWIFT;
@end
//当桥接到Swift的时候,这个方法变为:
publicclassMyClass:NSObject
{
publicfunc__indexOfThing(thing:AnyObject)->UInt
}

通过 Objc-C 方法,您就可以使用相同的名称来提供一个对 Swift 更友好的 API(通常在现有加下划线的原始版本的基础上来实现)。

extensionMyClass
{
///-Returns:指定`thing`的索引,如果未找到的话返回`nil`
funcindexOfThing(thing:AnyObject)->Int?
{
letidx=Int(__indexOfThing(thing))//调用原始方法
ifidx==NSNotFound{returnnil}
returnidx
}
}

现在我们就可以使用 if let 来使用这段代码了!

展望

Swift 是一门年轻的语言,其中每个代码库(codebase)都是不同的。大量微型函数库正在涌现,每位作者对于运算符、辅助方法以及命名规范都有着各自的标准。这个情况就需要苹果团队中需要谨慎采纳依赖库和标准库设置。

采用本文所述的技术不是为了写最酷、最时髦的 Swift 代码。诚然,维护您任务的某人——也是将来的您自己——可能会以一份全新的方式来思考这些代码。他们的目的只是为了更好地阅读代码,因此不要因为怎样简单怎样来,而是怎样明了怎样来。

本文中的所有译文仅用于学习和交流目的,转载请注明文章译者、出处、和本文链接。
感谢博文视点为本期翻译活动提供赞助

tags:

上一篇  下一篇

相关:

大公司头条:《星战》改写全球票房记录;万达引入铁人三项

我们的微信公众号上线啦,直接搜索「大公司头条」就可以订阅。希望每天早上在邮箱里收到「大公司头条」?点

这座斜坡屋顶上的房子,真的不是违章建筑

这所奇怪的房子在印度尼西亚雅加达,像是沿着屋顶建出来的。即便在很远的地方也都能看到白色的金属框架和玻

网页设计师闭着眼睛都得会的10个技能

快速发展的技术、千变万化的趋势、应接不暇的软件和素材,都是网页设计师需要面临的挑战。但是撇开这些一直

对初阶体验设计师「影响力」构建的浅思

国内一些刚刚从业、甚至已经从业好几年的用户体验设计师,在做了一段时间设计后,会或多或少产生一些困惑:

前中国最大社交网站51.com产品副总裁王武佳:一切伟大的创造来源于行业间的相互借鉴模仿|2015产品运营大会演讲实录

12月19日,在起点学院和人人都是产品经理联合主办的《2015产品运营年终大会》上,“云幕后”创始人,前中国

石灰石-石膏法与半干法烧结机烟气脱硫技术比较

烧结烟气是烧结混合料点火后,随台车运行,在高温烧结成型过程中所产生的废气,烧结烟气中含有一定浓度的SO

1000个赞的《快乐码农》期刊第10期

《快乐码农》期刊第 10 期发布,喜欢的朋友可以点此邮件订阅,就可以定期收到我们的期刊邮件。编程语言Java

女生真的有体香吗?

记得初中那会,追姐的男孩都一套说辞,你跟别的女孩不一样,有一股淡淡的香味。一度对女性体香好奇,每个女

《老炮儿》:吴亦凡冯小刚飚戏过瘾演技赞!(小飞角色形象之分析)

《老炮儿》:吴亦凡冯小刚飚戏过瘾演技赞!(小飞角色形象之分析)
管虎自编自导、冯小刚领衔主演,《

付费注册制应对以及浅谈中线卖出技术

说一下今天的消息面,注册制终于跨出重要一步,因为有明确的时间表,我看了一些相关分析文章,结合自己的想

站长推荐: