Author: Quang Huy

  • Swift: weak and unowned

    Swift: weak and unowned

       

    Đầu mục bài viết

       

    1-ARC

    Automatic Reference Counting aka ARC, là 1 tính năng của Swift dùng để đếm số lượng strong reference, và cho phép quản lý việc giải phóng memory khi 1 instance không còn được reference(tham chiếu) đến.

    Theo như doc của Apple:

    Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

    Chúng ta cũng cần nhớ rằng trong swift một reference của được định nghĩa mặc định là kiểu strong

    Ví dụ : 

    class FirstClass: UIViewController {
    
        let secondClass = SecondClass()
    
    }
    
    class SecondClass {
    
    }
    

    Trong ví dụ này, class FirstViewController của mình đang strong reference đến instance secondClass

       

    2-Vấn đề của strong reference cycle

    Như mình đã đề cập bên trên, ARC sẽ giúp chúng ta quản lý, phân bổ memory từ những instance không còn được tham chiếu đến.

    Tuy nhiên, câu hỏi đặt ra là điều gì sẽ xảy ra khi có 2 object/instance strong reference đến nhau?

    Từ strong việt hóa ra là mạnh, bền, … nghe thôi cũng cảm giác khó phá hỏng, phá hủy nó rồi đúng không? :)))

    Thì strong reference cũng thế, strong reference trỏ đến 1 instance/object và sở hữu nó, quyết định đến sự tồn tại của instance/object đó. 

    ARC không thể có cách nào giải phóng được memory từ những kiểu instace/object này, và điều này sẽ dẫn đến memory leak.

    Trong thực tế những project đã làm, thì mình rất hay thường gặp những case strong reference, và mình cũng rất hay vô tình tạo ra strong reference :v 

    Case mình hay gặp nhất là case khi khởi tạo delegate:

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        var delegate: DemoDelegate?
    }
    

    Trông ví dụ này có vẻ quen đúng không? :))) ở đây mình có 1 protocol DemoDelegate, và khởi tạo delegate này trong FirstClass. Và FirstClass và delegate DemoDelegate đang strong reference đến nhau.

    Để giải quyết vấn đề này, chúng ta có weak và unowned.

     

    Weak và Unowned

     

    1. Weak

    Trái ngược với strong pointer, weak pointer trỏ đến một instance/object và không quyết định đến sự tồn tại của instance/object đó.

    Vì thế nên ARC có thể dễ dàng giải phóng bộ nhớ từ instance/object này kể cả chúng ta vẫn còn đang tham chiếu đến nó 

    => Weak reference rất hữu ich, giúp chúng ra tránh được việc vô tình hay cố ý tạo ra 1 strong reference cycle.

    Note: Weak reference không thể sử dụng với việc khởi tạo instance/object là let bởi vì instance/object ở một vài case vẫn phải truyền nil.

    Việc sử dụng weak reference thực sự quan trọng, ví dụ nếu chúng ta để ý hơn và xem source code bên trong definition của UITableView, ta có thể nhận thấy rằng tất cả delegate(bao gồm 2 var quen thuộc là dataSource và delegate) đều được sử dụng với weak khi khởi tạo.

    Và mình nghĩ là đến apple còn để ý và chú trọng đến việc sử dụng weak, tại sao chúng ta lại không làm như thế ? :v

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

    Ok, thêm weak là xong, đơn giản phải không nào.

    Tuy nhiên, khi thêm weak, lại nảy sinh vấn đề không thể compile đoạn code:

    Để giải quyết, theo doc Protocols của apple:

    Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.

    Để fix, chỉ cần thêm keyword class là được

    protocol DemoDelegate: class {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

      2. unowned

    Một variable kiểu unownevề cơ bản giống với variable kiểu weak, nhưng khác nhau ở chỗ: compiler sẽ make sure rằng variable này khi được gọi đến sẽ không có giá trị nil.

    Vậy thực sự trong trường hợp nào nên sử dụng unowned thay vì sử dụng weak ? Vẫn theo doc của Apple: 

    Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

    Để hiểu rõ hơn, mình có 2 ví dụ như này

    Ví dụ của weak reference: mình sở hữu 1 chiếc xe đạp, nhưng 1 hôm đẹp trời nào đó, mình đổi ý không muốn đạp xe nữa, thì ở đây chiếc xe đạp vẫn còn đó, có chăng chỉ là đổi chủ, trong khi mình đang ngồi 1 chiếc 4 bánh nào đó :v 

    Ví dụ của unowned reference: mình chơi 1 yasuo chẳng hạn :))), thằng nhân vật của mình tăng skill theo cấp độ. Tuy nhiên, khi xám màn hình, những skill đó cũng sẽ xám theo. Có nghĩa rằng là những skill này có tuổi thọ = tuổi thọ của nhân vật mình đang chơi.

    Triển khai trong code:

    class Yasuo {
    
        let name: String
        let skill: Skill?
    
        init(name: String) {
            self.name = name
        }
    
    }
    
    class Skill {
    
        let damage: Int
        unowned let champ: Yasuo
    
        init(damage: Int, champ: Yasuo) {
            self.damage= power
            self.champ = champ
        }
    
    }
    

       

    3-Debugging

    Vậy là chúng ta đã hiểu phần nào về 2 keyword weak và unowned. Chúng dùng để tránh tình trạng vô tình tạo ra memory leak. 

    Nhưng sao để biết được có thực sự tránh được hay không?

    Cách đơn giản và xưa như quả đất, là sử dụng print trong 2 method init và deinit

    class FirstClass {
    
        init() {
            print("FirstClass is being initialized")
        }
    
        deinit { 
            print("FirstClass is being deinitialized") 
        }
    
    }
    

    Và để ý memory trong XCode nữa :v

       

    4-Tổng kết

    Strong, weak, unowned là những thuật ngữ cơ bản trong swift, tuy nhiên chúng ta thường – do vô ý hoặc chủ quan – bỏ qua chúng.

    Điều này không thực sự ảnh hưởng quá lớn với những project nhỏ. 

    Tuy nhiên với những app lớn, việc quản lý bộ nhớ ra sao cho hiệu quả là 1 việc quan trọng và đáng lưu ý, bởi vì khi memory leak trở thành 1 vấn đề nghiêm trọng, thì rất khó và tốn effort để fix. 😀

    HAPPY CODING!

    REFERENCE

    https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

    https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

  • TaskGroup Swift Part 2

    TaskGroup Swift Part 2

    Nếu đã đọc bài viết trước của mình về task group, mọi người sẽ hiểu bản chất task group, cách khởi tạo, cách add 1 task con, và cách nhận result của tất cả các task con đó.

    Tuy nhiên, trong bài viết đó mình không đi sâu vào 1 phần quan trọng của task group, đó là việc handle error.

    Nay mình rảnh rảnh nên lên gõ 1 bài về chủ đề này ?

       

    Đầu mục bài viết

       

    1-Tổng quát

    Như chúng ta đã biết, TaskGroup là 1 tập hợp những task con thực thi đồng thời.

    Tuy nhiên, sẽ có vấn đề được đặt ra là khi có bất kỳ 1 trong những task con gặp lỗi, task group handle việc đó như nào? Điều gì sẽ xảy ra cho những task con khác đang thực thi?

    Ở bài viết này, mình sẽ đưa ra 2 cách cơ bản để handle error trong một task group, đó là:

    – Throw error

    – Trả về result của tất cả các task con

    Ta sẽ đi sâu vào từng cách ở những phần bên dưới.

       

    2-Throw error

    Cách đầu tiên là throw error thông qua withThrowingTaskGroup.

    Nhưng trước tiên, chúng ta cần phải hiểu một task group sẽ hoạt động thế nào khi một task con của nó gặp lỗi:

    1. Một task group sẽ chỉ throw ra error đầu tiên mà task con của nó gặp phải, task group sau đó sẽ bỏ qua tất cả những error sau. Tức là khi bất kỳ task con nào trong quá trình thực thi mà gặp phải lỗi, task group sẽ throw lỗi đó, và sẽ không quan tâm đến bất kỳ error nào task con của nó gặp phải sau đó nữa.

    2. Khi một task con throw 1 error, tất cả các task con còn lại (kể cả những task đang thực thi) sẽ được đánh dấu.

    3. Những task con còn lại mà đã được đánh dấu đó sẽ tiếp tục thực thi cho đến khi chúng ta can thiệp và dừng chúng lại.

    4. Khi chúng ta đã can thiệp và dừng việc thực thi của những task con còn lại đó, những task con đó sẽ không trigger việc add vào result tổng của 1 task group nữa (kể cả với những task con đã hoàn thành việc thực thi).

    Để hiểu rõ hơn, mình có tạo ra 1 struct DemoChildTask, mà trong đó xử lý việc chia 2 số cho nhau, và sẽ throw ra error khi có bất kỳ phép chia nào cho 0

    enum ErrorCase: Error {
        case divideToZero
    }
    
    struct DemoChildTask {
        let name: String
        let a: Double
        let b: Double
        let sleepDuration: UInt64
        
        func execute() async throws -> Double {
            // Sleep for x seconds
            await Task.sleep(sleepDuration * 1_000_000_000)
            
            // Throw error when divisor is zero
            guard b != 0 else {
                print("\(name) throw error")
                throw ErrorCase.divideToZero
            }
            
            let result = a/b
            print("\(name) has been completed: \(result)")
            return result
        }
    }
    

    Tiếp theo, mình tạo ra 1 task group và add các task con vào trong đó. Các task con đó sẽ thực thi DivideOperation và trả ra 2 giá trị name và result.

    Khi tất cả các task con hoàn thành, task group sẽ add các result con đó vào một dictionary result tổng mà trong đó chứa tất cả giá trị name và result của task con.

    Bất kể khi nào một trong những task con gặp error, nó sẽ throw và truyền ra cho task group, và task group sẽ throw error đó.

    let demoOperation: [DemoChildTask] = [DemoChildTask(name: "operation-0", a: 4, b: 1, sleepDuration: 3),
                                            DemoChildTask(name: "operation-1", a: 5, b: 2, sleepDuration: 1),
                                            DemoChildTask(name: "operation-2", a: 10, b: 5, sleepDuration: 2),
                                            DemoChildTask(name: "operation-3", a: 5, b: 0, sleepDuration: 2),
                                            DemoChildTask(name: "operation-4", a: 4, b: 2, sleepDuration: 5)]
    
    Task {
        do {
            let allResult = try await withThrowingTaskGroup(of: (String, Double).self,
                                                            returning: [String: Double].self,
                                                            body: { taskGroup in
                // Loop through operations array
                for operation in demoOperation {
                    
                    // Add child task to task group
                    taskGroup.addTask {
                        
                        // Execute slow operation
                        let value = try await operation.execute()
                        
                        // Return child task result
                        return (operation.name, value)
                    }
                }
                
                // Collect results of all child task in a dictionary
                var childTaskResults = [String: Double]()
                
                for try await result in taskGroup {
                    // Set operation name as key and operation result as value
                    childTaskResults[result.0] = result.1
                }
                // All child tasks finish running, thus return task group result
                return childTaskResults
            })
            
            print("Task group completed: \(allResult)")
        } catch {
            print("Task group error: \(error)")
        }
    }
    

    withThrowingTaskGroup về cơ bản giống với withTaskGroup, tuy nhiên chúng ta cần sử dụng keyword try để có thể throw error. Đồng thời cũng cần dùng try khi call func execute() của task con, điều này cho phép error throw từ execute() có thể truyền ra cho task group. Bên cạnh đó, try cũng phải được dùng khi tổng hợp result từ các task con.

    Khi chạy đoạn code trên, ta sẽ nhận được đoạn log như này:

    Đoạn output trên cho chúng ta thấy “operation-3” sẽ throw ra lỗi vì đã chia cho 0.

    Mặc dù đoạn code trên đã khá hoàn chỉnh cho việc handle error với throw, tuy nhiên để tối ưu hơn, ta sẽ không muốn các task con khác thực thi tiếp khi một task con đã throw ra error. Đây là lúc chúng ta cần phải can thiệp vào việc thực thi của chúng như mình đã note bên trên.

    Để làm được việc này, đơn giản có thể sử dụng Task.checkCancellation(). Func này dùng để check những task nào còn đang thực thi.

    func execute() async throws -> Double {
            // Sleep for x seconds
            await Task.sleep(sleepDuration * 1_000_000_000)
            
            // Check for cancellation.
            try Task.checkCancellation()
            
            // Throw error when divisor is zero
    //        guard b != 0 else {
    //            print("\(name) throw error")
    //            throw ErrorCase.divideToZero
    //        }
    //
    //        let result = a/b
    //        print("\(name) has been completed: \(result )")
    //        return result
        }
    

    Về cơ bản, khi func checkCancellation() gặp bất kỳ task con nào đã được đánh dấu, nó sẽ dừng task con đó và throw ra CancellationError . Tuy nhiên, như mình đã đề cập, việc throw này không mang quá nhiều ý nghĩa vì task group sẽ reject toàn bộ những error này.

    Khi chạy lại đoạn code trên với checkCancellation(), ta sẽ nhận được log như này:

       

    3-Trả về result

    Cách thứ 2 này thực ra hoàn toàn ngược lại với cách đầu tiên. Cách này cơ bản task group sẽ ignore toàn bộ task con mà throw error, mà sẽ chỉ trả về tất cả result của task con thực thi thành công.

    Cách 2 này code khá giống với cách 1, chỉ khác là bây giờ sẽ sử dụng non-throwing task group thay vì throw task group, và sẽ ignore toàn bộ error với try?:

    Task {
        let allResult = await withTaskGroup(of: (String, Double)?.self,
                                            returning: [String: Double].self,
                                            body: { taskGroup in
            // Loop through operations array
            for operation in demoOperation {
                
                // Add child task to task group
                taskGroup.addTask {
                    
                    // Execute slow operation
                    guard let value = try? await operation.execute() else {
                        return nil
                    }
                    
                    // Return child task result
                    return (operation.name, value)
                }
            }
            
            // Collect results of all child task in a dictionary
            var childTaskResults = [String: Double]()
            
            for await result in taskGroup.compactMap({ $0 }) {
                // Set operation name as key and operation result as value
                childTaskResults[result.0] = result.1
            }
            // All child tasks finish running, thus return task group result
            return childTaskResults
        })
        
        print("Task group completed: \(allResult)")
    }
    

    Để trả về result tổng của tất cả task con, mình sử dụng compactMap để loại bỏ toàn bộ giá trị nil được trả về từ task con.

    Khi chạy đoạn code trên, đây sẽ là output:

       

    4-Tổng kết

    Như vậy, với 2 bài viết của mình, mọi người đã có những cái nhìn và hiểu biết rõ ràng hơn về task group, một trong những update mới và quan trọng của swift 5.5 ?

    Happy coding!!

  • TaskGroup Swift

    TaskGroup Swift

    Trong sự kiện WWDC21, apple đã giới thiệu với công chúng swift 5.5 với nhiều cải tiến, trong đó có async/await. Đây là một cập nhật lớn về cách chúng ta làm việc với bất đồng bộ, chúng dùng để viết những đoạn code async dễ hiểu và dễ đọc.
    Tuy nhiên, async/await bản thân nó không cho phép chúng ta chạy tất cả mọi thứ một cách đồng thời, ngay cả khi với nhiều CPU cores cùng hoạt động, async/ await code vẫn sẽ thực thi tuần tự.
    Để giải quyết vấn đề này, vẫn ở swift 5.5, apple giới thiệu với chúng ta Task và TaskGroups. Chúng là 1 trong những phần quan trọng trong concurrency framework của Swift.

       

    1-Bản chất taskgroup

    Đúng như tên gọi, TaskGroup là 1 tập hợp những task con thực thi đồng thời, nói cách khác, TaskGroup sẽ giúp chúng ta chia 1 công việc thành nhiều concurrent operations.
    Taskgroup hoạt động tốt nhất khi các task con của nó trả về cùng 1 kiểu data, tuy nhiên, ta cũng có thể ép chúng hỗ trợ kiểu data khác nhau.
    Để dể hiểu hơn, mình có 5 gạch đầu dòng về Taskgroup:

    – Một taskgroup là 1 tập hợp các task async mà trong tập hợp đó, các task này hoạt động độc lập với nhau.

    – Tất cả các task con sẽ thực thi 1 cách đồng thời và gần như ngay lập tức sau khi được add vào Taskgroup.

    – Chúng ta không thể kiểm soát khi nào các task con hoàn tất việc thực thi của chúng. Vì thế, chúng ta không nên sử dụng taskgroup nếu muốn các task con hoàn thành theo một thứ tự nào đó.

    – Một taskgroup chỉ return khi và chỉ khi tất các task con của nó hoàn tất. Nói cách khác, tất cả các task của Taskgroup chỉ tồn tại trong chính TaskGroup đó còn đang hoạt động.

    – Có thể dừng một taskgroup bằng cách return 1 giá trị, hoặc return void, hoặc throw error.

       

    2-Mức độ ưu tiên

    TaskGroup có thể được tạo với 1 trong 4 độ ưu tiên: High là độ ưu tiên cao nhất, tiếp đó là Medium, Low, và Background.
    Mức độ ưu tiên task cho phép hệ thống sắp xếp task nào thực thi trước.
    Nếu so sánh với các mức độ của DispatchQueue, userInitiated và utility sẽ là High và low. Taskgroup không có mức độ tương đương với userInteractive, vì với mức độ đó, nó dành riêng cho user interface.

       

    3-Khởi tạo taskGroup

    Để tạo một taskgroup, ta có thể sử dụng withTaskGroup(of:returning:body:) hoặc withThrowingTaskGroup(of:returning:body:). Ở bài viết này, mình không sử dụng taskgroup có throw error, nên nếu muốn tìm hiểu thêm về withThrowingTaskGroup(of:returning:body:), mọi người có thể xem thêm ở document của apple.

       

    4-Làm việc với taskgroup

    Mình có tạo ra 1 struct demoChildTask việc nhân 2 số với nhau, và trong đó có một khoảng nghỉ để tiện cho việc control taskgroup. Ở đây mình có tạo ra một mảng demoChildTask:

    let demoOperations = [
        demoChildTask(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
        demoChildTask(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
        demoChildTask(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
    ]
    

    Sau đó add các task con vào taskgroup bằng cách chạy vòng lặp array demoChildTask

    let demoResult = await withTaskGroup(of: (String, Double).self,
                                             returning: [String: Double].self,
                                             body: { taskGroup in
            
            // Loop through demoOperations array
            for operation in demoOperations {
                
                // Add child task to task group
                taskGroup.addTask {
                    
                    // Execute slow operation
                    let value = await operation.slowMulti()
                    
                    // Return child task result
                    print("Quang Huy -: \(operation.name)")
                    return (operation.name, value)
                }
                
            }
            
            // Collect child task result...
        })
    

    Lưu ý, kiểu dữ liệu task con trả về phải đúng là kiểu dữ liệu của task con mà ta đã khai báo khi khởi tạo TaskGroup

    Như đã đề cập, tất cả các task con đều thực thi đồng thời với nhau, nên ta không thể control việc khi nào chúng hoàn tất. Vì thế để nhận result của từng task con, ta phải loop qua taskgroup:

    // Collect results of all child task in a dictionary
    var demoChildTaskResults = [String: Double]()
    for await result in taskGroup {
        print("Quang Huy 1 - \(result.0)")
        // Set operation name as key and operation result as value
        demoChildTaskResults[result.0] = result.1
    }
            
    // All child tasks finish running, return task group result
    return demoChildTaskResults
    

    Ở đoạn code trên, mình sử dụng await keyword, keyword này có ý nghĩa là vòng lặp có thể dừng lại để đợi task con thực thi xong. Mỗi khi task con xong, vòng lặp lại tiếp tục và update giá trị cho childTaskResults.
    Sau khi xử lý result của tất cả các task con hoàn tất, ta return về result của taskgroup. Giống như task con, return type của taskgroup cũng cần phải trùng với kiểu return khi khởi tạo nó:

    Khi chạy đoạn code trên, ta nhận được đoạn log như này:

    Như có thể thấy, task group cần ~ 5s để hoàn thành, 5s cũng là khoảng nghỉ dài nhất mà mình khởi tạo.

       

    5-Tổng kết

    Với sự ra mắt của TaskGroup, việc quản lý và sử dụng các concurrent task chưa bao giờ đơn giản đến thế. Đồng thời cũng là sự kết hợp hoàn hảo với async/await, thứ cũng là 1 cập nhật lớn ở WWDC21.


    REFERENCE

    https://www.hackingwithswift.com/quick-start/concurrency/what-are-tasks-and-task-groups

    https://developer.apple.com/documentation/swift/taskgroup

  • Đừng lạm dụng Enum

    Đừng lạm dụng Enum

    Nhà tâm lý học người Mỹ Abraham Maslow có một câu nói rất nổi tiếng

    If you only have a hammer, you tend to see every problem as a nail. (Nếu dụng cụ duy nhất bạn có chỉ là một chiếc búa, thì mọi vấn đề đều trông giống cái đinh)

    Câu nói này rất phù hợp với lập trình. Mỗi vấn đề đều có nhiều cách tiếp cận với ưu nhược điểm riêng tuỳ theo ngữ cảnh và ràng buộc. Không có giải pháp nào luôn hợp lý hoặc luôn tệ trong tất cả các trường hợp, kể cả Singleton ?. Enum cũng vậy. Nó là một tính năng ngôn ngữ linh hoạt và mạnh mẽ, tuy nhiên việc lạm dụng enum không chỉ làm giảm chất lượng code mà còn khiến codebase khó mở rộng hơn.


    Đầu mục bài viết


    1-Bản chất enum

    Trong tài liệu của mình, Apple chỉ ra rằng enum được tạo ra để định nghĩa một type với mục đích chứa các giá trị liên quan tới nhau. Nói cách khác, hãy dùng enum để nhóm một tập giá trị hữu hạn, cố định, và có quan hệ với nhau. Ví dụ như enum định nghĩa phương hướng

    //Tạo enum Direction ở đây là hợp lý bởi
    //các case liên quan tới nhau và số lượng case là hữu hạn
    enum Direction {
        case north
        case south
        case east
        case west
    }
    

    2-Vấn đề của enum

    Một khi được định nghĩa, chúng ta sẽ không thể thêm case để mở rộng enum mà không làm ảnh hướng tới những chỗ nó được sử dụng. Điều này có thể mang lại lợi ích nếu ta không dùng default case bởi Xcode sẽ giúp tránh việc bỏ lọt code. Tuy nhiên, đây cũng là một nhược điểm lớn trong trường hợp code hiện giờ không quan tâm tới case mới đó.

    Dùng enum để model các Error phức tạp

    Chắc hẳn chúng ta đã từng dùng enum để nhóm các loại Error cho response API như dưới đây

    enum APIError: Error {
        case invalidData(Data)
        case parsingError(Error)
        case noConnection
        case unknown(code: Int, description: String)
        //... các case khác ...
    }
    

    Thoạt nhìn, việc tạo APIError là hợp lý bởi chúng đều là lỗi liên quan đến response API và giờ đây ta có thể gõ .parsingError hay .invalidData cực kì tiện lợi. Mặc dù vậy, hướng tiếp cận này có 2 nhược điểm lớn:

    • Ta không bao giờ muốn switch toàn bộ case của nó
    • Nó không phải là cách tối ưu bởi struct là công cụ tốt hơn để giải quyết bài toán này

    Trong quá trình sử dụng, ngoại trừ các case cần thiết của APIError, việc switch toàn bộ case là hơi thừa thãi. Có thể hiện tại ta chỉ quan tâm đến lỗi .noConnection để hiện alert riêng và các lỗi khác sẽ dùng chung một kiểu alert. Cũng có thể ta chỉ quan tâm đến một vài lỗi nhất định để xử lý logic code nhưng chắc chắn không bao giờ là tất cả case cùng lúc. Lý do là bởi ngoài việc cùng miêu tả các error response, các error trên không có quan hệ gì với nhau.


    Hơn nữa, về mặt logic, việc dùng enum ở đây là sai bởi số lỗi có thể xảy ra khi xử lý API là vô hạn. Điều này trái ngược trực tiếp với bản chất của enum được nhắc đến ở trên. Trong trường hợp này, model APIError bằng struct phù hợp hơn rất nhiều

    struct InvalidData: Error {
        let data: Data
    }
    
    struct ParsingError: Error {
        let underlyingError: Error
    }
    
    struct NoConnection: Error { }
    
    struct Unknown: Error {
        let code: Int
        let description: String
    }
    

    Nếu thực sự muốn nhóm các lỗi vào một kiểu Error, chỉ cần tạo riêng một protocol và conform chúng với protocol đó

    protocol APIError: Error { }
    
    extension InvalidData: APIError { }
    extension ParsingError: APIError { }
    extension NoConnection: APIError { }
    extension Unknown: APIError { }
    

    Việc model APIError bằng structprotocol giúp code linh hoạt hơn khi giờ đây việc tạo ra các Error mới không làm ảnh hưởng đến codebase. Ta cũng có thể cung cấp hàm khởi tạo custom cho chúng, hay conform từng lỗi với các protocol khác nhau một cách dễ dàng thay vì những switch statement cồng kềnh trong enum. Cuối cùng, việc thêm và truy cập biến trong struct đơn giản hơn so với associated value trong enum rất nhiều.


    Sử dụng enum để model các Error đơn giản và hữu hạn là điều nên làm. Tuy nhiên, nếu tập Error đó lớn, hoặc chứa nhiều data đính kèm như các lỗi liên quan đến API thì struct là một lựa chọn tốt hơn hẳn. Trong thực tế, Apple cũng chọn cách này khi tạo URLError xử lý cho Networking của Foundation.

    Dùng enum để config code

    Một sai lầm phổ biến nữa là dùng enum để config UIView, UIViewController, hoặc các object nói chung

    enum MessageCellType {
        case text
        case image
        case join
        case leave
    }
    
    extension MessageCellType {
        var backgroundColor: UIColor {
            switch self {
            case .text: return .blue
            case .image: return .red
            case .join: return .yellow
            case .leave: return .green
            }
        }
        
        var font: UIFont {
            switch self {
            case .text: return .systemFont(ofSize: 16)
            case .image: return .systemFont(ofSize: 14)
            case .join: return .systemFont(ofSize: 12, weight: .bold)
            case .leave: return .systemFont(ofSize: 12, weight: .light)
            }
        }
        
        //...
    }
    
    class TextCell: UITableViewCell {
        func style(with type: MessageCellType) {
            contentView.backgroundColor = type.backgroundColor
            textLabel?.font = type.font
        }
    }
    

    MessageCellType định nghĩa các style cho giao diện của cell ứng với từng loại message để tái sử dụng ở nhiều màn khác nhau. Các thuộc tính chung có thể kể đến như backgroundColor hay UIFont.


    Giống với APIError, vấn đề đầu tiên của MessageCellType là ta không muốn switch toàn bộ case của nó. Với mỗi loại cell, ta chỉ muốn dùng một type nhất định để config cell đó. Việc switch tất cả các case ở hàm cellForRow(at:) là không hợp lý bởi luôn phải trả ra fatalError hoặc một UITableViewCell bù nhìn để thoả mãn Xcode vì số lượng subclass của UITableViewCell là vô hạn ?‍♂️.


    Một vấn đề khác với MessageCellType là việc khó mở rộng. Bản chất của enum là tính hoàn thiện và hữu hạn. Khi thêm bất kì case mới nào, ta đều phải update tất cả các switch statement sử dụng nó. Điều này đặc biệt tệ trong trường hợp đang viết framework vì giờ đây thay đổi sẽ phá hỏng code từ phía client.
    Giải pháp cho MessageCellType là biến nó thành struct và tạo ra các biến static thuộc type này

    struct MessageCellType {
        let backgroundColor: UIColor
        let font: UIFont
    }
    
    extension MessageCellType {
        static let text = MessageCellType(backgroundColor: .blue, font: .systemFont(ofSize: 16))
        static let image = MessageCellType(backgroundColor: .red, font: .systemFont(ofSize: 14))
        static let join = MessageCellType(backgroundColor: .yellow, font: .systemFont(ofSize: 12, weight: .bold))
        static let leave = MessageCellType(backgroundColor: .green, font: .systemFont(ofSize: 12, weight: .light))
    }
    

    Refactor từ enum thành struct giúp việc thêm config mới không còn là vấn đề bởi nó không hề ảnh hưởng tới codebase. Một lợi ích nhỏ nữa là ta vẫn được gõ .join hoặc .leave khi truyền chúng vào trong function

    let cell: TextCell = TextCell()
    cell.style(with: .join)
    

    3-Tổng kết

    Trước khi tạo enum, hãy luôn nhớ rằng

    Enum dùng để switch. Nếu không chắc rằng mình muốn switch nó thì hãy sử dụng struct và protocol


  • Swift – Basic to advanced Closure

    Swift – Basic to advanced Closure

    Với nhiều bài toán, không phải lúc nào cũng đơn giản. Ví dụ chỉ với 2 số nguyên, thực tế có rất nhiều công thức áp dụng được với 2 số này, từ đơn giản như cộng, trừ, nhân, chia … đến phức tạp như hàm mũ, khai căn,… Nếu chỉ sử dụng cách định nghĩa sẵn các function ta không giải quyết tất các các case của bài toán. Giải pháp ở đây là chúng ta sử dụng closure.


    Đầu mục bài viết

    Closure cơ bản

    1. Tạo closure cơ bản
    2. Tạo tham số cho closure
    3. Trả về giá trị từ closure
    4. Truyền closure như 1 tham số vào Function

    Closure nâng cao

    1. Truyền closure với nhiều tham số vào Function
    2. Trả về closure từ functiion
    3. Lưu trữ dữ liệu với closure

    Các kiểu gọi hàm với closure

    Tổng kết


    Basic closure

    1. Tạo closure cơ bản

    Swift cho phép chúng ta sử dụng function như bất kỳ kiểu dữ liệu nào,ví dụ như string, integers,… Điều này có nghĩa rằng chúng ta có thể tạo ra 1 function và gán nó cho một biến, gọi function đó bằng cách sử dụng biến đó và thậm chí có thể gán function đó vào các function khác dưới dạng tham số.

    Function mà ta sử dụng theo cách này được gọi là closure, mặc dù cách hoạt động của nó giống function tuy nhiên cách viết khác nhau 1 chút.

    Ví dụ đơn giản để print 1 đoạn text :

    Ở đây mình tạo ra một function tuy nhiên không có tên, và gán nó cho biến driving. Ta có thể gọi driving() như thể nó là 1 hàm thông thường :

    2. Tạo tham số cho closure

    Khi khởi tạo closure, ta sẽ nhận ra nó không có tên nên sẽ không có bất kỳ vị trí nào để thêm tham số như function bình thường. Tuy nhiên không có nghĩa là closure không nhận tham số input, chỉ là nó có cách làm khác so với function: các tham số được liệt kê bên trong dấu {}.

    Cách tạo ra các tham số để closure có thể “chứa chấp” rất đơn giản, chỉ cần liệt kê chúng bên trong dấu ngoặc đơn ngay sau dấu ngoặc nhọn mở, sau đó thêm keyword in để Swift biết phần nào là phần bắt đầu closure.

    Ví dụ mình sửa driving() bên trên thành closure có chứa tham số

    Và một trong những điểm khác biệt nữa giữa closure và function là bạn không cần sử dụng tên tham số khi gọi closure.

    3. Trả về giá trị từ closure

    Closure có thể trả về giá trị, và cách viết tương tự như khai báo tham số: viết nó trong closure, ngay trước keyword in.

    Với closure driving() bên trên, mình sẽ trả về đoạn string kia thay vì print thẳng ra, để làm thế ta sử dụng → String trước keyword in, sau đó sử dụng return như function bình thường

    Bây giờ có thể gọi closure này và in ra giá trị String nó trả về

    4. Truyền closure như một tham số vào function

    Vì closure có thể được sử dụng như string, integer,…, bạn có thể truyền nó vào 1 function. Syntax của nó khá là rắc rối với newbie tuy nhiên nếu bạn đã hiểu về nó thì sẽ thấy không rắc rối lắm :))

    Đây là closure driving() gốc của chúng ta

    Nếu bạn muốn truyền closure này vào trong 1 function để nó có thể thực thi bên trong function đó, bạn phải chỉ định kiểu tham số là () -> Void.

    Ví dụ mình viết 1 function travel() mà nó nhận tham số là các kiểu action khác nhau

    Ta có thể gọi hàm travel() mà sử dụng closure driving

    Bên trên là 1 ví dụ về truyền closure như 1 tham số, tuy nhiên ta đang sử dụng () → Void, nói cách khác là không truyền vào tham số và cũng không nhận giá trị trả về.

    Tuy nhiên,1 closure vẫn có thể nhận tham số của chính nó khi chính closure đó đang là 1 tham số của function khác.

    Ví dụ, mình viết lại function travel() mà nó chỉ có 1 closure là tham số duy nhất, và closure đó nhận tham số là 1 String

    Và để thực thi function travel(), ta gọi nó với 1 tham số closure


    Advanced closure

    1. Truyền closure với nhiều tham số vào function

    Mình lại xin phép viết lại hàm travel() bên trên, tuy nhiên lần này hàm travel() của chúng ta sẽ cần 1 closure mà nó chứa một vài thông tin khác thay vì 1 string như bên trên, cụ thể nó sẽ chứa thông tin về địa điểm mà một người sẽ đến và tốc độ họ đi. Lúc này chúng ta cần sử dụng (String, Int) → String cho kiểu tham số của closure

    Để thực thi function travel(), ta gọi function này với tham số closure được truyền vào

    ( Bạn có thể thấy lạ với các keyword $0 và $1, hiện tại bạn không hiểu cũng không sao cả vì mình sẽ giải thích rõ hơn ở dưới =]] )

    Closure giống với function là nó có thể nhận bao nhiêu tham số cũng được, tuy nhiên bạn có thể thấy rằng một function mà nhận quá nhiều tham số thì sẽ rất khó hiểu dẫn đến confuse, điều này còn kinh khủng hơn với closure khi bản chất closure cũng đã rất phức tạp rồi. Vì thế để mọi thứ clear bạn nên chỉ sử dụng từ 1 đến 3 tham số  mà thôi.

    2. Trả về closure từ function

    Tương tự như việc bạn truyền tham số closure vào function, bạn cũng có thể nhận về 1 closure mà được trả về từ function.

    Syntax để return closure từ function có hơi rắc rối 1 chút, bởi vì nó dùng → 2 lần: một để chỉ định giá trị trả về của function và một để chỉ định giá trị trả về từ closure.

    Mình lại viết lại hàm travel() mà không nhận tham số, tuy nhiên lại có trả về 1 closure mà closure nhận tham số là String và trả về Void

    Chúng ta gọi travel() để nhận về closure đó, sau đó gọi nó như 1 function

    Còn 1 cách gọi nữa để gọi trực tiếp giá trị trả về từ travel() – cách này không được khuyến khích sử dụng:

    3. Lưu trữ dữ liệu với closure

    Nếu bạn sử dụng bất kỳ giá trị bên ngoài nào trong closure, Swift sẽ lưu và giữ chúng cùng closure, vì thế giá trị này có thể bị thay đổi kể cả nó không còn tồn tại.

    Ví dụ, việc lưu trữ giá trị trong closure xảy ra khi ta tạo 1 giá trị trong hàm travel() mà giá trị đó được sử dụng trong closure, ở đây ví dụ như ta muốn kiểm tra xem closure được gọi trả về bao nhiêu lần

    Mặc dù biến counter được tạo bên trong travel(), nó sẽ được lưu trữ bởi closure vì thế nến nó sẽ vẫn luôn tồn tại cho closure đó.

    Vì thế nếu ta gọi result(“London”) nhiều lần, biến đếm sẽ luôn tăng lên :


    Các kiểu gọi hàm với closure

    Ví dụ, ở đây định nghĩa 1 functiontypecalculationresultcallback với tham số đầu vào là 1 kiểu int và không có giá trị trả về, chúng ta cũng có thêm 1 hàm là multiplyNumber với 2 tham số nhận vào là kiểu int, không có giá trị trả về và kèm theo 1 tham số có tên là callback có kiểu là CalculationResultCallback. Trong ngôn ngữ lập trình nói chung, callback có nghĩa là lời gọi hàm sau, tức là sau quá trình sử lý logic và ra được 1 kết quả nào đó mà chúng ta cần xử lý thêm với kết quả đó.

    Hàm multiplyNumbers có ý nghĩa là, với 2 giá trị nguyên bất kỳ, nó sẽ tính phép nhân 2 số, sau đó sẽ trả về kết quả bằng 1 hàm mà ở đó, ta có thể tùy ý sử dụng kết quả theo ý ta muốn.

    Ta có các cách gọi hàm như sau:

    • Ta có thể thấy, 2 tham số đầu tiên có kiểu int được gọi hoàn toàn bình thường, nhưng với tham số thứ 3 với tên là callback, thay vì truyền vào 1 số, hoặc 1 tên hàm như trước, thì ta truyền vào hẳn 1 hàm,mà ở đó ta in ra dòng “Tích của 2 số là …”
    • Trong closure này,chúng ta có từ khóa in phân cách 2 nửa,bên trái là khai báo tham số nằm trong () và khai báo kiểu trả về là void,bên phải là lệnh được thực thi khi gọi đến closure này,toàn bộ closure được bao trong cặp dấu {}.
    • Giống như function bình thường,ta có thể bỏ void nếu hàm không trả về kết quả,và có thể bỏ đi luôn () bao tham số.
    • Đây chính là dạng gọi cơ bản nhất của closure
    • Swift quy định,với những hàm khai báo closure là tham số nhưng ở vị trí cuối cùng trong danh sách tham số: ví dụ như trên,ta có thể gọi hàm bằng cách gọi hàm với 2 tham số int bình thường,kéo theo 1 tham số closure được bao bằng dấu {},trường hợp này được gọi là Trailing closure
    • Để closure gọn hơn nữa, có 1 tính năng gọi là Shorthands, bản chất là thay vì khai báo tham số với tên biến cụ thể thì chúng ta sử dụng cú pháp $0,$1,$2,… để thay thế cho các tham số ở vị trí 0,1,2,.. Tất nhiên,nếu hàm chỉ có 1 tham số mà ta gọi đến $1 thì hàm sẽ báo lỗi.
    • Đối với shorthands, ta không thể khai báo tham số như những trường hợp trên nữa,đổi lại sẽ ngắn gọn hết sức có thể..
    • Tuy nhiên,với hàm có nhiều tham số,ta khó xác định $0,$1,$2,… là gì.Vì thế nên chỉ sử dụng shorthands với hàm đơn giản chỉ có từ 1-3 tham số mà thôi.

    Tổng kết

    Về bản chất

    • Closure là 1 function.
    • Nhưng là function không đầy đủ tên function và thân function , mà chỉ có mỗi thân function .
    • Mục đích của nó không phải gọi function bằng tên, mà là được chèn vào tham số của 1 function khác.