swift 新功能介绍

时间:2022-08-17 17:57:05

原文链接:http://www.cocoachina.com/applenews/devnews/2014/0617/8857.html

如果你和我一样,准备好好看看苹果的 Keynote,兴奋地准备开始尝试一些新的 API,结果你听到最多的是一门新的语言:Swift!你突然被告知,这不是 Objective-C的扩展,而是一门完完全全新的语言。你是会激动呢,还是高兴,抑或头脑一片空白?
 
Swift 将会完全改变我们写 iOS 和 Mac 应用的方式,在这片文章里,我概括了这门语言的一些要点,并和 Objective-C 里面相应部分做了对比。
 
注意:这不是 Swift 的入门读物,苹果已经发布了一本很全面的  Swift Programming Language,我强烈建议你先读它。这篇文章只会讨论一些特别 cool、值得玩味的知识点。
 
类型
第一个重大的事情是 Swift 提供了类型推断 ( type inference)。如果语言本身可以推断类型,那么开发者就再也不必声明每一个变量的类型了。编译器可以搞定这件事,如下面的例子,编译器可以自动把变量类型设为 String:
 
  1. // automatically inferred 
  2. var name1 = "Matt" 
  3. // explicit typing (optional in this case) 
  4. var name2: String = "Matt" 
与类型推断相伴而来的,Swift 提供了类型安全的保证 ( type safety)。在 Swift 中,编译器(大部分情况如此,偶有例外)知道一个对象的完整类型,这让它可以做决定来怎么编译代码。
 
这和 Objective-C 是大相径庭的,Objective-C 是一门非常动态的语言,在编译期对象的类型并不完全清楚,部分原因是因为,你可以在运行时给既存的类型增加新的方法、增加一个完整的新类、甚至改变一个实例的类型。
 
让我们看一个具体的例子,下面的 Objective-C 代码:
 
  1. Person *matt = [[Person alloc] initWithName: @"Matt Galloway"]; 
  2. [matt sayHello]; 
当编译器看到 sayHello 被调用的时候,它能够检查头文件里面是否有这个方法的声明,如果没有则编译会出错,但是它能做的仅此而已。这通常情况下是足够了,能够用来发现问题最先出现的地方,以及出现的打字错误。但是因为动态性,编译器不知道 sayHello 是否会在运行时被修改或者一直存在。譬如,它可能是某个协议的一个可选方法(记住 respondsToSelector: 检查)。
 
因为不是强类型的,所以 Objective-C 编译器能做的就比较有限,无法在方法调用的时候进行一些优化。众所周知,方法调用都是通过 objc_msgSend 来动态 dispatch 的,你应该在 backtrace 里看到过。对这个函数而言,选择器的实现会经过查找和跳转两步,我们不可否认这会增加额外开销和复杂度。
 
我们再来看看 Swift 怎样完成同样的事情:
 
  1. var matt = Person(name: "Matt Galloway"); 
  2. matt.sayHello() 
在 Swift 中,编译器知道更多类型信息,它能确切地知道 sayHello 在哪里被定义。因此,它能做一些优化而直接跳转到方法的实现入口,而不必经由动态 dispatch。在其他语言中,可能会通过 vtable 来进行 dispatch,这会比 Objective-C 采用的动态 dispatch 代价稍小一些,C++ 的虚函数就是这样做的。
 
在 Swift 中,编译器能提供更多帮助。他能阻止模糊类型带来的 bug,也能通过编译优化让你的代码跑得更快。
 
泛型
另一个重大的功能就是泛型。如果你对 C++ 比较熟悉,那你可能觉得泛型 (generic) 和模板 (templates) 比较像。因为 Swift 有严格的类型限制,你声明一个函数的时候,对于它的参数必须指明确定的类型。但有时候你对于不同的数据类型,可能都需要相似的功能。
 
一个例子是通常非常有用的数据结构 Pair,你需要将一对值存放在一起。对于整数,你可以这么干:
 
  1. struct IntPair { 
  2.     let a: Int! 
  3.     let b: Int! 
  4.     init(a: Int, b: Int) { 
  5.         self.a = a 
  6.         self.b = b 
  7.     } 
  8.     func equal() -> Bool { 
  9.         return a == b 
  10.     } 
  11. let intPair = IntPair(a: 5, b: 10) 
  12. intPair.a   // 5 
  13. intPair.b   // 10 
  14. intPair.equal()  // false 
看起来还比较有用。但是现在你想让它也能用在浮点数上,你可能就需要再定义一个 FloatPair 的类,这看起来就有点丑陋了。这时候泛型就能派上用场了,与再定义一个新的类相比,你也可以:
 
  1. struct Pair { 
  2.     let a: T! 
  3.     let b: T! 
  4.     init(a: T, b: T) { 
  5.         self.a = a 
  6.         self.b = b 
  7.     } 
  8.     func equal() -> Bool { 
  9.         return a == b 
  10.     } 
  11. let pair = Pair(a: 5, b: 10) 
  12. pair.a  // 5 
  13. pair.b  // 10 
  14. pair.equal()  // false 
  15.  
  16. let floatPair = Pair(a: 3.14159, b: 2.0) 
  17. floatPair.a   // 3.14159 
  18. floatPair.b   // 2.0 
  19. floatPair.equal() // false 
这就相当有用了!可能这还不够清楚说明你现在为什么要使用这一功能,但是相信我:机会无限!你很快就会在你的代码中用到它了。
 
容器
你应该已经明白并且喜欢使用 NSArray,NSDictionary 以及他们的可修改版本了,现在你会看到他们在 Swift 中的等价物。幸运的是,他们非常相像,你可以这样来声明一个数组和字典:
 
  1. let array = [1, 2, 3, 4] 
  2. let dictinary = ["dog": 1, "elephant": 2] 
这对你来说应该再熟悉不过,只是有些许差异:在 Objective-C 中,只要你愿意,数组和字典中可以放入任何类型的对象,但是在 Swift 里面,数字和字典必须指定数据类型,这都是通过前面所讲的泛型实现的。
 
上面的两个变量也可以这样定义,带上类型信息(记住:尽管你不必这么做):
 
  1. let array: Array = [1, 2, 3, 4] 
  2. let dictinary: Dictionary = ["dog": 1, "elephant": 2] 
注意看在定义哪些类型可以放入容器时,泛型的使用。对于数组还有一个简略的形式,看起来可读性更好,但本质上是做同样的事情:
 
  1. let array: Int[] = [1, 2, 3, 4] 
注意:现在会禁止任何 Int 之外的实例加入这个数组。这听起来不那么方便,但是毋庸置疑地有用:你的 API 再也不用长篇累牍地解释它返回的数组中会保存什么内容,或者一个属性里面能够保存什么内容,你可以把这些问题交给编译器,由它来进行前期的错误检查和优化,是更明智的做法。
 
可变性
Swift 中的容器的一个有意思的地方就是它的可变性。与 Objective-C 不一样,Array 和 Dictionary 并不存在一个可变版本,你只能通过 let 和 var 来区分它们。对于那些还没有读过原书,或者还未深入 Swift 的读者(我建议你们读一下,尽快!),只需要知道 let 用来声明常量,var 用来声明变量。let 有点类似于 C/C++/Objective-C 中的 const。
 
用 let 声明的容器无法改变它的大小,也就是说不能调用追加和删除方法,如果你这样做,就会得到类似错误:
 
  1. let array = [1, 2, 3] 
  2. array.append(4) 
  3. // error: immutable value of type 'Array' only has mutating members named 'append' 
这同样适用于字典类。这导致编译器可以推导集合的性质并做适当优化。比如,如果大小不能改变,那么已经保存的值就永远不用考虑重新分配以接纳新值。所以,对于不会发生变化的集合对象,总是使用 let 来声明是一种很好地做法。
 
字符串
Objective-C 中的字符串是众所周知的难用,就算只是连接字符串,代码也是非常冗长,例如:
 
  1. Person *person = ...; 
  2.   
  3. NSMutableString *description = [[NSMutableString alloc] init]; 
  4. [description appendFormat:@"%@ is %i years old.", person.name, person.age]; 
  5. if (person.employer) { 
  6.   [description appendFormat:@" They work for %@.", person.employer]; 
  7. else { 
  8.   [description appendString:@" They are unemployed."]; 
这种代码太啰嗦了,里面包含了太多与目的无关的字符。同样的事情 Swift 里面做起来就是这样:
 
  1. var description = "" 
  2. description += "\(person.name) is \(person.age) years old." 
  3. if person.employer { 
  4.     description += " They work for \(person.employer)." 
  5. else { 
  6.     description += " They are unemployed." 
清晰许多!注意格式化一个字符串的方法,你现在也可以通过 += 操作符来连接简单连接两个字符串,这都简洁太多,并且,也没有可变、不可变两种字符串存在。
 
Swift 另一个非常好的增强是字符串比较。你应该知道,Objective-C 中使用 == 操作符并不能比较两个字符串是否相等,你必须使用 isEqualToString: 这个方法,这是因为 == 只是比较两个指针是否相等。Swift 抛弃了这些概念,让你直接通过 == 操作符来直接比较内容是否相等,这也意味着字符串也可以用到 switch 语句中,下一节会介绍更多相关内容。
 
最后一则好消息就是 Swift 原生支持所有 Unicode 字符,你可以在字符串中使用任何 Unicode 代码点,甚至在函数名和变量名中,如果你愿意,你可以给一个函数命名为(pile of poo!)。
 
对 Unicode 支持的另一个非常方便的地方,就是提供了内建方法,来计算一个字符串的真实长度。一旦支持全范围的 Unicode 字符,字符串长度的计算就不再是一件简单的事情。在 UTF8 字符串里,你不能把字节数当成字符串长度,因为有些字符会占用超过一个字节的空间。在 Objective-C 中,NSString 总是按照 UTF16 来计算长度,把每两个字节当成一个字符,但技术上说这并不总是正确的,因为有些 Unicode 字符会占用 2 个以上的字节。
 
幸运的是,Swift 提供了一个非常方便的函数来计算代码点的真实个数,这就是名叫 countElements 的*函数。你可以像这样来使用它:
 
  1. var poos = "????????" 
  2. countElements(poos) // 2 
但这也并不是对所有情况都成立,它仅仅只统计了一下 Unicode 代码点的个数,并没有考虑引起字符变化的特殊情形。例如,你可以加一个元音符号到一个前缀字符上,这时候 countElements 会认为是两个字符,而实际上看起来只有一个字符:
 
  1. var eUmlaut = "e\u0308" // ë 
  2. countElements(eUmlaut) // 2 
说一千道一万,我想你肯定会赞成,相比 Objective-C,Swift 中的字符串好用太多!
 
Switch 语句
我想讲的最后一件事情就是 switch 语句,与 Objective-C 相比,Swift 对它做了彻底的改进。这是一件很有意思的事情,因为涉及到一些东西,如果不从根本上打破 Objective-C 是 C 的严格超集这一事实,就无法加入到 Objective-C 中去。
 
第一个让人兴奋的点是 switch 可以用到字符串上了,这可能是你以前想做但做不到的。在 Objective-C 中,你只能通过大量的 if 语句和 isEqualToString 组合来达到类似效果,例如:
 
  1. if ([person.name isEqualToString:@"Matt Galloway"]) { 
  2.   NSLog(@"Author of an interesting Swift article"); 
  3. else if ([person.name isEqualToString:@"Ray Wenderlich"]) { 
  4.   NSLog(@"Has a great website"); 
  5. else if ([person.name isEqualToString:@"Tim Cook"]) { 
  6.   NSLog(@"CEO of Apple Inc."); 
  7. else { 
  8.   NSLog(@"Someone else); 
这样的代码可读性太差,并且有太多的字符要敲入。而同样的事情,在 Swift 中则:
 
  1. switch person.name { 
  2.   case "Matt Galloway"
  3.     println("Author of an interesting Swift article"
  4.   case "Ray Wenderlich"
  5.     println("Has a great website"
  6.   case "Tim Cook"
  7.     println("CEO of Apple Inc."
  8.   default
  9.     println("Someone else"
除了字符串上的 switch 之外,还有一些有意思的地方需要注意。这里没有任何 break,这是因为 case 并不会一直执行下去,碰到下一个 case 的时候自动就退出了,这样可以减少好多不小心引发的 Bug!
 
下面的 switch 语句可能会让你头脑发蒙,请做好准备:
 
  1. switch i { 
  2. case 0, 1, 2: 
  3.     println("Small"
  4. case 3...7: 
  5.     println("Medium"
  6. case 8..10: 
  7.     println("Large"
  8. case _ where i % 2 == 0: 
  9.     println("Even"
  10. case _ where i % 2 == 1: 
  11.     println("Odd"
  12. default
  13.     break 
首先,这里出现了一个 break 语句。这是因为 switch 需要完全穷举,比如需要处理所有分支。在这里,我们默认是什么都不做,所以使用 break 来明确声明这一意图。
 
其次,这里有... 和..,这两个用来定义范围的操作符。前一个表示闭区间(包含最右边的边界值),而后一个则表示开区间(不包含最右边的边界值),这将会带来难以置信的便利。
 
最后,可以直接把 case 的分支变量作为计算的输入。以上为例,如果一个数字不在 0-10 范围内,那么对于偶数会打印「Even」,而对于奇数则会打印出「Odd」。神奇到爆,有没有!
 
接下来
希望这篇文章能让你初尝 Swift 的滋味,也能让你明白后面蕴藏了多少宝藏,但是还有更多事情要做,我强烈赞成你去读一下苹果公司的书,看看其他文档,以帮助你来深入学习这门语言——你迟早需要这么做!