iOS 11 in developer's eye [0]

356 阅读6分钟
原文链接: zhuanlan.zhihu.com
当然我只是一只半吊子半桶水二把刀三脚猫会写两行代码的家伙。不过只要上过 App Store 或者靠 iOS 拿过工资就可以腆着脸自称 iOS Developer 了吧 wwwww

# Developer's perspective

看了这两个页面:

Apple Developer Documentation apple/swift-evolution<img src="https://pic4.zhimg.com/v2-0d937cbb1b53f8aad7469ed303d9465f_b.png" data-rawwidth="1436" data-rawheight="877" class="origin_image zh-lightbox-thumb" width="1436" data-original="https://pic4.zhimg.com/v2-0d937cbb1b53f8aad7469ed303d9465f_r.png">

变化不小,所有的 Package (Library?) 都有修改。不过根据经验就知道水果的 API 变化很多都是瞎折腾。说几个我觉得有用的东西:

## Swift 4

把这个提出来说是因为对于普通开发者来说用处最大。说实在的各路的新 A-Kit B-Kit Core-C 看起来酷炫实际上并没有公司敢于用在核心功能上的,毕竟还有那么多不愿意升级 iOS 版本的用户。但是 Swift 只要用了就兼容所有 iOS 版本,不仅让人说一句 666. 只不过是 App 大了几个 MB,who cares.

### 最喜欢的功能:`Codable`

辣鸡知乎编辑器什么时候支持 Markdown? 就算写不出来能不能加个 inline code 的按钮?  不能什么功能微信不做你就不做啊。 @知乎小管家  @周源 @黄继新 @any-zhihu-guys 

#### 背景:data type serialization

现在世界上所有认真的 API 都是 JSON 的了。在语言/标准库层面提供 JSON 基础设施也成为了标配,而 Swift 之前一直都只是 bridge 了 Foundation 的 JSON 解析,在 Swift 内非常水土不服,很难用。比如我们有这样一个 API:

POST xxx.com/user?gender=female&loveRuiyang=yes

{
    "user": [
        {
            "name": "Aragaki Yui",
            "age": 28
        },
        {

            "name": "Miao",
            "age": 20
        }
    ]
}

为了获得首个用户的名字,在纯粹的 Swift 里面我们只能这么写:

if let statusesArray = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]],
    let user = statusesArray[0]["user"] as? [String: Any],
    let username = user["name"] as? String {
    // Finally we got the username
    print("\(username) loves Ruiyang!")
}
以及什么?知乎编辑器指定语言居然没有 Swift? 说好的都是果粉呢?

就非常恶心。这个恶心是为了绕开 Swift 的强大的静态类型检查和 null check. 稍微好一点的写法是:

if let JSONObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]],
    let username = (JSONObject[0]["user"] as? [String: Any])?["name"] as? String {
        // There's our username
}

但是第一,这个还是很恶心。第二,这个 optional chaining 只能拿到一个字段,既不能 deserialize 成为数据类也不能处理 array 中的多个 instance。

#### 用库

为了解决这个问题,我们一般会去用 SwiftyJSON:

let json = JSON(data: dataFromNetworking)
if let userName = json[0]["user"]["name"].string {
  //Now you got your value
}

SwiftyJSON 定义了一个 enum 类型,非常自如地平衡了类型检查和 JSON 本质动态的问题。但是如果我们想 deserialize/deserialize 还是要写一大堆 boilerplate:


struct User {
    let name:String
    let age:Int
}

extension User {
    init(_ json:JSON) {
        self.name = json["name"]
        self.age = json["age"]
        //what if there are 100 fields?
    }
    func toJSON() -> JSON {
        //make a dict and make a JSON
        //oh ugly
    }
}

这就很烦。一般来说,如果一个语言设计之初有一个目标,那么随着语言的进化,任何偏离目标的改动都会完蛋,比如 C++ 的██,██和███;C# 的██。Swift 也是如此。作为一种想要多加利用静态分析,并在工程上 elegant and simple 的语言,反复写 boilerplate 肯定是要被拍死在沙滩上的。

#### 反射

之前我们想要利用 reflection 解决问题:evermeer/EVReflection, 但是很可惜 Swift 就没有好好设计 reflection 的东西,可以说 Mirror 这一套东西完全是给 playground 准备的,很残。因而这个库也不幸地失败了,官方说明如下:

It's not possible in Swift to use .setObjectForKey for:

  • nullable type fields like Int?
  • properties based on an enum
  • an Array of nullable objects like [MyObject?]
  • a Set like Set
  • generic properties like var myVal:T = T()
  • structs like CGRect or CGPoint

尽管有这样那样的 workground, 这样严重的限制使得本库成为了工程上不实用的东西。

可以想见,尽管不能反射了,我们仍然可以使用 code generation 来解决问题。这样的难度在于每次改动数据结构都要 regenerate 一遍,而且有种种可能的 hidden bugs, 比如 data type declaration 和 generated code 版本不一致这样的,很难发现。最好的还是让 Swift 自己去 generate 这东西。于是——

#### Codable

终于,在所有程序员都在哭着写 boilerplate 之后,水果终于认识到第三方库不能很好地解决这个问题。他们推出了自己的 (hidden) code generation solution.

proposal 见 apple/swift-evolutionapple/swift-evolution.

首先,我们不需要写任何 encode 和 decode 的代码了,只要继承了 Codable, 编译器会自动根据 field name 来为我们进行序列化。官方代码示例:

// Codable implies Encodable and Decodable
// If all properties are Codable, protocol implementation is automatically generated by the compiler:
public struct Location : Codable {
    public let latitude: Double
    public let longitude: Double
}

public enum Animal : Int, Codable {
    case chicken = 1
    case dog
    case turkey
    case cow
}

public struct Farm : Codable {
    public let name: String
    public let location: Location
    public let animals: [Animal]
}
只要一个 class/struct/enum 的所有 field 都遵循 Codable, 它自己就可以无痛遵循 Codable. 简直无比感人。尤其是所有有意义的内置类型(String, Int, Double, ..., Date, Array, Dictionary) 都已经写好了 Codable(我要哭出来惹)

现在想要 Encode 只需要一行:

let payload: Data = try JSONEncoder().encode(farm)

而 Decode 只需要另一行:

let farm = try JSONDecoder().decode(Farm.self, from: payload)

(水果爸爸好)

你还可以自定义 Serialization 函数,并且可以定义只能 Encode 和只能 Decode 的东西。还可以定义序列时的 number limit 等等细节。反正工程上的东西水果都帮你想好了。

讲道理这个东西在理论上没什么了不得的地方,但是工程上简直让人泪流满面。太感人了。

不过我不喜欢 Swift 的异常处理。实际上……我不喜欢任何异常处理。以前在自己的代码里不会有 try, 但是现在看来代码中即将充满了 try. Sad.

<img src="https://pic1.zhimg.com/v2-82665133c539a0459c1fd68d8e3529f0_b.png" data-rawwidth="84" data-rawheight="95" class="content_image" width="84">

### Protocols, Associated Types and Constraints

直到 Swift 3, 水果号称的 Protocol-oriented Programming 也没成为完全体。一个真正基于 Protocol 的代码中应该可以有这样的代码:

protocol JSONEncodable {
    func toJSON() -> String
}

extension Int : JSONEncodable {
    func toJSON() -> String {
        return "\(self)"
    }
}
//some implementations for string, float, date

extension Array : JSONEncodable where Element : JSONEncodable {
    func toJSON() -> String {
        let content = self.map { $0.toJSON() }.joined(separator: ",")
        return "[\(content)]"
    }
}

然鹅……事实是残酷的。

<img src="https://pic2.zhimg.com/v2-7fd25f65c5e0efb0ef833468a6cdc211_b.png" data-rawwidth="745" data-rawheight="124" class="origin_image zh-lightbox-thumb" width="745" data-original="https://pic2.zhimg.com/v2-7fd25f65c5e0efb0ef833468a6cdc211_r.png">extension 中是不能加 where 的。这就很难受了。之前一直有小道消息说本来会有这个功能只是没做完。现在终于有了。请看:

extension 中是不能加 where 的。这就很难受了。之前一直有小道消息说本来会有这个功能只是没做完。现在终于有了。请看: apple/swift-evolution
apple/swift-evolution
<img src="https://pic3.zhimg.com/v2-fc3e969910a617f463400acd0b425b46_b.png" data-rawwidth="73" data-rawheight="78" class="content_image" width="73">