Swift 1.2Nate Cook Croath Liu 🚩🌱

Swift 正如其名,速度飞快。本周将关注 Swift 1.2 版本的一个 重大 更新。Swift 开发团队在这次加速俯冲中一次性响应了开发社区的诸多需求,带来了许多激动人心的新特性。本次发布的每一个小更新都带来了巨大的好处:增量构建、丰富了 Xcode 中的错误信息、提升 Xcode 稳定性、静态类属性、支持 C 的 union 和 bitfield、将 Swift 中的 enum 打通到 Objective-C 中、更安全的类型转换、单行闭包的提升等等。

在这么多的更新中我们主要关注两个明显提升使用体验的新功能:一个是 if let 语句 optional 绑定 终于有了 巨大变化,另一个是 Objective-C 中的空值标注。

提升 Optional 绑定

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 解析成指定的包含 NSBundleNSURLNSData 类型以及一些 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?
}

NS_ASSUME_NONNULL_BEGIN/END

在 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

不只是 Swift

新的 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 中国大陆」许可协议授权。