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

Array vs. Optional

Ở phần trước, chúng ta đã học được function map()flatmap() trên Array<T>:

swift
1
2
3
// Method on Array<T>
    map( transform: T ->          U  ) -> Array<U>
flatMap( transform: T ->    Array<U> ) -> Array<U>

Điều này có nghĩa là, cho 1 biến đổi T->U bạn có thể biến đổi 1 mảng của T thành 1 mảng của U. Đơn giản chỉ cần gọi map(transform: T->U) trên Array<T> và nó sẽ trả về một Array<U>

function map()flatmap() trên Optional<T> cũng hoạt động tương tự:

swift
1
2
3
// Method on Optional<T>
    map( transform: T ->          U  ) -> Optional<U>
flatMap( transform: T -> Optional<U> ) -> Optional<U>

map() on Optionals

map method sẽ làm gì ở kiểu Optional<T> (còn gọi là T?)?

Nó sẽ xử lý tương tự như ở Array<T>, nó lấy nội dung của Optional<T>, và biến đổi nó bằng cách sử dụng biến đổi T->U, kết quả tạo ra một Optional<U> mới.

Nó tương tự như những gì Array<T>.map thực hiện: nó áp dụng biến đổi mỗi phần tử bên trong của Array<T> (hoặc Optional<T>) và trả về một Array<U> (hoặc Optional<U>) mới.

Quay trở lại ví dụ

Trong phần code gần nhất, chúng ta có 1 itemDesc["icon"] như là 1 String?, và chúng ta muốn biến nó thành 1 UIImage; nhưng UIImage(named:) nhận 1 tham số kiểu String, không phải là 1 String?, do đó chúng ta cần 1 kiểu String và điều này xảy ra khi optional thực sự có giá trị bên trong (not nil).

Một giải pháp có thể sử dụng Optional Binding để làm điều này:

swift
1
2
3
4
5
6
let icon: UIImage?
if let iconName = itemDesc["icon"] as? String {
  icon = UIImage(named: iconName)
} else {
  icon = nil
}

Nhưng việc này sẽ sử dụng nhiều dòng trong cho 1 phép toán đơn giản. Trong ví dụ trước, chúng ta đã sử dụng giải pháp thay thế, đó là nil-coalescing operator ??

swift
1
2
let iconName = itemDesc["icon"] as? String
let icon = UIImage(named: iconName ?? "")

Nó sẽ hoạt động vì nếu iconNamenil, chúng ta sẽ khởi tạo UIImage và sử dụng UIImage(named: "") mà thực sự nó là 1 nil image.

Hãy sử dụng map

Vậy tại sao không thử sử dụng map? Thực ra, chúng ta muốn unwrap Optional<String> nếu nó không nil, nó sẽ chuyển giá trị bên trong cho UIImage và trả về UIImage.

swift
1
2
let iconName = itemDesc["icon"] as? String
item.icon = iconName.map { imageName in UIImage(named: imageName) }

Đợi một chút, đoạn code trên sẽ không biên dịch. Bạn có thể đoán tại sao không?

Có gì sai ở đây?

Vấn đề với code bên trên là UIImage(named: …) cũng trả về 1 optional. Nếu không có image nào có tên như vậy, nó không thể tạo ra 1 UIImage, và cuối cùng nó cũng trả về nil.

Vấn đề ở đây nằm trong closure, chúng ta đưa đầu vào là 1 String và đầu ra là 1 UIImage? Và nếu chúng ta sử dụng function map 1 lần nữa, nó là 1 closure của kiểu T->U và trả về U?. Do đó ở trường hợp chúng ta, đầu vào là UIImage? và đầu ra sẽ là UIImage??. Đó là 1 double-optional.

flatMap() sẽ giải quyết vấn đề này

flatMap giống như map, nhưng nó thực hiện 1 biến đổi T->U? (thay vì biến đổi T->U), và kết quả của biến đổi này chỉ có 1 level của optional. Đây là điều chúng ta mong muốn.

swift
1
2
let iconName = itemDesc["icon"] as? String
item.icon = iconName.flatMap { imageName in UIImage(named: imageName) }

Dưới đây là những gì flatMap làm trong thực tế:

  • Nếu iconNamenil, nó sẽ trả về trực tiếp nil (nhưng kiểu như UIImage?)
  • Nếu iconName không phải là nil, nó sẽ áp dụng các biến đổi, và cố gắng tạo ra một UIImage sử dụng String, và trả về kết quả - nghĩa là đã có UIImage?, nhưng nó có thể nil nếu quá trình khởi tạo UIImage không thành công.

Ngắn gọn, item.icon sẽ có giá trị non-nil nếu itemDesc["icon"] as? Stringnon-nil VÀ khởi tạo UIImage(named: imageName) thành công.

Sử dụng init như một closure

Đoạn code trên cũng có thể được viết theo cách ngắn gọn hơn, như Xcode 7 đã giới thiệu constructors của 1 kiểu thông qua thuộc tính init của nó.

Điều này có nghĩa là UIImage.init thực sự là 1 function có sẵn, mà nó có đầu vào là String và trả về 1 UIImage?. Do đó, chúng ta có thể sử dụng nó trực tiếp như là 1 tham số của flatMap, không cần wrap nó trong 1 closure.

swift
1
2
let iconName = itemDesc["icon"] as? String
item.icon = iconName.flatMap(UIImage.init)

Wow! kỳ diệu chưa!

Ý kiến:

  • Tôi cảm thấy đọc code trên khó khăn hơn và ý kiến cá nhân là nên sử dụng một closure cụ thể ở đây, để cho đoạn code rõ ràng hơn và ít mơ hồ. Nhưng đó chỉ là một vấn đề sở thích cá nhân.

  • Nó thực sự biên dịch nhưng có vẻ không như mong đợi - Tôi đoán Swift compiler thực hiện map UIImage.init đến {UIImage(contentsOfFile:$0)} thay vì sử dụng {UIImage(named:$0)}. Thêm một lý do nữa để sử dụng closure rõ ràng ở đây.

Swift code cuối cùng

Áp dụng bài học ở trên vào đoạn code.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ListItem {
    let icon: UIImage?
    let title: String
    let url: NSURL
    
    static func listItemsFromJSONData(jsonData: NSData?) -> [ListItem] {
        guard let jsonData = jsonData,
            let json = try? NSJSONSerialization.JSONObjectWithData(jsonData, options: []),
            let jsonItems = json as? Array<NSDictionary> else { return [] }
        
        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 iconName = itemDesc["icon"] as? String
            let icon = iconName.flatMap { UIImage(named: $0) }
            return ListItem(icon: icon, title: title, url: url)
        }
    }
}

Cùng nhìn lại ObjectiveC code

Mất một ít thời gian để so sánh đoạn code Swift với đoạn code ObjectiveC ban đầu. Chúng ta đã học được khá nhiều thứ từ đó.

Nếu bạn so sánh ObjectiveC code vs Swift code, bạn sẽ nhận ra Swift code ngắn hơn không bao nhiêu (5 + 15 LoC1 cho ObjectiveC vs 19 LoC1 cho Swift), nhưng nó an toàn hơn.

Đặc biệt, trong Swift code chúng ta học được cách sử dụng guard, try?as? buộc chúng ta phải kiểm tra mọi thứ trước khi dùng, những điều mà ObjC code không bận tâm và code có thể bị crashed 💣💥. Vì vậy có lẽ nó cùng kích thước, nhưng ObjectiveC code là cách nguy hiểm hơn!

Kết luận

Với loạt bài này, tôi hy vọng bạn nhận ra rằng bạn không nên cố gắng chuyển ObjectiveC code sang Swift code trực tiếp. Thay vào đó, hãy tư duy theo cách khác trong ngôn ngữ Swift. Tốt hơn hết là bắt đầu từ một clean state và viết lại code của bạn với các Swift idioms trong đầu hơn là cố gắng dịch sang ObjectiveC code trực tiếp.

Tôi không nói rằng nó dễ dàng. Để thay đổi cách suy nghĩ của bạn khi mà bạn đã quen với cách tư duy trong ObjC, cách sử dụng patterns và phương pháp viết code có thể mất một thời gian. Nhưng nó chắc chắn là tốt hơn.