Khởi tạo (Initialization) trong Swift

by DaiLM
1 comment 929 views

Việc khởi tạo trong bất cứ ngôn ngữ lập trình nào đều rất rất quan trọng, trong một project thì bạn sẽ liên tục phải thực hiện khởi tạo các instance của các struct, class hoặc enum. Việc hiểu rõ và sử dụng thành thạo quá trình khởi tạo trong swift sẽ giúp bạn tăng performance cũng như chất lượng của source code, dưới đây sẽ là bài viết đào sâu hơn vào quá trình khởi tạo trong swift nhé.

I. Khởi tạo trong Structure

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

Việc khởi tạo này đơn gỉản và giống với việc tạo 1 instance của method không có parameter nào, ví dụ

let phoneX = Phone()

Syntax

init() {
   // New Instance sẽ được khởi tạo ở đây
}

Example

struct Rectangle {
    var length: Double
    var breadth: Double
    init() {
        length = 12.0
        breadth = 5.0
    }
}

var rec = Rectangle()
print("area of rectangle is \(rec.length * rec.breadth)")

Ở đoạn code bên trên 1 instance là rec được khởi tạo với các thuộc tính là chiều rộng và chiều dài lần lượt là length = 12, và breadth = 5, sau khi chạy đoạn code trên sẽ thu được kết quả:

area of rectangle is 60.0

Chuyện gì sẽ xảy ra nếu xoá phần init() trong đoạn code trên:

struct Rectangle {
    var length: Double
    var breadth: Double
}

var rec = Rectangle()
print("area of rectangle is \(rec.length * rec.breadth)")

Khi chạy đoạn code trên sẽ có thông báo lỗi là

error: missing argument for parameter 'length' in call
let rec = Rectangle()

Ở đây thì trình biên dịch đang thông báo 1 error và bắt chúng ta phải khởi tạo với đối số của length, taị sao lại như vậy???
Bởi vì bạn đang cố khởi tạo cho struct Rectangle mà các stored property chưa được gán giá trị mặc định
Đoạn code sửa lỗi này được sửa như sau:

struct Rectangle {
    var length: Double = 12.0
    var breadth: Double = 5.0
}

var rec = Rectangle()
print("area of rectangle is \(rec.length * rec.breadth)")

Chúng ta gán giá trị mặc định cho tất cả các stored property, ở đây thì chúng ta đã tạo ra instance rec với các stored property được gán giá trị ban đầu lần lượt là 12.0 và 5.0,
vậy là vấn đề được giải quyết.

Như trên thì chúng ta đã được tiếp cận với việc khởi tạo 1 struct với kiểu mặc định, nhìn lại 2 cách viết ở trên nhé:

// 1
struct Rectangle {
    var length: Double
    var breadth: Double
    init() {
        length = 12.0
        breadth = 5.0
    }
}

// 2
struct Rectangle {
    var length: Double = 12.0
    var breadth: Double = 5.0
}

2 cách viết nói trên đều đưa đến 1 kết quả như nhau là đều tạo ra 1 instance có các giá trị khởi tạo mặc định là 12.0 và 5.0,
tuy nhiên thì cách viết thứ 2 sẽ giảm bớt line code và tường minh hơn, vì vậy trong trường hợp cần khởi tạo với giá trị mặc định thì chúng ta nên chọn cách viết thứ 2 nhé.

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

Khi cần khởi tạo 1 instance với các giá trị của property được truyền vào như dưới đây:

struct Rectangle {
    var length: Double
    var breadth: Double

    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }
}

let rect = Rectangle(length: 6, breadth: 12)
print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Kết quả sau khi chạy:

length is: 6.0
breadth is: 12.0

Giờ bạn thử xoá phần init trong đọan code trên xem có gì xảy ra nhé.

struct Rectangle {
    var length: Double
    var breadth: Double
}

let rect = Rectangle(length: 6, breadth: 12)
print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Oh, Code trên vẫn chạy bình thường. Do với struct thì Swift mặc định đã tạo sẵn cho bạn 1 hàm khởi tạo rồi, tuy nhiên nếu sử dụng hàm khởi tạo mặc định này thì chúng ta sẽ gặp không ít phiền phức như:

  • Khi đổi thứ tự các stored property thì khi gọi hàm khởi tạo mặc định thứ tự của Parameter cũng phải đổi tương ứng
struct Rectangle {
    var breadth: Double
    var length: Double
}

Khi gọi phải đảo lại parameter:

let rect = Rectangle(breadth: 6, length: 12)
  • Khi thêm init với name các parameter khác với mặc định trong struct:
struct Rectangle {
    var length: Double
    var breadth: Double

    init(length: Double) {
        self.length = length
        self.breadth = 5.0
    }
}

let rect = Rectangle(length: 6, breadth: 12) // Lỗi
print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Đoạn code trên sẽ lỗi vì chúng ta đã viết 1 hàm init với parameter là length ở trong struct, việc này sẽ khiến cho swift không tự động viết hàm khởi tạo cho bạn nữa.
Vậy làm thế nào để vừa có hàm khởi tạo tự viết và cả hàm khởi tạo tự động mà swift sẽ tạo cho chúng ta, đơn giản chỉ cần chuyển hàm init của chúng ta qua extension:

struct Rectangle {
   var length: Double
   var breadth: Double
}
extension Rectangle {
   init(length: Double) {
       self.length = length
       self.breadth = length + 5.0
   }
}

Okey, vấn đề được giải quyết.

3. Khởi tạo với các Parameter mặc định

Đôi khi trong các hàm khởi tạo, bạn muốn gán giá trị mặc định cho các tham số như dưới đây:

struct Rectangle {
    var length: Double
    var breadth: Double

    init(length: Double = 12.0, breadth: Double = 5.0) {
        self.length = length
        self.breadth = breadth
    }
}

let rectA = Rectangle()
print(" rectA:\n length is: \(rectA.length)\n breadth is: \(rectA.breadth) ")

let rectB = Rectangle(length: 50)
print(" rectB:\n length is: \(rectB.length)\n breadth is: \(rectB.breadth) ")

let rectC = Rectangle(length: 20, breadth: 6)
print(" rectB:\n length is: \(rectC.length)\n breadth is: \(rectC.breadth) ")

Kết quả là:

rectA:
length is: 12.0
breadth is: 5.0
rectB:
length is: 50.0
breadth is: 5.0
rectB:
length is: 20.0
breadth is: 6.0

Việc khởi tạo với các parameter mặc định khiến cho việc gọi hàm được đa dạng hơn, với tuỳ trường hợp thì sẽ có hay không có parameter.

4. Initializer Delegation

Initializer Delegation thực chất chính là việc gọi hàm khởi tạo từ các hàm khởi tạo khác, như ví dụ dưới đây:

struct Rectangle {
    var length: Double
    var breadth: Double

    // 1
    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }

    // 2
    init(length: Double) {
        let breadth = length + 2.0
        self.init(length: length, breadth: breadth)
    }

}

let rect = Rectangle(length: 50)
print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Kết quả là:

rect:
length is: 50.0
breadth is: 52.0

Ở đoạn code trên thì chúng ta có tới 2 hàm init trong struct, ở hàm init thứ 2 thì chúng ta thực hiện tính toán bề rộng trước khi gọi đến hàm init 1.
Đây chính là Initializer Delegation nhé, viết ra 1, 2 lần là quen tay ngay.

Điều gì sẽ xảy ra khi chúng ta gọi self trước khi call hàm init số 1, sửa đoạn code như sau:

struct Rectangle {
    var length: Double
    var breadth: Double

    // 1
    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }

    // 2
    init(length: Double) {
        self.length = 5.0 // Báo lỗi: 'self' used before 'self.init' call or assignment to 'self'

        let breadth = length + 2.0
        self.init(length: length, breadth: breadth)
    }

}

let rect = Rectangle(length: 50)
print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Lúc này thì compiler báo lỗi: ‘self’ used before ‘self.init’ call or assignment to ‘self’
Lỗi này thông báo rằng chúng ta không được dùng self trước khi gọi Initializer Delegation. Việc này đảm bảo code sẽ không bị thay đổi không mong muốn. Giả sử như đoạn code trên pass qua compiler thì lúc này length = 50.0 chứ không phải bằng 5.0 như mong muốn.

Chú ý trong phần I:

- Khi khởi tạo 1 instance, chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.
- Không gọi self trước khi call Initializer Delegation

II. Khởi tạo trong Class

Ở phần trên giới thiệu việc khởi tạo cũng như các vấn đề thường gặp khi khởi tạo 1 stucture. Đối với class thì việc khởi tạo sẽ tương đối khác và biến hoá hơn, biến hoá dư nào thì các bạn đọc ở bên dưới nhé:

1. Hai loại khởi tạo trong class

Riêng với class thì khởi tạo chia thành 2 loại là designated initializers and convenience initializers

1.1. Designated Initializer

Được coi là việc khởi tạo chính của class, đây là cách khởi tạo thông thường nhất. Trong hàm khởi tạo chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.

class Rectangle {
    var length: Double
    var breadth: Double

    // Designated Initializer
    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }
}

Khi class của bạn có stored property là non-opional thì Việc khởi tạo với kiểu Designated Initializer là bắt buộc.

1.1. Convenience Initializers

Được coi là hàm hỗ trợ việc khởi tạo của class, trong hàm khởi tạo này chúng ta sẽ gọi đến các hàm khởi tạo khác ( Khá giống với Initializer Delegation trong struct)

class Rectangle {
    var length: Double
    var breadth: Double

    // Designated Initializer
    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }

    // Convenience Initializer
    convenience init(length: Double, area: Double) {
        let breadth = area / length
        self.init(length: length, breadth: breadth)

    }
}

let rect = Rectangle(length: 5, area: 20)
print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

Kết quả sau khi chạy

rect:
length is: 5.0
breadth is: 4.0

Nhìn vào ví dụ trên thì chúng ta cũng thấy được vai trò của hàm Convenience Initializer rồi, khi bạn cần xử lý abc-xyz ở trong hàm khởi tạo, để việc khởi tạo gọi là tiện lợi nhất đối với bài toán của bạn,
thì hãy nghĩ ngay đến Convenience Initializer nhé.

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

Để hiểu rõ phần này chúng ta đến với phần ví dụ luôn nhé:

class Rectangle {
    var length: Double
    var breadth: Double

    // Designated Initializer
    init(length: Double, breadth: Double) {
        self.length = length
        self.breadth = breadth
    }
}

class Square: Rectangle {
    var area: Double
}

Khi run đoạn code trên chúng ta sẽ nhận được lỗi là

stored property 'area' without initial value prevents synthesized initializers
    var area: Double

Như đã được phân tích ở phần I, khi khởi tạo 1 instance bất kỳ thì luôn phải đảm bảo các giá trị mặc định cho tất cả các stored property non-optional
Ở đây do đã khởi tạo thêm biến area ở subclass là Square nên swift yêu cầu phải gán giá trị.

Như dưới đây là 2 cách để pass qua compiler

// option 1
class Square: Rectangle {
    var color: String = "red"
}

// option 2
class Square: Rectangle {
    var color: String
    init(length: Double, breadth: Double, color: String) {
        self.color = color
        super.init(length: length, breadth: breadth)
    }
}

Ở option 1, sẽ gán giá trị mặc định cho property color là “red”
Ở option 2, thêm một hàm khởi tạo ở subclass Square để thực hiện Designated Initializer

Vậy sẽ thế nào nếu chúng ta viết như sau:

class Square: Rectangle {
    var color: String
    init(length: Double, breadth: Double, color: String) {
        self.color = color
        self.length = length
        self.breadth = breadth
    }
}

Lúc này ta nhận được error với nội dung:

error: 'self' used in property access 'length' before 'super.init' call
        self.length = length

Compiler đang báo chúng ta đang sử dụng self để truy cập vào property length trước khi gọi super.init.
super ở đây chính là class cha Rectangle, việc sử dụng super.init là bắt buộc để gán giá trị cho lengt và breadth.

Tóm lại thì việc khởi tạo của subclass cũng chung quy tắc là phải gán giá trị ban đầu cho tất cả các non-optional stored property, thứ tự thì sẽ là gán property ở subclass trước, sau đó là gọi super.init để gán cho các property ở lớp cha.

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

Ở subclass, nếu chúng ta muốn khởi tạo bằng cách gọi hàm khởi tạo của class cha thì bắt buộc subclass đó phải có giá trị mặc định của tất cả các non-optional stored property và subclass không có hàm init. Việc này khiến khá khó chịu, vì vậy để sử dụng hàm khởi tạo của class cha chúng ta có thể sử dụng thừa kế hàm khởi tạo như sau:

class Square: Rectangle {
    var color: String

    init(length: Double, breadth: Double, color: String) {
        self.color = color
        super.init(length: length, breadth: breadth)
    }

    override init(length: Double, breadth: Double) {
        self.color = "red"
        super.init(length: length, breadth: breadth)
    }
}

Đơn giản chỉ cần override lại hàm init từ class cha là Rectangle, chúng ta có thể sài hàm khởi tạo của class cha một cách ngon lành rồi.

III. Tổng kết

Bài viết này mình đã giới thiệu về khởi tạo ở structure cũng như class, hy vọng sẽ giúp các bạn hiểu sâu hơn về khởi tạo trong swift.
Phần sau mình sẽ giới thiệu về các cách khởi tạo của 1 custom class thuộc UI element nhé (ví dụ như UIView)
Mong các bạn đón đọc.

1 comment
2

1 comment

nguyenmanhhung131298 October 21, 2019 - 4:15 PM

Còn trường hợp required init() nữa anh nhỉ?

Reply

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

%d bloggers like this: