多邻国团队的Swift代码实践
最近我们刚刚发布了一款新的基于Swift的应用,当时还被苹果着重推荐了,目前它已经获得了相当多的用户。在这片文章里,我们想要分享一下这些经验,把我们对于这个新语言的看法呈现给大家,并且指出Swift中那些可以让我们写出更好程序的新特性。
这不是一篇Swift入门指南,这篇文章的受众是那些对Swift并不是很熟悉,而且好奇Swift在真实的编程过程中是怎么样子的开发者。我们会引用一些技术概念并且会在合适的地方提供关于它们的入门指南和文档的链接。
首先,我们会简单介绍一下这个新的应用是做什么的和我们的主要目标是什么。
新的应用
你可能已经很熟悉我们的主应用Duolingo,一个非常受欢迎的语言学习应用,它拥有超过6000万的用户(截止2014年12月),它也曾被苹果评为2013年年度应用。如果你想要学习一门新的语言,Duolinggo将是你在你的iPhone或者iPad上的首选应用。
之后,我们发布了Duolingo Test Center(下文称Test Center),这个应用非常实用,它可以让你测试你对一门语言的掌握情况。例如,如果你是一个外国人,并且想要在美国或者英国的大学里寻求一份工作,这些工作通常都会要求你有一些官方证书,来证明你可以熟练使用英语。此应用的用户可以通过一些测试来让用户确定自己的语言水平,同时为了防止作弊,会有真人来监督测试。
这款应用发布伊始就被苹果在超过50个国家的APP Store“最佳新应用”中推荐。
目标
性能方面,Test Center对性能要求并不高。应用中大部分都是一些静态内容和少量的控件。另外,为了防止作弊,测试的全过程会被录像,基本Test Center就是这样了。我们在使用Swift的过程中并没有碰到任何性能问题,但是还是必须要注意一下性能。
对我们来说更重要的是应用的稳定性和健壮性。由于测试会持续大约20分钟并且它们是收费的,所以在测试途中崩溃会造成相当差的用户体验1。另外,一旦一个测试开始,你就必须完成它(就是说,用户不能暂停或退出此应用;这样做是为了防止作弊)。所以,我们需要将崩溃的可能性最小化。
对Swift的一般看法
当Swift刚发布时,许多人只是看了看它的语法就开始拿它和其它语言作比较、下结论。有些人说他们现在“不再需要忍受Objective-C的语法”,可以直接开始iOS开发了。老实说,这种看法是错误的。谁会在意语法(只要语法不是很变态)?对于一门语言来说,除了语法还有很多更重要的东西,比如它可以让你更容易地表达你的想法,还有不鼓励不好的行为。
Swift比Objective-C或者任何其它语言都能给我们带来更多的启发。如果你在Twitter上关注了Swift的一些作者,你就会知道他们从其它地方拿来了很多非常好的概念,包括函数式编程,同时他们也在合适的地方摒弃了很多现存的(但是并不十分理想)的概念。
由于我们已经习惯了用Objective-C 来编程,Swift对我们来说是一个不错的的且友好的进步。如果你本来用的语言是Haskell(或者similar),你可能会觉得Swift仍有进步的空间。同时我们也很期待未来的Swift版本会带来什么更多的改进。
优点
Swift支持了很多新特性,这些特性是开发者在其它语言的使用过程中已经习惯了的,像是自定义操作符和函数重载。值类型(含有字面上的值的类型,例如Swift的结构体)可以让你更容易的理解代码。
我们也非常喜欢使用Swift中更强大的静态类型系统,还有类型推断。尤其是当Objective-C中没有泛型时,在Swift中我们终于有了类型安全的集合,而不是只能希望在NSArray中存储的是某一种类型的对象。
接下来详细得看一下我们在Swift中发现的十分实用的特性
没有Exception
到现在为止,Swift中还没有错误处理。我们并不知道是Swift的作者在设计这门语言时特意不加入错误处理,或者只是因为当时时间不够。不论如何,我们觉得没有错误处理是一件非常好的事情,因为(没有被处理的)exception让代码更难被读懂(被良好处理的exception能让代码变得更清晰,让开发者知道在哪里会发生exception,但是它们又显得过于笨重了,反正Objective-C中就不支持exception处理)。
事实上,在我们最常碰到的应用崩溃的原因中,第七个就是因为苹果提供的一个方法抛出了exception(-[AVAssetWriterInputHelper markAsFinished])。这个方法并没有被标记为会抛出exception,在文档中也没有注明,所以在真正看到这个崩溃报告之前,我们完全不知道它的行为会是这样,而那时有些用户的应用已经崩溃了。
有经验的Cocoa开发者会知道,尽管Objective-C提供exception抛出和处理的机制,但它只在极少数情况下使用,而且这些情况经常是一些不可恢复的情况(尽管有一些例子)。在这种情况下,更好的解决方案可能不是去获取并处理这个exception,而是去改善代码,使得这个exception根本不会被抛出。有些人可能会争论说这样exception好像变成是一个失败断言方法,但可能这个概念本来的设计目的就是这样呢,那么在一个含有assert()和fatalError()的新语言中,为什么还要保留它呢?
通常,我们都想要避免自己忘记去处理一个错误,更理想的情况,我们想要在编译时就发现所有的问题,而不是在我们的应用已经崩溃之后。Exception 只会让这变得困难,所有我们在Swift中为什么还需要使用它呢?
Optional
Swift中有很多非常重要的基本概念,Optional(你可能知道这个和Haskell中的Maybe类型很像)便是其中之一。苹果的文档中这么写道:
Optional是一个有两个值的枚举类型,None和Some(T),它们分别代表无值和有值。所有类型都可显式地(或者隐式地转换为)一个Optional类型。
同时,Swift提供了简单方便的使用Optional类型的语法糖,例如在None的情况下可以使用nil,特殊的展开语法,操作符等等。另外,Optional链还允许你写出简单清晰的包含多Optional依赖的代码。
那么我们怎么使用它呢?Optional是一个非常好的用来表示“值可能为空”的方法,你可以用它作为函数的返回值类型,来表示这个函数可能会不返回任何结果(只要你不好奇这究竟是为什么)
为什么这会比在Objective-C中给一个指针赋值为空更好呢?因为这样编译器(在编译期)就能保证我们操作的是正确的类型。换句话说,在Swift中一个不是Optional类型的值永远不可能为空,另外,由于Swift中的Optional不仅仅是简单的指针类型,所以他们的用处更广泛。
这里是一个关于Optional使用的小例子:在Objective-C里,所有返回直指针类型的方法,比如对象初始化方法(例如-init),都可能会合法的返回nil(例如当一个对象不能被初始化)。一个很明显的例子就是+ (UIImage *)imageNamed:(NSString *)name,只通过看这个方法名,你并不能确定它会不会返回nil。
然而在Swift里你就可以。苹果在Swift中引入了可失败的初始化程序的概念,这样就可以很方便地在类型的层面上表达一个方法不会返回nil。在Swift里,同样的例子是这样子的: init?(named name: String) -> UIImage,注意这里有个问号,这个问号表示如果标识符为name的变量找不到时,init方法可能会返回nil。
我们在合适的地方大量的使用了这个特性(我们在试图避免对Optional进行显式的拆包或者强制拆包)。如果一个表达式可能会返回nil(例如失败时)而且我们不需要知道为什么,那么Optional便是很好的选择。
Result
如果你有一个可能会失败的函数调用,而且你想要知道为什么它会失败,那么你可以使用Swift提供的Result(对于函数式编程的开发者而言,他就像Either的子类型),它会是一个既简单又实用的选择。
和Optional相似,Result使你可以在类型的层面上表示一个东西可能是一种类型的某个值,或者是一个NSError
像Optional一样,Result也是一个简单的枚举类型,它有两个枚举值Success(T)和Failure(NSError)。正常情况下success枚举值会包含你感兴趣的正常的值,如果有错误,你会得到一个.Failure和一个描述性的NSError。
和Optional不同的是,Result不是Swift标准库的一部分,也就是说,你必须自己定义它。(当前阶段,编译器还缺少一些相关的特性,你需要找到一个变通方案。)
我们在我们的网络通信、I/O、和代码分析模块的很多地方都用到了Result,这个方案要比老的NSError指针在函数里传入传出,或者通过一个completion块来包含成功的值和错误指针(或者更复杂的布尔型返回值和NSError指针的一起使用的方案)要好太多了,
Result是一个相当优雅的解决方案,它能让你写出更好、更简洁、更安全的代码。在我们的应用中,任何可能执行失败(非致命的失败)的表达式都会返回一个Optional或者Result。
和Objective-C的互操作
与Objective-C的互操作是Swift设计时的一个很重要的考虑因素。如果苹果仅仅是发布一个新的编程语言,然后想用Swift的实现完全代替之前的所有代码库是行不通的——至少现在还不行。另外,开发社区里还有大量的Objective-C的代码,如果没有与Objective-C不错的互操作性,可能不会有人愿意去用Swift。
幸运的是,Swift和Objective-C之间的互操作相当简单,而且我们已经在一个很小的范围里进行了一些实践,效果还是不错的。但是值得注意的是,有些Swift的概念(比如枚举)在Objective-C中并不能直接使用。
例如,我们的应用中有一个小的功能部件需要操作PDF文件,这个部件我们是用Swift来写的,然后我们又想在主应用中使用这个模块,主应用是用Objective-C写的。哎,偏偏有一些方法使用了仅Swift中才有的特性,这就意味着这些方法不能在Objective-C中不能自动被桥接。为什么绕过这个问题,我们简单地对Swift的方法做了一个包装方法,这个方法可以在Objective-C中使用2。
当然,在Swift中直接使用我们主应用中已有的Objective-C代码也是非常简单的。如果想要这么做,你只要简单地把那部分代码从应用中拿出来(或者更好,它本身就是一个单独的模块),然后通过一个桥接头文件导入你的Swift代码中。
缺点
尽管Swift相比Objective-C而言有了很多进步,但是现在它还是有一些地方需要改进的。例如,这门新语言缺少一些其他现代语言中常见的高可表达性。但是作为一个新的语言,可能这种情况会很快改变。
苹果保障说会保证兼容性,但是还说他们可能会在合适的时候修改这门语言的一些特性(实际上,他们已经这样做过几次了)。这就意味着在更新编译器之后你可能必须去修改你的代码,否则就不能编译通过。我们知道这种事情会发生,并且也无所谓,幸运的是,对于我们现存的之前运行良好的代码,“修复”它们往往不需要花太多的时间。
我们对Swift最不爽的——也是我们受挫的根源——可能并不是语言本身,而是与之配套的工具。在Xcode(苹果的Objective-C和Swift的IDE)上使用Swift的体验还不是很好。在我们开发的过程中,Xcode经常会运行很卡或者直接崩溃。大部分时间里并没有(或者很慢)代码提示,基本上可以说没有调试器,不稳定而且不可信的语法高亮,编辑器很慢(一旦项目达到了一定的大小),还有没有重构工具。
另外,编译器报的错误信息经常难以理解,编译器中也还有一些bug和缺失的特性(例如类型推断经常出错)。
从我们开始使用到现在,Xcode已经有了很大的进步了,大部分都很好,只是有一些小地方破坏了编程体验。我们希望苹果能多一些关注并且不断改进这个开发工具。
一些数字
苹果是在2014年6月的WWDC大会上发布Swift的,同年的7月底,我们启动了Test Center,它是我们第一个只用Swift语言开发的应用,之后我们在十一月中旬发布了它。开发到1.0版本耗费了3个月多一点的时间(一个程序员;Android版本和web版本那时已经存在了,所以当时我们确实已经有了完整的后台和设计)。
像我们之前说的,健壮性和稳定性对我们而言非常重要,所以让我们在这方面是怎么做的。
崩溃
在写这篇文章时,Test Center已经发布有大约两个半月了,并且已经有了相当大的下载量和用户量(可能要归功于被苹果推荐的原因)。
和其他任何第一版一样,我们碰到了很多之前没碰到过的问题,但是幸运的是,我们似乎并没有忽略任何很重要的bug。到今天为止,test center的崩溃率在大约0.2%,好像还不错嘛3。
如果仔细看一下崩溃组(由于同样的原因造成的崩溃):崩溃组的第一名(造成了大约30%的崩溃)是由于外部的Objective-C库。事实上,前五名中有四组是由于Objective-C的原因造成的(第五名是由于一个我们在最终的发布版本中忘了关掉的失败断言)。
还有一个值得注意的是,第七名是因为前面提到的那个苹果提供的Objective-C函数中有时会抛出exception,而这点在文档中并没有体现(-[AVAssetWriterInputHelper markAsFinished])。
我们把这么低的崩溃率归功于可靠的软件架构和我们对一些很好的编程原则的坚持,然而,Swift的优良的设计也减低了很多bug产生的可能性,这对我们去构建我们的软件架构是很有帮助的。例如,使用Swift的类型系统,很多的错误可以在编译期被发现,而不是在已发布产品运行时才被发现4。
编译器性能
我们必须要问一个问题,对于一个像我们这种规模的项目,编译器是怎么来编译的。根据sloc的数据,我们的项目中现在有10634行实际代码(不包含空行和注释等)。
清除Xcode的缓存,然后运行完time xcodebuild -configuration Release命令需要2分钟,一次调试运行需要大约30秒的编译时间。所有的测试都是在一个mid 2013 Retina MacBook Pro上做的。需要注意的是编译xib也需要一定的时间,并不全是Swift5。
你可以明显的感觉到Xcode会随着你的项目的增长变得越来越慢,而且碰到这个问题的不仅仅是我们。循环时间(当你在改动了代码之后,从按下CMD+R,到应用在模拟器里打开的时间)也比Objective-C要长。在一次简单的测试中,在代码中增加一行,要等14秒编译,这个时间取决于在这行代码中到底做了什么,而在Objective-C的项目中作相似的改动,只需要2、3秒。
当然,这并不是复杂的编译器基准测试,所以可以有保留的看待这些数字。希望你至少能对现在的编译器性能有一个大致的了解。
结论
对于Objective-C的长期开发者来说——尤其是那些对现代编程语言感兴趣的——Swift是一个受欢迎的且激动人心的进步,同时,由于(当前的)开发工具的原因,它有时也可能会让人倍感挫折。
我们已经展示了(至少在我们这类的应用中)Swift可以用来写出稳定的健壮的并且高容量的应用。我们的主应用Duolingo,也已经使用了一部分Swift代码,我们也计划在将来更多的使用它。
那么为什么你会选择Swift呢?只要你在开发大型项目时有保持更新的用户(你只能支持iOS7以上)和耐心,Swift提供了一个新鲜的,良好结构的编程语言选择。我们真诚地推荐你试一下它,特别地,去理解一下苹果想要推广的这种编程哲学。
如果你正在使用Objective-C,那么转换到Swift还是比较简单并且直接的。你可以用和Objective-C一样的编程方法来使用Swift。当你使用到一些Swift中新的概念时,会很有趣。尤其现在好像有一种拥抱函数式编程的趋势,我们认为这挺好的。