+2

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

I. Giới thiệu

Trong phần 1phần 2 của loạt bài viết này, chúng ta đã tìm hiểu về quá trình khởi tạo của Struct và Class trong Swift. Trong phần 3 này, chúng ta sẽ tiếp tục tìm hiểu sâu hơn về quá trình khởi tạo của Class trong Swift, cụ thể trong phần này chúng ta sẽ tìm hiểu về quá trình khởi tạo của Subclass, kênh khởi tạo (The Initialization Funnel) và thừa kế khởi tạo.

II. Nội dung

1. Two-Phase Initialization (khởi tạo 2 pha)

Trong Swift, quá trình khởi tạo của Class được chia làm 2 pha. Hai pha của quá trình khởi tạo đảm bảo cho việc khởi tạo các instance trong Swift được "an toàn". Trong phần 2, chúng ta trải nghiệm các lỗi gặp phải trong khi viết hàm khởi tạo của các class. Các lỗi này xảy ra là do chúng ta đã vi phạm các quy tắc của 2 pha khởi tạo này. Quy tắc này rất đơn giản như sau:

  • Chỉ có thể gán giá trị cho các property của subclass (và bắt buộc phải gán toàn bộ non-optional property chưa có giá trị mặc định) trong phase 1
  • Chỉ có thể dùng được instance mới được khởi tạo trong phase 2

VÍ dụ:

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

class Employee: Person {
    var employeeCode: String
    
    init(name: String, age: Int, employeeCode: String) {
        self.name = "abc"   // lỗi
        super.init(name: name, age: age)
        self.employeeCode = employeeCode
    }
}

Hàm init(name: , age: , employeeCode: ) của class Employee bên trên mắc liền 2 lỗi của quy tắc trong 2 pha khởi tạo của class:

  • Không gán toàn bộ giá trị cho subclass trong phase 1: emplyeeCode không được gán giá trị trước hàm super.init(name: , age: )
  • Gán giá trị cho superclass trong phase 1: name chỉ có thể gán giá trị sau khi hàm super.init(name: , age: ) được gọi

Chúng ta cần sửa lại hàm khởi tạo cho class Employee như sau:

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

2. Không thừa kế khởi tạo (Un-inheriting Initializers)

Giả sử chúng ta khởi tạo 1 instance của class Employee như sau:

let tom = Employee(name: "Tom", age: 25)

Để ý các param được truyền vào trong hàm khởi tạo bên trên, chúng ta thấy hàm khởi tạo này gồm 2 tham số "name" và "age". Hàm khởi tạo này chính là hàm khởi tạo của class Person chứ không phải Employee, và ở đây chúng ta sẽ gặp lỗi: "missing argument for parameter 'employeeCode' in call".

Như vậy, các hàm khởi tạo của super class không thể được sử dụng trực tiếp trong subclass. Để có thể sử dụng hàm khởi tạo của super class, subclass của chúng ta cần phải có giá trị mặc định cho các non-optional property, hoặc subclass không có thêm property nào mới so với super class, và subclass không được có thêm hàm khởi tạo khác. Ví dụ, chúng ta gán gía trị mặc định cho employeeCode của class Employee như sau:

class Employee: Person {
  var employeeCode: String = "ABCD"
  
  //    init(name: String, age: Int, employeeCode: String) {
  //        self.employeeCode = employeeCode
  //        super.init(name: name, age: age)
  //    }
}

Lúc này, code của chúng ta "let tom = Employee(name: "Tom", age: 25)" sẽ chạy được, hàm khởi tạo này là hàm của super class Person. Tóm lại, để sử dụng được hàm khởi tạo từ super class, các property của subclass cần được gán các giá trị mặc định, và trong subclass không được có hàm khởi tạo. Trong thực tế việc này ít khi xảy ra nên việc sử dụng hàm khởi tạo của super class trong Swift hiếm khi được sử dụng.

3. Kênh khởi tạo (Initialization Funnel)

Nhắc lại một chút về convenience init và designated init mà chúng ta đã tìm hiểu trong phần 2:

designated init là kiểu khởi tạo mà tất cả các property đều được gán giá trị trong hàm init convenience init là kiểu khởi tạo mà các property không trực tiếp được gán giá trị trong đây mà trong hàm khởi tạo này, chúng ta gọi đến designated init để thật sự khởi tạo instance

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)
  }
}

Convenience init có thể gọi qua nhiều convenience init khác, nhưng cuối cùng của việc gọi convenience init phải là một designated init:

Designated init của subclass có thể gọi đến Designated init của super class:

Tuy nhiên Designated init của subclass không thể gọi đến convenience init của super class:

Tương tự vậy, convenience init của subclass cũng không thể gọi đến convenience init của super class:

Trong một hệ các class gồm super class và subclass, mỗi class có thể:

  • Không có hàm Designated init và Convenience init nào
  • Có một hoặc nhiều Designated init
  • Có một hoặc nhiều Convenience init và một hoặc nhiều Designated init

Dưới đây là biểu đồ thể hiện các khả năng có thể sảy ra của các class:

Như hình vẽ trên chúng ta thấy, nếu chúng ta gọi hàm Convenience init của subclass E, chúng ta sẽ gọi qua hàng loạt hàm khởi tạo của cả chính class E và các super class D, C, A.

4. Thừa kế hàm khởi tạo

Nhớ lại bên trên 1 chút, trong phần Un-inheriting Initializers, chúng ta chỉ sử dụng được hàm khởi tạo của super class khi subclass chúng ta không có hàm khởi tạo nào và các property được gán giá trị mặc định. Việc này khá tù, vì thế thay vì việc sử dụng trực tiếp hàm khởi tạo của super class, chúng ta có thể viết hàm khởi tạo cho subclass kế thừa từ super class để sử dụng. Để thừa kế hàm khởi tạo của super class trong subclass, chúng ta thêm tiền tố "override" vào trước hàm khởi tạo. Ví dụ:

class Employee: Person {
   var employeeCode: String
   
   init(name: String, age: Int, employeeCode: String) {
       self.employeeCode = employeeCode
       super.init(name: name, age: age)
   }
   
   override init(name: String, age: Int) {
       self.employeeCode = "ABCD"
       super.init(name: name, age: age)
   }
}

let tom = Employee(name: "Jerry", age: 25)

Như trong code trên, trong class Employee chúng ta thừa kế hàm init(name: , age: ) từ class Person. Như vậy thì chúng ta có thể thêm nhiều property và hàm init cho class Employee mà không sợ đoạn code khởi tạo instance "let tom = Employee(name: "Jerry", age: 25)" bị lỗi nữa. Bởi vì lúc này đoạn code này gọi đến hàm init(name: , age: ) của class Employee chứ không phải class Person nữa.

III. Tổng kết

Trên đây chúng ta đã tìm hiểu về quá trình khởi tạo của class trong Swift. Hi vọng thông qua bài viết này, và cả 2 bài viết trước trong loạt bài này, có ích với các bạn trong việc tìm hiểu về quá trình khởi tạo của Struct và Class trong Swift. 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í