Lập trình Swift: Lười là thông minh!

Tác giả: Lê Xuân Quỳnh

Nghe hơi vô lý nhưng rõ ràng Apple đã tạo điều kiện để Developer lười theo cách thông minh của họ.

Chúng ta sẽ tìm hiểu vì sao nói lười là thông minh? Nghe hư cấu nhỉ? 🤣

Nào cùng bắt đầu tìm hiểu về lười = lazy!

Mảng lười

Mở bài hơi sốc 1 xíu thôi, cùng theo dõi đoạn code sau nhé:

var numbers: [Int] = [1, 2, 3, 6, 9]
let modifiedNumbers = numbers
.filter { number in
print("Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Doubling the number")
return number * 2
}
print(modifiedNumbers)

/*
kết quả:
Even number filter
Even number filter
Even number filter
Even number filter
Even number filter
Doubling the number
Doubling the number
[4, 12]
*/

Như các bạn thấy, đầu tiên chúng ta tạo 1 mảng số nguyên. Sau đó dùng hàm filter để tìm ra các số chẵn(chúng ta có 2, 6). Tiếp theo chúng ta dùng hàm map để nhân đôi các số sau khi filter.

Kết quả chúng ta có 4, 12.

Bây giờ chúng ta sẽ dùng lazy như nào? Hãy theo dõi đoạn code sau:

let modifiedLazyNumbers = numbers.lazy
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedLazyNumbers)
// Prints:
// LazyMapSequence>, Int>(_base: Swift.LazyFilterSequence>(_base: [1, 2, 3, 6, 9], _predicate: (Function)), _transform: (Function))

bằng việc dùng thêm từ khóa lazy để chỉ định rằng mảng “lười” là mảng mà chúng ta cần thao tác. Khi đã xác định bản chất nó như vậy, thì nó lười đi trông thấy: không 1 dòng print nào của filter và map được hiển thị.

Rõ ràng nó làm biếng. Thêm dòng code tiếp vào sau đoạn trên:

print(modifiedLazyNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
*/

Vậy là bây giờ đã thấy xuất hiện 2 dòng print và 1 dòng double và kết quả là 4 sau khi nhân đôi số đầu tiên lên.

Tại sao lại vậy? Do yêu cầu chúng ta là chỉ lấy phần tử đầu tiên, cho nên mảng lười chỉ thao tác cho đến khi phần tử đầu tiên xuất hiện. Do vậy chúng ta hạn chế được những động tác thừa không cần thiết.

Rõ ràng trường hợp này là 1 sự lười = thông minh phải không nào 😆

Một trường hợp hay áp dụng trong thực tế:

lập trình swift
Tìm kiếm trong ứng dụng

Trong trường hợp như hình, khi chúng ta tìm chữ nào đó thì ứng dụng sẽ bắt đầu hiển thị các places liên quan tới và đồng thời load image cho place đó.

Ví dụ chúng ta muốn tìm các địa điểm bắt đầu bằng chữ “h”.

Khi không dùng lazy, theo dõi đoạn code sau:

let places = ["Ho Chi Minh", "Ha Noi", "Hue", "Da Nang", "Vung Tau", "Nha Trang"]
places
.filter { place in
print("filtered name")
return place.lowercased().first == "h"
}.forEach { place in
print("Fetch image for \(place)")
}

/*
Ket qua:
filtered name
filtered name
filtered name
filtered name
filtered name
filtered name
Fetch image for Ho Chi Minh
Fetch image for Ha Noi
Fetch image for Hue
*/

Như kết quả, chúng ta sẽ duyệt toàn bộ những địa điểm và lấy các địa điểm có tên bắt đầu bằng chữ “h”, sau đó rồi mới fetch ảnh về.

Trường hợp dùng lazy, theo dõi đoạn code sau:

 let places = ["Ho Chi Minh", "Ha Noi", "Hue", "Da Nang", "Vung Tau", "Nha Trang"]
places.lazy
.filter { place in
print("filtered name")
return place.lowercased().first == "h"
}.forEach { place in
print("Fetch image for \(place)")
}
/*
Ket qua:
filtered name
Fetch image for Ho Chi Minh
filtered name
Fetch image for Ha Noi
filtered name
Fetch image for Hue
filtered name
filtered name
*/

Chúng ta tìm ra Ho Chi Minh, sau đó ảnh của địa điểm này sẽ được load luôn. Sau đó tìm ra Ha Noi, va Hue cũng làm tương tự. Rõ ràng nếu không có lazy, thì mảng sẽ phải duyệt qua toàn bộ, sau đó mới fetch ảnh về. Còn với lazy, chúng ta duyệt – tìm ra – fetch về luôn. Theo bạn phương án lười này có thông minh không? Rõ ràng nó được 💯 điểm thông minh cho trường hợp này.

Tuyển dụng lập trình IOS lương cao hấp dẫn tại đây.

Lazy không lưu cache

Lười thì cũng thông minh đấy, nhưng 1 số trường hợp lại không thực sự như vậy. OK, nói đi thì phải nói lại, dù sao thì chúng ta phải quyết định nó sẽ dùng như nào, nên phải hiểu bản chất của nó trước.

Quay lại ví dụ đầu tiên, chúng ta theo dõi đoạn code sau:

let modifiedLazyNumbers = numbers.lazy
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedLazyNumbers.first!)
print(modifiedLazyNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
*/

Chúng ta yêu cầu lấy phần tử đầu tiên 2 lần qua 2 lệnh print, và để ý đoạn log mà chương trình trả về, nó hiển thị rất nhiều!. Với các mảng bình thường, theo dõi đoạn code sau:

let modifiedNumbers = numbers
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedNumbers.first!)
print(modifiedNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
Lazy Doubling the number
4
4
*/

Nó duyệt qua 1 lần, và lưu vào 1 mảng, sau đó chúng ta dù có gọi nhiều lần phần tử đầu tiên, nó chỉ hiển thị kết quả cuối cùng.

Vậy trong trường hợp này, các bạn sẽ rút ra 2 kết luận:

  • lazy thì không lưu vào cache. Nó sẽ thực hiện lại toàn bộ quy trình từ đầu.
  • lazy trong trường hợp này là ngu ngốc!

OK, vậy để đặt lại cái tiêu đề bài viết cho đỡ chửi thì có lẽ là dùng lazy như nào cho thông minh phải không? Nhưng thôi tôi xin phép để tiêu đề vậy để nhận thêm gạch đá.

Luôn tận dụng API của apple

Với ví dụ trên, chúng ta có thể thay đổi bằng cách dùng API có sẵn của apple và nhận được kết quả tương tự.

Xem xét ví dụ sau:

let collectionOfNumbers = (1…1000000)
let lazyFirst = collectionOfNumbers.lazy
.filter {
print("filter")
return $0 % 2 == 0
}.first
print(lazyFirst) // Prints: 2

Với mảng nhiều phần tử như trên, lazy chỉ thực hiện đúng 2 lần cho ngữ cảnh trên. Và Apple cũng có 1 API để làm điều đó:

let firstWhere = collectionOfNumbers.first(where: { $0 % 2 == 0 })
print(firstWhere) // Prints: 2

Tội gì không dùng hàng Apple anh em nhỉ?

Những trường hợp khác để tối ưu hiệu năng cho ứng dụng

Apple có nhiều API lắm, kết quả thì giống nhau và đây là 1 vài sự so sánh giữa chúng.

Nên dùng contains thay vì first(where:) != nil

Hai API này cùng xác định 1 mảng có chứa 1 phần tử nào đó hay không, nhưng hiệu năng contains sẽ tốt hơn là first(where:) != nil.

Phương án tốt:

let numbers = [0, 1, 2, 3] 
numbers.contains(1)

Phương án tệ:

let numbers = [0, 1, 2, 3] 
numbers.filter { number in number == 1 }.isEmpty == false 
numbers.first(where: { number in number == 1 }) != nil

Dùng isEmpty thay vì count == 0

Khi để kiểm tra 1 mảng có rỗng hay không thì dùng property isEmpty. Còn nếu bạn dùng count, nếu 1 mảng không tuân theo RandomAccessCollection protocol, việc tính count sẽ duyệt qua toàn bộ phần tử của mảng.

Best practice:

let numbers = [] 
numbers.isEmpty

Bad practice:

let numbers = [] 
numbers.count == 0

Tương tự với string – được coi là tập hợp của các ký tự thì dùng isEmpty vẫn là tốt hơn dùng so sánh count với 0.

Lọc phần tử đầu tiên trong tập hợp với điều kiện

Có 2 cách để thực hiện điều đó: dùng filter và sau đó dùng first. Hoặc dùng first where.

Cách 1, chúng ta sẽ duyệt toàn bộ tập hợp, sau đó lấy phần tử đầu tiên. Cách 2 chúng duyệt cho đến khi nào tìm thấy phần tử đầu tiên thì dùng. Rõ ràng là tốt hơn.

Best practice:

let numbers = [3, 7, 4, -2, 9, -6, 10, 1] 
let firstNegative = numbers.first(where: { $0 < 0 })

Bad practice:

let numbers = [3, 7, 4, -2, 9, -6, 10, 1] 
let firstNegative = numbers.filter { $0 < 0 }.first

Điều này cũng đúng với các trường hợp last where.

Tìm phần tử lớn nhất, nhỏ nhất của tập hợp

Có 2 cách để tìm phần tử lớn nhất, nhỏ nhất của mảng:

  • dùng toán tử có sẵn min hoặc max.
  • Dùng toán tử sort sau đó lấy first để tìm min hoặc last để tìm max

Best practice:

let numbers = [0, 4, 2, 8] 
let minNumber = numbers.min() 
let maxNumber = numbers.max()

Bad practice:

let numbers = [0, 4, 2, 8] 
let minNumber = numbers.sorted().first 
let maxNumber = numbers.sorted().last

Xác định toàn bộ mảng phù hợp với điều kiện nào đó

Ví dụ chúng ta muốn mảng chứa toàn bộ là số chẵn. Có 2 phương án để dùng.

Best practice:

let numbers = [0, 2, 4, 6] 
let allEven = numbers.allSatisfy { $0 % 2 == 0 }

Toán tử allSatisfy(_:) được giới thiệu ở swift 4.2 và bạn có thể tham khảo thêm tại đây: SE-0207

Bad practice:

let numbers = [0, 2, 4, 6] 
let allEven = numbers.filter { $0 % 2 != 0 }.isEmpty

Kết luận

Chúng ta có nhiều cách để tiếp cận 1 vấn đề, nhưng sẽ có phương án tốt hơn phương án kia. Việc sử dụng hoàn toàn phụ thuộc vào trình độ của bạn. Do vậy đôi khi giả sử tôi kém, tôi không hiểu, thì bạn có thể dùng tool để check: Swiftlint ở đây. Tool này sẽ cảnh báo những cái mà bạn đang dùng hơi ngáo ngổ, và suggest cho bạn phương án tốt hơn. OK, đó là tất cả.

Toàn bộ bài viết được lấy từ nguồn ở đây và có sửa đổi 1 chút:

Performance, functional programming and collections in Swift

Bài viết gốc được đăng tải tại codetoanbug.com
Xem thêm:

Việc làm IT mọi cấp độ được TopDev cập nhật mỗi ngày, tham khảo ngay!