Swift Generics Part II

Theo tài liệu: Tutorial, và tiếp theo từ Phần I

Writing a Generic Data Structure

Queue là 1 kiểu cấu trúc dữ liệu mà bạn chỉ có thể thêm phần tử mới vào 1 đầu, và lấy ra phần tử ở đầu kia, đúng như ý nghĩa "xếp hàng" của nó. Bạn hãy tạo 1 struct như sau:

struct Queue<Element> {
}

Ở đây bạn đã định nghĩa Queue như là 1 generic type với một type argument (đối số truyền vào) là Element. Tức là khi khai báo 1 Queue trong thực tế bạn sẽ phải truyền kiểu vào Element. Ví dụ như Queue<Int>Queue<String> sẽ trở thành những kiểu khác nhau khi runtime, nó chỉ có thể enqueue và dequeue strings và integers tương ứng. Thêm property sau vào queue:

fileprivate var elements: [Element] = []

Bạn sử dụng array này để lưu giữ các phần tử, và bạn init nó như 1 array rỗng. Chú ý rằng bạn có thể sử dụng từ khoá Element như 1 type thực sự cho dù nó sẽ được fill vào sau. Bạn đánh dấu nó là fileprivate bởi vì bạn không muốn khi sử dụng Queue<Element> người ta có thể access trực tiếp đến elements. Bạn muốn bắt buộc mọi người khi sử dụng phải thông qua methods mà bạn cho phép. Ngoài ra sử dụng fileprivate thay vì private sẽ cho phép bạn sử dụng elements được ở trong phần extension của Queue nếu bạn muốn viết về sau. CUối cùng, implement 2 methods chính của Queue như sau:

mutating func enqueue(newElement: Element) {
 elements.append(newElement)
}

mutating func dequeue() -> Element? {
 guard !elements.isEmpty else { return nil }
 return elements.remove(at: 0)
}

Một lần nữa, kiểu parameter Element có thể sử dụng ở bất cứ đâu trong body của Struct, bao gồm cả bên trong methods. Bạn đã implement 1 type-safe generic data structure giống như ở trong standard library. Hãy thử sử dụng queue của bạn với những dòng code dưới đây:

var q = Queue<Int>()

q.enqueue(newElement: 4)
q.enqueue(newElement: 2)

q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()

Hãy thử vui vẻ bằng cách tạo ra các error có thể như add string vào queue, bạn càng hiểu về những error này thì sẽ càng dễ dàng hơn cho bạn trong việc nhận biết và đối phó với chúng trong các dự án phức tạp.

Writing a Generic Function

Hãy nghĩ đến việc có 1 yêu cầu như sau: Bạn hãy viết 1 mothods với đầu vào là 1 dạng dictionary: keys và values, rồi convert nó thành 1 list? Hãy thêm function sau vào playground:

func pairs<Key, Value>(from dictionary: [Key: Value]) -> [(Key, Value)] {
 return Array(dictionary)
}

Bạn có thể thấy function này là generic đối với 2 kiểu mà bạn đặt tên là KeyValue. Parametter duy nhất là 1 dictionary với 1 cặp key-value thuộc kiểu KeyValue. Giá trị trả về là 1 array các tuples của form (Key, Value). Bạn có thể sử dụng pairs(from:) với bất kỳ dictionary hợp lệ nào, và nó sẽ hoạt động:

let somePairs = pairs(from: ["minimum": 199, "maximum": 299]) 
// result is [("maximum", 299), ("minimum", 199)]

let morePairs = pairs(from: [1: "Swift", 2: "Generics", 3: "Rule"]) 
// result is [(2, "Generics"), (3, "Rule"), (1, "Swift")]

Bởi vì bạn không thể điều khiển được thứ tự mà các items của dictionary đi vào array, bạn có thể thấy thứ tự của các giá trị ttrong tuple lại là ''Generic", "Rule", và "Swift". Khi runtime, mỗi KeyValue sẽ hoạt động như 1 function riêng biệt. Bạn đã tạo ra 1 function đơn mà có thể trả về những kiểu khác nhau với cách gọi khác nhau. Điều đó thật tuyệt vời, bạn có thể thấy việc đặt logic ở chỉ 1 nơi có thể làm đơn giản code của mình đi rất nhiều. Thay vì cần tới 2 functions khác nhau, bạn đã xử lý cả 2 cách gọi chỉ với 1 function. Bây giờ bạn đã biết được cơ bản về cách làm việc với generic types và functions, đã đến lúc để chuyển sang phần sâu hơn về generic.

Constraining a Generic Type

Giả sử có 1 yêu cầu về viết 1 function để sort 1 array và đưa ra giá trị trung bình (middle value). Add function sau vào playground:

func mid<T>(array: [T]) -> T? {
 guard !array.isEmpty else { return nil }
 return array.sorted()[(array.count - 1) / 2]
}

Bạn sẽ nhận được 1 error như sau:

error: missing argument for parameter 'by' in call
   return array.sorted()[(array.count - 1) / 2]
                       ^
                       by: <#(T, T) -> Bool#>

Vấn đề ở đây đó là để sorted() có thể hoạt động, elements của array phải là kiểu Comparable. Bạn cần phải bằng cách nào đó nói cho Swift biết rằng mid có thể nhận bất kỳ array nào là parameter miễn sao kiểu của element trong array phải implements Comparable. Chuyển function declaration thành như sau:

func mid<T: Comparable>(array: [T]) -> T? {
 guard !array.isEmpty else { return nil }
 return array.sorted()[(array.count - 1) / 2]
}

Ở đây bạn đã sử dụng cú pháp : để thêm vào 1 type constraint vào generic type parameter T. Bây giờ bạn chỉ cần gọi function với 1 array của các phần từ Comparable, method sorted() sẽ hoạt động bình thường. Hãy thử constrained function bằng cách thêm vào:

mid(array: [3, 5, 1, 2, 4]) // 3

Bây giờ, bạn đã hiểu về constraints bạn có thể tạo ra phiên bản generic của function add mà chúng ta đã nói đến ở phần I, nó sẽ trở nên thanh lịch hơn rất nhiều. Thêm protocol và extensions sau vào playground:

protocol Summable { static func +(lhs: Self, rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}

Đầu tiên, bạn tạo ra 1 Summable protocol - bất kỳ 1 kiểu nào conforms lại protocol này đều sử dụng được phép toán cộng +. Rồi bạn chỉ ra rõ rằng IntDouble conform theo nó. Bây giờ, sử dụng generic parameter T và hạn chế kiểu (type constraint), bạn có thể tạo ra 1 generic function add:

func add<T: Summable>(x: T, y: T) -> T {
 return x + y
}

Bạn đã vừa giảm số lượng functions của mình xuống 1, và bnor đi cả những code bị lặp. Bạn có thể dùng function mới này cho cả integers và doubles, và thậm chí còn dùng được cho cả strings:

let addIntSum = add(x: 1, y: 2) // 3
let addDoubleSum = add(x: 1.0, y: 2.0) // 3

extension String: Summable {}
let addString = add(x: "Generics", y: " are Awesome!!! :]") // "Generics are Awesome!!! :]"