Tư duy trong ngôn ngữ Swift, Phần 4: map all the things!
Array vs. Optional
Ở phần trước, chúng ta đã học được function map()
và flatmap()
trên Array<T>
:
1 |
|
Đ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()
và flatmap()
trên Optional<T>
cũng hoạt động tương tự:
1 |
|
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:
1 |
|
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 ??
1 |
|
Nó sẽ hoạt động vì nếu iconName
là nil
, 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
.
1 |
|
Đợ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.
1 |
|
Dưới đây là những gì flatMap
làm trong thực tế:
- Nếu
iconName
lànil
, nó sẽ trả về trực tiếpnil
(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ộtUIImage
sử dụngString
, 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ạoUIImage
không thành công.
Ngắn gọn, item.icon
sẽ có giá trị non-nil
nếu itemDesc["icon"] as? String
là non-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.
1 |
|
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.
1 |
|
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?
và 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.