Protocol trong Swift
1. Giới thiệu Protocol Khi nói đến Protocol nghĩa là ta đang nói đến 1 cái gì đó trừu tượng. Vậy trừu tượng là sao ? Thông thường ta sẽ biết được “Con mèo kêu meo meo. Con chó kêu gâu gâu. Con vịt kêu cạp cạp” nhưng ta lại không thể dám chắc rằng “Vật nuôi thì kêu ...
1. Giới thiệu Protocol
Khi nói đến Protocol nghĩa là ta đang nói đến 1 cái gì đó trừu tượng. Vậy trừu tượng là sao ? Thông thường ta sẽ biết được “Con mèo kêu meo meo. Con chó kêu gâu gâu. Con vịt kêu cạp cạp” nhưng ta lại không thể dám chắc rằng “Vật nuôi thì kêu thế nào hoặc gia giầm thì kêu ra sao”. Vì thế ta gọi nó là trừu tượng, mặc dù ta luôn biết là vật nuôi hay gia cầm nào cũng có thể kêu được.
Trong thế giới lập trình cũng vậy, người ta quy các vấn đề / hành vi / thuộc tính mà được sử dụng trong rất nhiều các đối tượng khác nhau lập thành protocol. Để minh họa mình sẽ tạo một protocol CanMakeSound (có thể kêu được) và cách sử dụng protocol này.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
protocol CanMakeSound { func sound() // method trong protocol sẽ không có thân hàm (body), vì nó trừu tượng, có ghi cũng không biết ghi gì :D } // protocol có thể được adopted / implemented bởi 1 Struct struct Dog: CanMakeSound { // Lúc này method sound là bắt buộc phải được định nghĩa và có thân hàm. func sound() { print("Gau gau") // Ta biết được con chó nó kêu thế nào } } // protocol có thể được dùng với class class Cat: CanMakeSound { func sound() { print("Méo méo") } } // hoặc Enum cũng được enum DogStatus: CanMakeSound { case Normal, BeHungry func sound() { switch self { case .Normal: print("Gâu gâu") case .BeHungry: print("ấu ấu ...") // chỗ này mình tự chế để minh họa thôi :)) } } } |
Tới đây ta biết được protocol có thể được sử dụng bởi class, struct và cả enum (thật bá đạo). Khi protocol được adopted ở đâu thì nơi đó (class/struct/enum) phải định nghĩa và viết chi tiết cho tất cả các methods mà protocol đó có. Tới đây chúng ta thấy protocol có vẻ giống với khái niệm interface trong các ngôn ngữ hướng đối tượng khác như Java, C#, … Trong Swift protocol không chỉ có method mà còn có property nữa (bá đạo tập 2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
protocol HasName { var name:String { get set } // thuộc tính khi được adopted phải được phép thay đổi func fullName() -> String } protocol CanMove { var legCount:Int { get } func move() } // Ta có thể adopt nhiều protocol struct Duck: HasName, CanMove { // let name:String sẽ báo lỗi vì theo protocol thì biến này phải được phép thay đổi => đỗi let thành var var name:String let legCount:Int = 2 // ở đây let hay var đều được nhé, nhưng biến này mà cho đổi được thì kì lắm func fullName() -> String { return name } func move() { print("I have (legCount) legs and I can move. Let's move !!!") } } var myDuck = Duck(name: "Donal") print("Hello, I am (myDuck.fullName())") myDuck.move() |
2. Mutating methods trong protocol
Mutabling method trong protocol nghĩa là cho phép method đó thay đổi thuộc tính nội tại (instance property) của Struct hoặc Enum mà adopt protocol đó. Để minh họa mình sẽ edit lại enum DogStatus ở trên một chút:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
protocol CanEat { mutating func eat() } enum DogStatus: CanMakeSound, CanEat { // ... giống như khai báo ở vd đầu tiên mutating func eat() { switch self { case .Normal: print("I am not hungry :(") case .BeHungry: print("I am eating ... Ok I am full") self = .Normal // cập nhật giá trị của enum } } } |
3. Method khởi tạo (init) trong protocol
Trong Swift ta có các method đặc biệt dùng để khởi tạo Class, Struct và Enum cũng không ngoại lệ (bá đạo tập 3). Protocol trong Swift cũng có thể chứa các method init nhằm đảm bảo rằng ai mà adopt nó thì phải viết chi tiết cho hàm khởi tạo. Thế nhưng riêng với class thì có hơi khác 1 tí:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
protocol MustHaveInit { init() // init thì sẽ ko cần func phía trước } struct TestStruct: MustHaveInit { var name:String // Chỗ này bình thường nhé, ko cần từ khóa required init() { name = "Unnamed" } } class B: MustHaveInit { required init() { // Chỗ này bắt buộc phải có từ khóa required } } class C: B { required init() {} // C là subclass của B, nếu C có hàm init thì ta vẫn cần từ khóa required } final class D: MustHaveInit { init() {} // Ở đây thì không cần nữa do D là final class, nghĩa là sẽ không bao giờ có subclass } |
Đối với class, ta cần từ khóa required trước khai báo hàm khởi tạo. Mình sẽ làm 1 bài khác chi tiết hơn về các method khởi tạo.
Thực tế ta sẽ thường gặp từ khóa này khi subclass một UIView và viết lại hàm init cho nó. Trình biên dịch bắt ta phải required method. Đại loại là như sau:
1 2 3 4 5 6 7 8 9 10 |
class MyView:UIView { init() {} // Chỗ này bắt buộc nếu ta có viết lại init(), XCode sẽ nhắc và tự thêm như sau required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } |
Lý do là tại vì bản thân UIView có adopt 1 protocol tên là: NSCoding và trong protocol này có hàm public init?(coder aDecoder: NSCoder)
4. Protocol for class-only
Nói nhanh gọn là protocol class-only nghĩa là chỉ xài được cho class mà thôi. Mặc định Protocol là kiểu giá trị (value type), và ta có thể biến nó thành kiểu tham chiếu bằng cách:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
protocol ForClassProtocol:class { // mọi để ý sau tên Protocol có ":class" func test() } class TestClass:ForClassProtocol { // OK func test() {} } struct TestStruct:ForClassProtocol { // Lỗi func test() {} } enum TestEnum:ForClassProtocol { // Lỗi func test() {} } |
Protocol:class còn được sử dụng vào mục đích tạo delegate. Nếu ta dùng 1 object (class instance) để làm delegate thì chúng ta khai với từ khóa weak (để chống retain cycle dẫn tới leak memory). Nhưng nếu ta gán 1 giá trị (enum/struct) và biến weak này thì sẽ bị báo lỗi do biến weak chỉ đi với kiểu tham chiếu (class) thôi.
1 2 3 4 5 6 7 8 9 10 |
protocol DownloaderDelegate:class { func downloaderHasFinished(downloader:Downloader) } class Downloader { weak var delegate:DownloaderDelegate? // Rõ ràng ở đây nếu ta không có :class phía trên protocol sẽ bị báo lỗi. Bỏ weak ra thì sẽ bị retaint cycle nên ta cần nó là như thế } |
5. Optional Protocol Requirements
Tới đây chúng ta đã biết 1 nguyên tắc cơ bản khi dùng protocol là trong đó có bao nhiêu method thì mình phải viết lại bấy nhiêu method khi mình adopt nó. Như vậy thì lỡ mà protocol đó có quá nhiều method thì sao trời, ngồi viết hết chắc cắn lưỡi quá @@.
Apple cung cấp cho ta 1 giải pháp là dùng thêm từ khóa @objc, từ khóa này chỉ ra rằng protocol này có thể dùng được với code Objective C, nghe chả liên quan gì cả nhưng thật sự nó như vậy, bất kể ta có dùng code Objective C trong project hay không. Từ khóa này cũng sẽ làm cho protocol trở thành for class-only
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Trước protocol sẽ có @objc @objc protocol OptionalMethodsProtocol { func mustImplement() optional func haveNotImplement() } class SomeClass: OptionalMethodsProtocol { @objc func mustImplement() { // từ khóa @objc ở đây là bắt buộc } // ta không cần viết method haveNotImplement vì nó là optional } struct Pet:OptionalMethodsProtocol {} // báo lỗi vì protocol trên chỉ dành cho class enum Status:OptionalMethodsProtocol {} // báo lỗi vì protocol trên chỉ dành cho class |
Vậy là thực tế Protocol trong Swift (thuần) sẽ không có khái niệm optional method … nghĩa là nếu ta dùng Protocol cho 1 struct/enum thì có bao nhiêu method thì viết bấy nhiêu lại thôi.
Techtalk via IDE Academy