Bài này được mình dịch từ đây: alisoftware.github.io

Bài này là 1 phần của series. Bạn có thể đọc các phần còn lại ở đây: phần 1, phần 1 bổ sung, phần 2, phần 3, phần 4

Tôi thấy rằng, những-người-mới-tìm-hiểu về Swift thường hay cố chuyển đổi code của họ từ ObjC sang Swift. Nhưng phần khó nhất để code trong Swift không phải là về cú pháp mà là cách bạn tư duy trong Swift, sử dụng những khái niệm mới trong Swift mà không có trong ObjC.

Trong series bài này, tôi sẽ lấy một ví dụ về code ObjC rồi chuyển đổi sang Swift và giới thiệu nhiều khái niệm của Swift trong quá trình chuyển đổi.

Phần 1 nói về: optionals, forced-unwrapped optionals, ponies, if let, guard, và 🍰.

ObjC code

Bạn muốn tạo ra danh sách các item (để hiển thị trong TableView chẳng hạn) - mỗi item có icon, title và url - nó được khởi tạo từ JSON. Đây là code ObjC:

objectivec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ListItem : NSObject
@property(strong) UIImage* icon;
@property(strong) NSString* title;
@property(strong) NSURL* url;
@end

@implementation ListItem
+(NSArray*)listItemsFromJSONData:(NSData*)jsonData {
    NSArray* itemsDescriptors = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
    
    NSMutableArray* items = [NSMutableArray new];
    for (NSDictionary* itemDesc in itemsDescriptors) {
        ListItem* item = [ListItem new];
        item.icon = [UIImage imageNamed:itemDesc[@"icon"]];
        item.title = itemDesc[@"title"];
        item.url = [NSURL URLWithString:itemDesc[@"url"]];
        [items addObject:item];
    }
    return [items copy];
}
@end

Chuyển đổi trực tiếp sang Swift

Với những-người-mới-tìm-hiểu Swift, đây là cách chuyển đổi code sang Swift thường thấy:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ListItem {
    var icon: UIImage?
    var title: String = ""
    var url: NSURL!
    
    static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
        let jsonItems: NSArray = try! NSJSONSerialization.JSONObjectWithData(jsonData!, options: []) as! NSArray
        let items: NSMutableArray = NSMutableArray()
        for itemDesc in jsonItems {
            let item: ListItem = ListItem()
            item.icon = UIImage(named: itemDesc["icon"] as! String)
            item.title = itemDesc["title"] as! String
            item.url = NSURL(string: itemDesc["url"] as! String)!
            items.addObject(item)
        }
        return items.copy() as! NSArray
    }
}

Với những-người-có-nhiều-kinh-nghiệm trong Swift, họ sẽ thấy rất nhiều vấn đề xuất hiện trong đoạn code trên.

Có vấn đề gì với đoạn code trên?

Vấn đề là, sử dụng implicitly-unwrapped optionals (value!), force-casts (value as! String) và force-try (try!) ở mọi nơi. Đây là thói quen rất xấu của những-người-mới-tìm-hiểu Swift.

Optionals là những người bạn: vì họ ép buộc bạn phải suy nghĩ về những trường hợp mà values là nil và nên làm gì trong hoàn cảnh đó (khi values là nil). Ví dụ như: “Tôi nên hiển thị cái gì khi không có icon? Tôi có nên sử dụng placeholder trong TableViewCell?”.

Đây là những trường hợp mà chúng ta thường lãng quên khi viết code ObjC, nhưng Swift giúp chúng ta không bỏ quên các trường hợp bị nil này. Vì vậy, bằng cách dùng force-unwrapping, chúng ta đã không sử dụng lợi thế này của Swift và làm cho code-của-bạn bị crash khi nó là nil.

Không bao giờ force-unwrapping một value, trừ khi bạn thực sự biết những gì bạn đang làm. Hãy nhớ rằng mỗi khi bạn thêm ! chỉ vì compiler gợi ý, bạn đang giết chết một pony 🐴. [0]

Điều đáng buồn, những sai lầm này được khuyến khích bởi Xcode, bởi vì khi error thông báo: value of optional type ‘NSArray?’ not unwrapped. Did you mean to use ! or ?, và đề nghị sửa chữa bằng cách thêm một dấu ! ở cuối 🙀.

Giải cứu ponies

Làm thế nào để tránh được dấu ! ở mọi nơi? Sau đây là một số cách:

  • Sử dụng optional binding if let x = optional { /* use x */ }
  • Sử dụng as? thay vì as!, trả về nil nếu cast fails; có thể sử dụng kết hợp với if let
  • Sử dụng try? thay vì try!, trả về nil nếu expression failed [1]

Hãy xem code của chúng ta sau khi sử dụng những quy tắc trên [2]:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ListItem {
    var icon: UIImage?
    var title: String = ""
    var url: NSURL!
    
    static func listItemsFromJSONData(jsonData: NSData?) -> NSArray {
        if let nonNilJsonData = jsonData {
            if let jsonItems: NSArray = (try? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: [])) as? NSArray {
                let items: NSMutableArray = NSMutableArray()
                for itemDesc in jsonItems {
                    let item: ListItem = ListItem()
                    if let icon = itemDesc["icon"] as? String {
                        item.icon = UIImage(named: icon)
                    }
                    if let title = itemDesc["title"] as? String {
                        item.title = title
                    }
                    if let urlString = itemDesc["url"] as? String {
                        if let url = NSURL(string: urlString) {
                            item.url = url
                        }
                    }
                    items.addObject(item)
                }
                return items.copy() as! NSArray
            }
        }
        return [] // In case something failed above
    }
}

Pyramid of doom (Kim tự tháp chết chóc)

Một vấn đề xuất hiện, là khi thêm if let ở khắp mọi nơi, dẫn đến là quá nhiều if lồng vào nhau, đây là pyramid of doom (kim tự tháp chết chóc).

Một vài cách có thể giúp giảm bớt tình huống này:

  • kết hợp nhiều lệnh if let vào làm một if let x = opt1, y = opt2
  • sử dụng lệnh guard, cho phép ta giải quyết sớm 1 function nếu điều kiện không thỏa mãn, tránh được phần còn lại của function.
swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ListItem {
    var icon: UIImage?
    var title: String = ""
    var url: NSURL!
    
    static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
        guard let nonNilJsonData = jsonData,
            let json = try? NSJSONSerialization.JSONObjectWithData(nonNilJsonData, options: []),
            let jsonItems = json as? Array<NSDictionary>
            else {
                // nếu 1 trong các lệnh guard let trả về nil
                // hoặc JSON không phải là mảng của NSDictionaries,
                // thì trả về chuỗi rỗng, không cần thực hiện các lệnh bên dưới
                return []
        }
        
        var items = [ListItem]()
        for itemDesc in jsonItems {
            let item = ListItem()
            if let icon = itemDesc["icon"] as? String {
                item.icon = UIImage(named: icon)
            }
            if let title = itemDesc["title"] as? String {
                item.title = title
            }
            if let urlString = itemDesc["url"] as? String, let url = NSURL(string: urlString) {
                item.url = url
            }
            items.append(item)
        }
        return items
    }
}

Lệnh guard rất tuyệt. Vì ở phần đầu của function, nó giúp ta tập trung kiểm tra các biến-đầu-vào hợp lệ và trong phần còn lại, không cần bận tâm đến việc kiểm tra nữa. Nếu biến-đầu-vào không như mong đợi, chúng sẽ được giải quyết sớm và chúng ta chỉ tập trung vào phần chúng ta mong đợi ở sau.

Không phải Swift nhỏ gọn hơn ObjC sao?

Đúng là code trong Swift có vẻ phức tạp hơn so với ObjC. Đừng lo, chúng ta sẽ làm cho nó nhỏ gọn hơn trong phần 2.

Nhưng quan trọng nhất, code Swift này an toàn hơn nhiều so với ObjC. Trong thực tế, code ObjC ngắn hơn chỉ vì chúng ta quên để thực hiện rất nhiều các bài test an toàn. Thậm chí nếu nhìn có vẻ khá phổ biến, code ObjC có thể sẽ bị crash trong một số trường hợp, nếu như chúng ta đưa cho nó một JSON không hợp lệ, hoặc một trong số đó không được cấu trúc như một array of dictionary of strings (ví dụ: nếu ai đó tạo JSON và nghĩ rằng “icon” chính là kiểu Boolean(vì nghĩ nếu item có icon hoặc không), thay vì kiểu string …). Chỉ đơn giản, chúng ta quên xử lý tất cả những trường hợp này trong ObjC, vì ObjC không giúp chúng ta suy nghĩ về những trường hợp này trong khi Swift buộc chúng ta phải xem xét.

Vì vậy, code ObjC sẽ ngắn hơn bởi vì chúng ta đã bỏ qua một số thứ. Làm cho code ngắn hơn nhưng phải đánh đổi bằng việc code có thể bị crash.

Kết luận

Swift được thiết kế để an toàn hơn. Đừng bỏ qua optionals bởi force-unwrapping: khi bạn nhìn thấy một ! trong code Swift, bạn biết rằng nó có thể là một code tệ hại và cái gì đó sai sai ở đây.

Trong phần 2 của series này, chúng ta sẽ xem thử làm thế nào cho code Swift ngắn gọn hơn và tiếp tục tư duy trong Swift bằng cách thay thế vòng lặp forif let bằng mapflatMap.


[0]. Chú thích của người dịch: Pony ở đây là tác giả ví code của bạn chạy như ngựa. Việc giết chết ponies là tác giả muốn ám chỉ việc sử dụng dấu ! bừa bãi mà không suy nghĩ thông qua các cách sau: implicitly-unwrapped optionals (value!), force-casts (value as! String) và force-try(try!) khiến code của bạn có thể crash bất cứ lúc nào.

[1]. Lưu ý rằng try? âm thầm loại bỏ các error: khi sử dụng nó, bạn không thể biết tại sao code failed. Vì vậy, để tốt hơn hãy sử dụng do { try … } catch { } thay vì try? nếu có thể. Trong trường hợp này, chúng ta muốn trả về một mảng rỗng nếu quá trình phân tích JSON failed dù bất kỳ lý do gì, dùng try? ở đây là ok.

[2]. Như bạn thấy, tôi đã giữ lại 1 as! ở trong đoạn code items.copy() as! NSArray. Đôi khi nó có thể giết chết 1 pony force-cast, nếu bạn phải thực sự hiểu rằng kiểu trả về ở đây không thể khác được nữa, giống như kiểu mutableArray.copy(). Trường hợp ngoại lệ như ở đây là rất hiếm, bạn luôn nghĩ đến cách sử dụng as? đầu tiên.