Swift 正如其名,速度飞快。本周将关注 Swift 1.2 版本的一个 重大 更新。Swift 开发团队在这次加速俯冲中一次性响应了开发社区的诸多需求,带来了许多激动人心的新特性。本次发布的每一个小更新都带来了巨大的好处:增量构建、丰富了 Xcode 中的错误信息、提升 Xcode 稳定性、静态类属性、支持 C 的 union 和 bitfield、将 Swift 中的 enum
打通到 Objective-C 中、更安全的类型转换、单行闭包的提升等等。
在这么多的更新中我们主要关注两个明显提升使用体验的新功能:一个是 if let
语句 optional 绑定 终于有了 巨大变化,另一个是 Objective-C 中的空值标注。
Swift 1.2 允许通过多值并行 optional 绑定来避免 if let
语句的多重深嵌套。多个 optional 绑定可以通过逗号分隔开,并且可以带一个和传统的 if
语句同样效果的 where
分句。这样的话,拜占庭的 厄运金字塔 就可以被修缮成中世纪风格的现代牧场了:
从前:
let a = "10".toInt()
let b = "5".toInt()
let c = "3".toInt()
if let a = a {
if let b = b {
if let c = c {
if c != 0 {
println("(a + b) / c = \((a + b) / c)")
}
}
}
}
现在:
if let a = a, b = b, c = c where c != 0 {
println("(a + b) / c = \((a + b) / c)") // (a + b) / c = 5
}
这两个例子中判断语句的执行顺序是一样的。使用新的语法可以让每个绑定都按顺序执行,如果某一个绑定是
nil
就会停下来。只有在所有的 optional binding 都成功的情况下才会进行where
语句检查。一个
if
语句可以包含多个通过逗号分隔的let
绑定。因为每个let
可以绑定多个 optional 和 一个where
分句,所以通过这种方式可以支持真正更高级的逻辑。(感谢 Stephen Celis 帮忙解释清楚这点。)
更棒的是,后续的绑定语句可以引用之前的绑定。这意味着你只用一个 if let
语句就可以解析 Dictionary
对象或者对 AnyObject?
值进行强制类型转换,然后将其用于另一个语句中。
让我们回顾一个经典的例子,将 这样一个庞大的 JSON 块 在 Swift 1.2 中解析。样例中用一个 if let
将 JSON 解析成指定的包含 NSBundle
、NSURL
和 NSData
类型以及一些 AnyObject?
值的对象:
var users: [User] = []
// load and parse the JSON into an array
if let
path = NSBundle.mainBundle().pathForResource("users", ofType: "json"),
url = NSURL(fileURLWithPath: path),
data = NSData(contentsOfURL: url),
userList = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as? [[String: AnyObject]]
{
// extract individual users
for userDict in userList {
if let
id = userDict["id"] as? Int,
name = userDict["name"] as? String,
email = userDict["email"] as? String,
address = userDict["address"] as? [String: AnyObject]
{
users.append(User(id: id, name: name, ...))
}
}
}
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
...
]
我感觉我们之后写的代码中会有不少的逗号出现。
在 Swift 第一版发布的时候,每一个对 Cocoa API 的调用方法都会返回一个模糊解析的 optional 类型(比如说 AnyObject!
)。因为它们会在读取过程中让程序崩溃,所以如果没有清晰的文档表明该方法是否会返回一个 null 值那么这种模糊解析内在是不安全的。这些都是不好的现象。Swift 确实能够调用 Cocoa API 了,但调用方法长得真奇怪。
随着 beta 发布的完善,内部评估中不断移除不友好的标点符号,用真 optional 、非 optional 或永不为空的值来替代模糊解析的 optional。这极大提高了使用 Cocoa 时的体验,但是,使用三方代码的时候这个问题却仍然存在。
这种情况不复存在了!Swift 1.2 带来了新版本的 Clang。新的属性参数和指针标注允许指针、Objective-C 属性、方法参数或返回值是否可以为 nil
。
nonnull
:标示该指针不应该
为nil
。在 Swift 中使用标注nonnull
的指针时会保留他们原始的值(例如NSData
)。nullable
:标示该指针在通常情况下可以
为nil
。它们在 Swift 中会以 optional 值表示(比如NSURL?
)。null_unspecified
:像以前一样的模糊解析 optional 类型,理想状态是只标注而不使用。null_resettable
:标示这个属性永远会有一个值,也可以被赋为nil
。拥有非nil
默认值的属性可以用这个字段标注,例如:tintColor
。根据文档,这种属性在 Swift 中使用时会变成一种(相对安全)的模糊解析 optional 类型。
前三种标注也可以通过双下划线前置(__nonnull
、__nullable
和 __null_unspecified
)用于 C 指针和 block 指针。最后一种 null_resettable
标注仅可用于 Objective-C 属性。
下面的样例中,我们使用一个 data controller 来控制一个位置信息列表,每个位置信息可能附图。这个样例展示了这几种新标注的优点:
@interface LocationDataController : NSObject
@property (nonatomic, readonly) NSArray *locations;
@property (nonatomic, readonly) Location *latestLocation;
- (void)addPhoto:(Photo *)photo forLocation:(Location *)location;
- (Photo *)photoForLocation:(Location *)location;
@end
如果不用空值标注,LocationDataController
类中的每个指针在 Swift 中使用时都会变成模糊解析的 optional 类型:
class LocationDataController : NSObject {
var locations: [AnyObject]! { get }
var latestLocation: Location! { get }
func addPhoto(photo: Photo!, forLocation location: Location!)
func photoForLocation(location: Location!) -> Photo!
}
这!么!多!感!叹!号!真!是!够!了!如果在 Objective-C 中就标注好:
@interface LocationDataController : NSObject
@property (nonnull, nonatomic, readonly) NSArray *locations;
@property (nullable, nonatomic, readonly) Location *latestLocation;
- (void)addPhoto:(nonnull Photo *)photo forLocation:(nonnull Location *)location;
- (nullable Photo *)photoForLocation:(nonnull Location *)location;
@end
首先 locations
列表是 nonnull
的,因为最糟糕的情况下它也就是一个空数组了,但如果列表中没有任何位置信息,latestLocation
却 可以 是 nil
。同理,两个方法中的参数永远不应该为空。因为不是每个位置信息都有一张照片,所以第二个方法的返回值是 nullable
型的 Photo 实例。这种声明方式在 Swift 中就变得更好、更清晰、更安全,而且也没那么多烦人的感叹号了:
class LocationDataController : NSObject {
var locations: [AnyObject] { get }
var latestLocation: Location? { get }
func addPhoto(photo: Photo, forLocation location: Location)
func photoForLocation(location: Location) -> Photo?
}
在 Objective-C 头文件中标注 任何 指针都会导致编译器对整个文件进行标注分析,带来不少的警告。因为大多数的标注都是 nonnull
的,一个用于标注已存在类的提高效率的宏就出现了。在头文件中,例外情况单独标注,其他地方开始和结尾的地方用 NS_ASSUME_NONNULL_BEGIN
和 ..._END
包围起来就好了。
和上述 data controller 能在 Swift 中产生同样效果的另一个更易读的版本就出现了:
@interface LocationDataController : NSObject
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSArray *locations;
@property (nullable, nonatomic, readonly) Location *latestLocation;
- (void)addPhoto:(Photo *)photo forLocation:(Location *)location;
- (nullable Photo *)photoForLocation:(Location *)location;
NS_ASSUME_NONNULL_END
@end
新的 Objective-C 空值标注为 Swift 带来了巨大好处,与此同时如果不写任何 Swift 还是可以从中受益的。如果将 nil
值传给了带有 nonnull
标注的指针,那么自动补全时就会有提示:
// Can I remove a photo by sending nil?
[dataController addPhoto:nil forLocation:currentLocation];
// Nope -- Warning: Null passed to a callee that requires a non-null argument
激动人心的是,关于这次更新的故事我们只讲了一半。除了 Swift 语法的变化和编译器变异方式的变化之外,标准库也升级到了一个 大版本,带来了新的 Set
类(再见了朋友)。呵呵呵呵,所以,我们以前的代码也都不能运行了,Stack Overflow 上又多了 21,000 个过期的 Swift 问题?其实想想还蛮有意思的。
除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。