+5

Tìm hiểu về khởi tạo (Initialization) trong Swift (part 2/3)

I. Giới thiệu

Như đã đề cập trong phần 1 của loạt bài viết này, việc khởi tạo các instance là việc gần như lúc nào cũng phải làm trong quá trình lập trình. Swift lại là một ngôn ngữ "an toàn", nó có rất nhiều quy tắc về khởi tạo mà chúng ta cần phải tuân theo. Quá trình khởi tạo các instance trong Swift có thể sẽ gây ra nhiều "đau thương" cho các bạn mới làm quen với Swift, vì thế việc hiểu các quy tắc khởi tạo của Swift khá quan trọng, giúp chúng ta tránh được những đau thương không đáng có. Trong phần 1, tôi đã giới thiệu đến các bạn việc khởi tạo Structure trong Swift. Tiếp theo, chúng ta sẽ tiếp tục tìm hiểu sâu hơn việc khởi tạo Class trong Swift. Việc khởi tạo trong class có phần lằng nhằng và nhiều quy tắc hơn mà chúng ta cần phải đề cập tới, vì thế trong phần 2 này, và phần 3 tiếp theo, chúng ta sẽ tìm hiểu về việc khởi tạo của Class trong Swift. OK, let's start!

II. Tìm hiểu việc khởi tạo trong Class

Cũng giống như trong phần 1 của loạt bài viết này, chúng ta chỉ cần tạo playground để sử dụng trong bài viết này. Các bạn vào Xcode -> File -> New -> Playground để tạo playground mới.

1. Initializer Delegation

Qua tìm hiểu việc khởi tạo Structure trong phần 1, chúng ta đã biết có thể viết hàm khởi tạo cho Structure gán giá trị cho các property, hoặc viết một hàm khởi tạo sử dụng hàm khởi tạo có sẵn khác (Initializer Delegation). Đối với Class, chúng ta cũng có các cách khởi tạo tương tự như đối với Structure, nhưng các cách khởi tạo trong Class có các tên khác nhau.

a. Designated Initializer

Designated Initializer là việc khởi tạo theo cách thông thường nhất, trong hàm khởi tạo chúng ta gán giá trị ban đầu cho toàn bộ các non-optional stored property. Ví dụ:

class Person {
    var name: String
    var age: Int
    
    // Designated Initializer
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

Trong ví dụ bên trên, hàm khởi tạo init(name: , age: ) được gọi là designated Initializer bởi vì hàm này chỉ gán các giá trị cho các property cần thiết mà không gọi một hàm init khác.

b. Convenience Initializer

Giống với initializer delegation của Structure, convenience initializer của class là dạng hàm khởi tạo mà bên trong hàm này, chúng ta gọi đến 1 hàm khởi tạo khác của class. Ví dụ:

class Person {
    var name: String
    var age: Int
    
    // Designated Initializer
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    // Convenience Initializer
    convenience init(name: String, yearOfBirth: Int) {
        let date = Date()
        let calendar = Calendar.current
        let year = calendar.component(.year, from: date)
        self.init(name: name, age: year - yearOfBirth)
    }
}

Như chúng ta thấy bên trên, hàm init(name:, yearOfBirth:) là kiểu convenience initializer. đối với hàm khởi tạo kiểu này, trước hàm cần thêm tiền tố convenience để xác định đây là kiểu convenience initializer.

Cũng giống với hàm khởi tạo của Structure, việc khởi tạo trong Class cũng được chia thành 2 phase, và trong phase 1 chúng ta không thể sử dụng "self". Ví dụ

    // Convenience Initializer
    convenience init(name: String, yearOfBirth: Int) {
        let date = Date()
        let calendar = Calendar.current
        let year = calendar.component(.year, from: date)
        self.name = "hohoho" // lỗi sảy ra
        self.init(name: name, age: year - yearOfBirth)
    }

Ví dụ bên trên, khi chúng ta sử dụng "self" trong phase 1 của khởi tạo, lỗi sẽ sảy ra. Nhìn vào console log, chúng ta có thể thấy lỗi được miêu tả như sau: "error: use of 'self' in property access 'name' before self.init initializes self". Trong phần sau nữa của loạt bài viết này, chúng ta sẽ tìm hiểu sâu hơn về 2 phase của Class.

2. Xử lý khởi tạo lỗi

Tương tự Structure, chúng ta có thể sử dụng optional init hoặc throw exception để xử lý vấn đề khởi tạo lỗi cho Class. Ví dụ:

    // Convenience Initializer with throws exception
    convenience init(name: String, andAge age: Int) throws {
        if name.isEmpty {
            throw InvalidPersonError.EmptyName
        }
        
        if age < 0 {
            throw InvalidPersonError.InvalidAge
        }
        
        self.init(name: name, age: age)
    }
    
    // Convenience Initializer with optional
    convenience init?(name: String, withAge age: Int) {
        guard !name.isEmpty && age >= 0 else {
            return nil
        }
        self.init(name: name, age: age)
    }

Chúng ta khởi tạo các instance cho 2 hàm khởi tạo bên trên như sau:

do {
    let belly = try Person(name: "belly", andAge: -1)
} catch InvalidPersonError.EmptyName {
    print("name is empty")
} catch InvalidPersonError.InvalidAge {
    print("age is invalid")
}

let adam = Person(name: "adam", withAge: -10)

Bên trên, chúng ta xử lý khởi tạo lỗi cho convenience initializer, việc xử lý lỗi cho designated initializer cũng hoàn toàn tương tự.

3. Khởi tạo với subclass

Khác với Structure, Class trong Swift có thể thừa kế từ Class khác. Việc khởi tạo các instance của subclass cũng nhiều quy tắc, lằng nhằng hơn Structure. Phần còn lại của bài viết này (và bài viết sau) chúng ta sẽ đi sâu tìm hiểu việc khởi tạo của Subclass trong Swift.

Chúng ta sẽ đề cập đến việc viết class, viết hàm khởi tạo trong Objective-C một chút. việc khởi tạo class trong Swift khác rất nhiều so với Objective-C. Đối với Objective-C, chúng ta không nhất thiết phải gán giá trị cho tất cả property trong hàm init. Đối với các property không được gán giá trị trong hàm khởi tạo, các property này sẽ được gán giá trị dạng empty-ish (nil, No, 0,..). xét ví dụ sau (chú ý là playground của chúng ta không sử dụng được Objective-C, code ví dụ dưới đây cũng khá là đơn giản, vì vậy các bạn chỉ cần đọc cũng được, hoặc có thể tạo project để nghịch nếu muốn)

@interface Person: NSObject

@property(nonatomic, copy) NSString *name;
@property(nonatomic) int age;

- (instancetype)init;

@end

Như bên trên chúng ta thấy, hàm init trong Objective-C, khi chúng ta không gán giá trị mặc định cho các property (name, age), các property này sẽ nhận giá trị mặc định (nil, 0). Swift không thể dùng hàm khởi tạo như vậy, để sử dụng được hàm khởi tạo chúng ta cần viết một hàm khác như sau:

- (instancetype)initWithName:(NSString *)name age:(int)age;

Lúc này, trong code Swift, chúng ta có thể sử dụng hàm khởi tạo trên như sau:

let tom = Person(name: "Tom", age: 14)

Bây giờ, từ class Person đã có, chúng ta sẽ viết 1 class Employee thừa kế từ class Person. Thêm code sau vào playground:

class Employee: Person {
    var employeeCode: String
}

Run playground, chúng ta thấy xuất hiện lỗi trong class Employee này. Nội dung lỗi như sau: "stored property 'employeeCode' without initial value prevents synthesized initializers".

Lỗi này sảy ra là do trong class Employee có 1 stored-property mới, property này không phải dạng optional và cũng không được gán giá trị mặc định. Để sửa lỗi này thì hoặc chúng ta gán giá trị mặc định cho property này, hoặc chúng ta viết thêm hàm khởi tạo cho subclass này.

a. Xét trường hợp fix lỗi thứ 1, chúng ta gán giá trị mặc định cho property, như code sau:

class Employee: Person {
    var employeeCode: String = "abcde"
}

b. Xét trường hợp fix lỗi thứ 2, chúng ta viết thêm hàm init cho class này. Để viết hàm init dạng designated initializer cho subclass, chúng ta phải viết như sau:

    init(name: String, age: Int, employeeCode: String) {
        self.employeeCode = employeeCode
        super.init(name: name, age: age)
    }

Như chúng ta thấy bên trên, chúng ta cần gán giá trị cho employeeCode trước và sau đó gọi hàm super.init(name:,age:) để hàm khởi tạo của superclass gán giá trị cho các property còn lại. Ở đây, nếu chúng ta đổi chỗ 2 dòng code:

    init(name: String, age: Int, employeeCode: String) {
        super.init(name: name, age: age)
        self.employeeCode = employeeCode
    }

Lỗi sẽ sảy ra ở đây, nội dung lỗi như sau: "error: property 'self.employeeCode' not initialized at super.init call".

Hoặc chúng ta muốn gán giá trị trực tiếp cho các property name, age trong hàm khởi tạo của subclass, như sau:

    init(name: String, age: Int, employeeCode: String) {
        self.employeeCode = employeeCode
        self.name = name
        self.age = age
    }

Lỗi cũng sẽ sảy ra ở đây, nội dung lỗi như sau: "error: use of 'self' in property access 'name' before super.init initializes self".

Tóm lại, khi viết hàm khởi tạo cho subclass, chúng ta bắt buộc phải gán giá trị cho các property của subclass, rồi sau đó gọi hàm khởi tạo của superclass để gán nốt giá trị cho các property thừa kế từ superclass.

III. Tổng kết

Trong phần 2 của loạt bài viết này, chúng ta đã tìm hiểu sâu hơn về việc khởi tạo class và subclass trong Swift. Tôi xin kết thúc phần này của loạt bài viết ở đây, Trong phần 3 tới của loạt bài viết này, chúng ta sẽ tiếp tục tìm hiểu sâu hơn nữa về việc khởi tạo trong class: Tìm hiểu về 2 phase trong quá trình một instance của class được khởi tạo. Hi vọng bài viết này có thể mang lại một chút thông tin bổ ích đến các bạn. Cuối cùng, xin cảm ơn các bạn đã theo dõi bài viết này, have a nice day !!!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí