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

Trong phần 1, ta đã biết cách làm thế nào để tránh force-unwrapping optionals, để giải cứu ponies 🐴 và tránh code bị crash. Trong phần 2 này, ta sẽ chỉnh sửa code để làm nó trở nên Swift-er hơn thông qua map()flatMap().

Phần 2 này sẽ nói về mapflatMap trên Arays

Nhắc lại, tư duy trong Swift

Đây là code khi kết thúc phần 1

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
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 {
                // If we failed to unserialize the JSON or that JSON wasn't an NSArray,
                // then bail early with an empty array
                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
    }
}

Mục đích là áp dụng nhiều patterns và cú pháp Swift-er để làm cho code tốt hơn và ngắn gọn hơn.

Giới thiệu map()

map() là một method của Array. map() có thể lấy một function như là một tham số của nó, điều này giải thích làm thế nào mà mỗi phần tử trong mảng biến đổi thành một giá trị mới. Nó cho phép biến đổi một mảng [X] thành mảng [Y] chỉ bằng cách giải thích làm thế nào để biến đổi X -> Y, mà không cần tạo ra 1 mảng tạm thời làm trung gian.

Do đó, trong trường hợp này, thay vì sử dụng vòng lặp for như đã làm trước đó, ta có thể áp dụng map cho jsonItems(JSON array của NSDictionary) và đưa ra cách để biến đổi mỗi NSDictionary thành một ListItem instance.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
return jsonItems.map { (itemDesc: NSDictionary) -> ListItem in
    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
    }
    return item
}

Đó có vẻ là một sự thay đổi đơn giản, nhưng nó cho phép ta tập trung vào câu hỏi “làm cách nào để biến đổi một NSDictionary thành một ListItem” - đây là mấu chốt của vấn đề - và quan trọng hơn là tránh được việc tạo một mảng trung gian như trong Obj. Luôn luôn tránh tình trạng này khi có thể.

Hư hỏng dữ liệu

Có một vấn đề với code ở trên là chúng ta vẫn tạo ra được một ListItem thậm chí nếu dữ liệu đầu vào không đúng. Nhưng nếu một phần trong NSDictionary không đúng thì chúng ta vẫn sẽ kết thúc ở đầu ra với mảng rỗng ListItem(). Điều này thực sự không có ý nghĩa.

Quan trọng hơn, chúng ta vẫn đang giết chết một vài ponies 🐴 như cách ta đang dùng NSURL! và code vẫn cho phép tạo ra một ListItem instances mà không có NSURL(item.url không bị ảnh hưởng nếu không có giá trị trong "url") và code của chúng ta sẽ bị crash nếu chúng ta cố gắng truy cập NSURL! không hợp lệ.

Để giải quyết, chúng ta có thể làm cho việc biến đổi đơn giản hơn bằng cách trả về một nil ListItem nếu đầu vào không hợp lệ. Việc này được đánh giá cao hơn so với ListItem bị hỏng hoặc rỗng.

swift
1
2
3
4
5
6
return jsonItems.map { (itemDesc: NSDictionary) -> ListItem? in
    guard …/* condition for valid data */… else { return nil }
    let realValidItem = ListItem()
    … /* fill the ListItem with the values */
    return realValidItem
}

Nhưng nếu chúng ta vẫn dùng jsonItems.map để biến đổi từ NSDictionary -> ListItem?, nó vẫn có thể tạo ra một mảng [ListItem?] mà mảng này vẫn chứa những nil items tại những vị trí mà NSDictionary không hợp lệ. Cách này là tốt hơn trước đó, nhưng vẫn chưa giải quyết được việc này.

Sử dụng flatMap()

flatMap() sẽ giải quyết vấn đề còn tồn tại ở trên. flatMap() tương tự như map() nhưng nó sử dụng cách biến đổi T->U? thay vì T->U, và nó không add những item mà giá trị sau khi biến đổi là nil (tức là thằng nào nil thì không add vào mảng cuối cùng)

Áp dụng và chúng ta có code như sau:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
return jsonItems.flatMap { (itemDesc: NSDictionary) -> ListItem? in
    guard let title = itemDesc["title"] as? String,
        let urlString = itemDesc["url"] as? String,
        let url = NSURL(string: urlString)
        else { return nil }
    let li = ListItem()
    if let icon = itemDesc["icon"] as? String {
        li.icon = UIImage(named: icon)
    }
    li.title = title
    li.url = url
    return li
}

Bây giờ code chỉ trả về một ListItem hợp lệ nếu các đầu vào đều có giá trị (trong đó NSURL đảm bảo sẽ không nil). Nếu đầu vào không hợp lệ, guard statement sẽ trả về nil sớm, và flatMap không add các thành phần không hợp lệ này vào mảng được trả về.

Điều này làm cho code tốt hơn và an toàn hơn, phải không nào? Và chúng ta loại bỏ các vấn đề hư hỏng dữ liệu và các rủi ro có thể gặp.

Kết luận

Trong phần này, chúng ta đã biết cách làm thế nào để thay thế vòng lặp for bằng map hoặc flatMap, và chúng ta đảm bảo code an toàn hơn bằng cách tránh tạo đầu ra không phù hợp khi dữ liệu đầu vào không hợp lệ.

Trong phần tiếp theo, chúng ta sẽ xem xét làm thế nào để biến đổi ListItem như một struct và khám phá cách dùng khác của mapflatMap - đặc biệt ở Optionals.

Trong khi chờ đợi phần tiếp theo, hãy dành thời gian để khám phá sức mạnh của map()flatMap() trên mảng. Tôi biết chúng có thể là đáng sợ hay phức tạp lúc đầu, nhưng một khi bạn hiểu được nó, bạn sẽ muốn sử dụng chúng ở khắp mọi nơi!