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

I. Giới thiệu

Thông thường khi code, chúng ta sẽ phải liên tục khởi tạo các instance của class, structure hoặc enum. Khởi tạo là thời điểm chúng ta quản lý giá trị của các property, gán các giá trị mặc định cho các property này. Tuy nhiên, khác với Objective-C trước đây, Swift là ngôn ngữ “an toàn”, vì vậy ngay từ bước khởi tạo, Swift đã có rất nhiều qui định phải tuân theo. Một vài quy định trong số đó không được rõ ràng cho lắm. Trong loạt bài viết này, chúng †a sẽ †ìm hiểu sâu hơn về quá †rình khởi tạo trong Swift, cụ thể trong part 1 này, tôi xin giới thiệu về quá trình khởi tạo của Structure, chúng ta sẽ tìm hiểu thông qua những đoạn demo code.

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

Đầu tiên, chúng ta mở Xcode tạo 1 playground mới (File -> New -> Playground). Trong bài viết này, chúng ta chỉ cần code trên playground là đủ. OK, mọi thứ đã sẵn sàng, chúng ta bắt đầu vào tìm hiểu thôi.

1. Khởi tạo mặc định

Khởi tạo mặc định là kiểu khởi tạo mà chúng ta không cần thêm các tham số vào trong quá trình khởi tạo. Đại loại như kiểu

let A = B()

Đầu tiên, chúng ta thêm code sau:

struct Person {
    
}

let tom = Person()

Bên trên, instance tom được khởi tạo mặc định từ structure Person. Trong Structure, chúng ta có thể khởi tạo mặc định khi struct không có bất kỳ stored properties nào hoặc tất cả stored properties đều có giá trị mặc định. Ví dụ, thêm code sau vào struct Person:

struct Person {
    let name: String = "Tom"
    let age: Int = 27
}

Chúng ta thấy, instance tom vẫn được khởi tạo bình thường mà không có lỗi. Bởi vì các property trong structure Person đều được gán giá trị mặc định.

OK, vậy thì trường hợp các property là dạng optional thì sao? trong trường hợp property dạng optional, chúng ta cần để ý khai báo của property đó là variable hay constant. Thử thêm code sau vào structure Person:

struct Person {
    let name: String = "Tom"
    let age: Int = 27
    var numberOfChild: Int?
}

Chúng ta thấy, instance tom vẫn được khởi tạo mặc định dù property không được gán giá trị mặc định. Trong trường hợp optional property là dạng variable thì chúng ta không cần gán giá trị mặc định cho nó. Tuy nhiên, nếu chúng ta đổi thành:

let numberOfChild: Int?

Lúc này, instance tom của chúng ta sẽ không thể khởi tạo mặc định được nữa, để khởi tạo được trong trường hợp này, chúng ta phải gán giá trị mặc định cho numberOfChild

struct Person {
    let name: String = "Tom"
    let age: Int = 27
    let numberOfChild: Int? = 2
}

2. Khởi tạo với các parameter

Đây là trường hợp khởi tạo mà chúng ta thường xuyên sử dụng để truyền các giá trị cho instance mà chúng ta muốn khởi tạo. Ví dụ:

let rect = CGRect(origin: CGPoint.zero, size: CGSize.zero)

bên trên chúng ta khởi tạo 1 instance với các tham số origin và size.

Quay lại playground, chúng ta sửa lại struct Person một chút như sau:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int?
}

Tiếp đó, chúng ta sửa lại cách khởi tạo instance tom như sau:

let tom = Person(name: "Tom", age: 27, numberOfChild: nil)

Như chúng ta thấy, tất cả các property đều không được gán giá trị mặc định, vì vậy khi khởi tạo instance tom, chúng ta phải khai báo giá trị cho tất cả property này.

Các bạn có thể thắc mắc: “tại sao mình chưa tạo hàm khởi tạo với các param như trên cho struct này mà mình vẫn viết được code khai báo không hề lỗi”. Đó là do Swift đã tự động tạo cho chúng ta hàm khai báo này. Các bạn lưu ý chỉ Structure trong Swift có thể làm được điều này, nếu chúng ta đổi thành class Person, chúng ta sẽ không có hàm khởi tạo như bên trên nữa.

Việc tự động tạo hàm khởi tạo, có điểm thuận lợi là nó rất tiện lợi, giúp chúng ta không phải viết 1 hàm khởi tạo cho struct nữa. Tuy nhiên, nó lại gây ra cho chúng ta khá nhiều khó khăn phiền toái khác:

Đầu tiên, xét trường hợp chúng ta muốn đổi thứ tự các property của struct Person, giả sử đổi chỗ của name và age như sau:

struct Person {
	let age: Int
    let name: String    
    var numberOfChild: Int?
}

lúc này, việc khởi tạo instance tom sẽ gây ra lỗi, chúng ta phải đảo lại các param trong hàm khởi tạo như sau:

let tom = Person(age: 27, name: "Tom", numberOfChild: nil)

Việc này sẽ rất phiền toái nếu chúng ta khởi tạo các instance của struct Person ở nhiều nơi. Chúng ta sẽ phải viết lại tất cả khai báo ở tất cả các file.

Tiếp theo, xét trường hợp chúng ta thêm property vào struct Person và trường hợp chúng ta muốn gán giá trị mặc định cho 1 property, ví dụ:

struct Person {
    let name: String    
	let age: Int = 27
    var numberOfChild: Int?
}

Lúc này, hàm khởi tạo do Swift tự sinh lại tiếp tục bị thay đổi, chúng ta cần sửa lại đoạn khởi tạo instance tom như sau:

let tom = Person(name: "Tom", numberOfChild: nil)

Tiếp nữa, chúng ta xét trường hợp viết thêm hàm khởi tạo cho struct Person. Ví dụ:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int?
    
    init(name: String) {
        self.name = name
        self.age = 27
        self.numberOfChild = 2
    }
}

lúc này hàm khởi tạo do swift tự động tạo ra cho structure của chúng ta:

let tom = Person(name: "Tom", age: 27, numberOfChild: nil)

Khởi tạo bên trên lại tiếp tục lỗi. WTF? chúng ta đã viết đúng hàm khởi tạo của struct Person rồi cơ mà? tại sao lại sảy ra lỗi ở đây? Câu trả lời là do cơ chế tạo hàm khởi tạo tự động của Structure. Khi chúng ta viết thêm hàm khởi tạo thì Swift sẽ không tạo hàm khởi tạo cho chúng ta nữa. Vậy làm sao để Swift lại tự động tạo hàm khởi tạo cho chúng ta? Câu trả lời là hoặc chúng ta không viết thêm hàm khởi tạo, hoặc chúng ta viết thêm hàm khởi tạo trong Extension. Ví dụ:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int?
}

extension Person {
	init(name: String) {
        self.name = name
        self.age = 27
        self.numberOfChild = 2
    }
}

lúc này, chúng ta có thể khởi tạo instance tom của Person theo cả 2 cách:

let tom = Person(name: "Tom", age: 27, numberOfChild: nil)
let tom = Person(name: "Tom")

3. Viết thêm hàm khởi tạo

Khi viết thêm các hàm khởi tạo (như bên trên chẳng hạn) chúng ta phải đảm bảo tất cả các stored property mà chưa có giá trị mặc định phải được gán giá trị. Bất kỳ property nào chưa được gán giá trị sẽ sinh ra lỗi compiler error. Ví dụ:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int
}

extension Person {
	init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.numberOfChild = 2
    }
}

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

Trong hàm khởi tạo bên trên, cả 3 property đều là stored property, không có giá trị mặc định và chúng đều được gán giá trị trong hàm khởi tạo, nên không có lỗi sảy ra ở đây.

Trong hàm khởi tạo, chúng ta còn có thể gán giá trị mặc định cho các tham số. Chúng ta thử viết lại extension Person như sau:

extension Person {
	init(name: String =Tom, age: Int = 27) {
        self.name = name
        self.age = age
        self.numberOfChild = 2
    }
}

let tom1 = Person()    // không có lỗi
let tom2 = Person(name: "Tom", age: 27)  // không có lỗi

Bên trên, các param trong hàm init đều có để nhận giá trị mặc định, khi chúng ta khởi tạo mà không truyền tham số vào thì các param sẽ nhận giá trị mặc định, nên cả 2 cách khởi tạo bên trên đều được.

4. Sử dụng Initializer Delegation

Initializer Delegation là việc chúng ta gọi 1 hàm init trong 1 hàm init. Nghe thì có vẻ không quen, code cái thấy quen ngay:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int
}

extension Person {
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.numberOfChild = 2
    }
    
    init(name: String, yearOfBirth: Int) {
        let age = 2017 - yearOfBirth
        self.init(name: name, age: age)
    }
}

let tom = Person(name: "Tom", yearOfBirth: 1990)

Như chúng ta thấy bên trên, hàm khởi tạo init(name:, yearOfBirth:) gọi đến hàm init(name:, age:). Việc này được gọi là initializer delegation. Sử dụng initializer delegation giúp chúng ta sử dụng code trong hàm khởi tạo có sẵn mà không cần phải viết lại toàn bộ code.

Bây giờ thử viết lại 1 tí hàm init(name:, yearOfBirth:) như sau:

	init(name: String, yearOfBirth: Int) {
        let age = 2017 - yearOfBirth
	    self.numberOfChild = 5  // xảy ra lỗi
        self.init(name: name, age: age)
    }

Với đoạn code bên trên, chúng ta sẽ dính lỗi compiler error. Trong Swift, chúng ta không được dùng self trước khi gọi Initializer Delegation. Điều này đảm bảo code của chúng ta không bị thay đổi không mong muốn. Nếu code bên trên được thực hiện, chúng ta sẽ có numberOfChild bằng 2 chứ không phải bằng 5 như chúng ta mong muốn.

5. 2 phase của quá trình khởi tạo

Quá trình khởi tạo được chia thành 2 phase:

  • phase 1: bắt đầu từ khi bắt đầu quá trình khởi tạo đến khi tất cả stored property được gán giá trị.
  • phase 2: sau khi phase 1 kết thúc Trong phase 1, chúng ta không thể sử dụng instance được khởi tạo. Nhưng trong phase 2, instance đã được khởi tạo ở phase 1, nên chúng ta có thể sử dụng instance trong phase 2:
extension Person {
    init(name: String, age: Int) {
        // phase 1
        self.name = name
        self.age = age
        self.numberOfChild = 2
        // tất cả stored property đã được khởi tạo, chuyển sang phase 2
        // phase 2
    }
    
    init(name: String, yearOfBirth: Int) {
        // phase 1 Initializer Delegation
        let age = 2017 - yearOfBirth
        self.init(name: name, age: age)
        // phase 2 Initializer Delegation
    }
}

Trong code bên trên, trước khi phase 2 của Initializer Delegation sảy ra, chúng ta không thể sử dụng self để gán giá trị cho instance.

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

Trong nhiều trường hợp, chúng ta cần xử lý để không cho việc khởi tạo sảy ra. Ví dụ trong struct Person, tuổi của mỗi người không thể nhỏ hơn 0, vì thế trường hợp truyền vào param age nhỏ hơn 0, chúng ta cần phải ngăn không cho khởi tạo instance. Để làm được điều này, chúng ta có 2 cách: sử dụng hàm khởi tạo với optional hoặc sử dụng throw exception

a. Sử dụng optional init

Chúng ta viết lại hàm init của extension Person như sau:

extension Person {
    init?(name: String, age: Int) {
        if age < 0 {
            return nil
        }
        
        self.name = name
        self.age = age
        self.numberOfChild = 2
    }
}

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

Bên trên, hàm init?(name:, age:) là khởi tạo dạng optional, vì vậy hàm này có thể trả về giá trị nil. Instance tom trong code trên là dạng optional và chúng ta cần unwrap optional khi sử dụng instance này.

b. Sử dụng throw exception

Trong nhiều trường hợp, chúng ta cần biết nhiều hơn về lý do tại sao việc khởi tạo bị thất bại, chứ không chỉ đơn thuần trả về giá trị nil. Lúc này chúng ta cần sử dụng throw exception cho hàm init.

Viết lại đoạn code cho Person như sau:

struct Person {
    let name: String
    let age: Int
    var numberOfChild: Int
}

enum InvalidPersonError: Error {
    case EmptyName
    case InvalidAge
}

extension Person {
    init(name: String, age: Int) throws {
        if name.isEmpty {
            throw InvalidPersonError.EmptyName
        }
        
        if age < 0 {
            throw InvalidPersonError.InvalidAge
        }
        
        self.name = name
        self.age = age
        self.numberOfChild = 2
    }
}

do {
    let tom = try Person(name: "Tom", age: 27)
} catch InvalidPersonError.EmptyName {
    print("name cannot empty")
} catch InvalidPersonError.InvalidAge {
    print("age cannot smaller than 0")
}

Trong code bên trên, chúng ta có thể bắt hết các trường hợp không thể tạo instance theo ý chúng ta muốn bằng việc sử dụng throw exception.

III. Tổng kết

Trên đây tôi đã giới thiệu đến các bạn về cách thức mà quá trình khởi tạo hoạt động trong Structure, trong bài viết sau của loạt bài này, chúng ta sẽ tìm hiểu về quá trình khởi tạo của Class. Hi vọng bài viết này có thể giúp ích cho các bạn tìm hiểu về khởi tạo trong Swift.

Cuối cùng, xin cảm ơn các bạn đã theo dõi bài viết của tôi. Have a nice day !!!

All Rights Reserved