Tìm hiểu về Generic

Generic programming là 1 cách để viết function và kiểu dữ liệu trong khi đưa ra những giả định về loại dữ liệu đang được dùng. Generics swift viết code không cần xác định về loại dữ liệu cụ thể, cho phép trừu tượng hóa để tạo ra code clean hơn, ít lỗi hơn.

1. Tổng quan

VD ta có hàm cộng 2 số như sau:

func addInt(x: Int, y: Int) -> Int {
  return x + y
}
func addDouble(x: Double, y: Double) -> Double {
  return x + y
}
let a = addInt(x: 1, y: 2)
let b = addDouble(x: 1.0, y: 2.0)

Hàm addInt()addDouble() là khác nhau nhưng nội dung trong hàm giống nhau là return x+y . Nó là 2 hàm nhưng code bên trong lại lặp lại. Generics có thể được dùng để gộp 2 hàm này làm 1 và xóa redundant code. Ngoài ra, có thể bạn không nhận ra. Nhưng 1 số cấu trúc phổ biến mà ta hay sử dụng như Array, Dictionary hay Optional đều là genric type.

Array

let numbers = [1, 2, 3]
let a = numbers[0]

Array là 1 generic type. Generic type yêu cầu 1 tham số phải được xác định. Khi bạn tạo 1 insatance và chỉ định 1 kiểu tham số sẽ cho phép xác định kiểu cụ thể của instance. Để thấy rõ hơn về generic của Array ta có thể khai báo như sau:

var numbers : Array<Int> = []
numbers.append(1)
numbers.append(2)
numbers.append(3)

let a = numbers[0]

Bạn sẽ bị lỗi như sau nếu insert vào number 1 kiểu khác Cannot convert value of type ‘String’ to expected argument type ‘Int’. Hàm append của Array được gọi là generic method.

Dictionary

Dictionary cũng là kiểu generic

let codes = ["English": "en", "Japan": "jp",]
let code = codes["English"]

Optinal

Cách tạo 1 kiểu optional string với full syntax

let optionalName = Optional<String>.some("Ozawa")
if let name = optionalName {}

Check kiểu của name sẽ là String. Đó là optional binding (if-let) là 1 generic transformation của sorts. Nó truyền vào 1 generic value của kiểu T? và trả ra kiểu T. Điều đó có nghĩa là bạn có thể dùng if let với tất cả các kiểu.

2. Generic Data Structure

Ta thử tạo 1 Queue là data structures kiểu giống list hoặc stack.

struct Queue<Element> {
}

Queue là 1 generic type với 1 type argument là Element. 1 cách gọi khác Queue là generic over type Element. Nếu ta khai báo Queue<Int> hay Queue<String> thì các kiểu dữ liệu trong nó sẽ là Int hoặc String. Ta định nghĩa 1 property và 2 method cho nó:

fileprivate var elements: [Element] = []

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

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

Sau đó, ta khởi tạo và chạy thử. Kiểm tra các giá trị in ra màn hình.

var q = Queue<Int>()

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

q.dequeue() //5
q.dequeue() //2
q.dequeue() //nil

3. Generic Function

Ta muốn viết 1 hàm truyền vào 1 dictionary và trả ra là 1 list key-value của dictionary. Generic function sẽ giúp ta làm việc đó

func pairs<Key, Value>(from dictionary: [Key: Value]) -> [(Key, Value)] {
  return Array(dictionary)
}
let somePairs = pairs(from: ["min": 199, "max": 299]) 
//  [("max", 299), ("min", 199)]

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

Ta đã có thể tạo 1 hàm có khả năng trả về nhiều kiểu dữ liệu khác nhau với nhiều cách gọi khác nhau.

4. Constraining a Generic Type

Ta muốn lấy ra 1 giá trị ở giữa của 1 list đã được sort. Ta khai báo như sau:

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

Ta sẽ bị error. Để sort có thể work được, element của array cần được so sánh. Ta cần khai báo element của array kiểu Comparable

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

Ta đã thêm 1 type constraint vào generic type T. Chỉ cần gọi hàm của 1 array với element Comparable và sorted() sẽ luôn chạy. Tương tự như ở phần 1, ta muốn cộng 2 số với nhiều kiểu dữ liệu khác nhau. Ta có kiểu Summable

func add<T: Summable>(x: T, y: T) -> T {
  return x + y
}
let a = add(x: 1, y: 2) // 3
let b = add(x: 1.0, y: 2.0) // 3
let c = add(x: "A", y: "B") //AB

5. Extending a Generic Type

Ta cũng có thể extend Generic Data Structure và viết thêm các hàm extend cho nó

extension Queue {
  func peek() -> Element? {
    return elements.first
  }
}

q.enqueue(newElement: 5)
q.enqueue(newElement: 2)
q.peek() // 5

6. Subclassing a Generic Type

Ta có 1 generic class

class Box<T> {
}

Ta muốn extend box nhưng vẫn giữ là generic để có thể truyền vào nhiều kiểu dữ liệu khác nhau. Nhưng ta cũng muốn có 1 subclass đặc biệt để biết những item cụ thể trong box. Ta có thể làm như sau:

class Gift<T>: Box<T> {
  func wrap() {
    print("Wrap")
  }
}
class Pizza {
}
class PizzaBox: Gift<Pizza> {
override func wrap() {
  print("Wrap pizza")
}
}
class Shoe {
}
class ShoeBox: Box<Shoe> {
}

Ta chạy thử và xem giá trị

let box = Box<Pizza>() 
let gift = Gift<Pizza>() 
let shoeBox = ShoeBox()
let pizzaBox = PizzaBox()
gift.wrap() // Wrap
pizzaBox.wrap() // Wrap pizza

7. Enumerations With Associated Values

Dưới đây là generic enum với 2 value: 1 là value trả về, 2 là error

enum Result<Value> {
  case success(Value), failure(Error)
}

Ngoài ra ta muốn khai báo 1 error cụ thể cho từng trường hợp. Ta đã định nghĩa 1 error enumeration type và 1 hàm chia 2 số Int:

enum MathError: Error {
  case divisionByZero
}

func divide(_ x: Int, by y: Int) -> Result<Int> {
  guard y != 0 else {
    return .failure(MathError.divisionByZero)
  }
  return .success(x / y)
}

let a = divide(10, by: 2) // .success(5)
let b = divide(10, by: 0) // .failure(MathError.divisionByZero)