Bài 3.3 Type Casting And Inspection - Develop in Swift - Fundamentals

Khi bạn làm việc với dữ liệu, kiểu dữ liệu đóng vai trò rất quan trọng. Ví dụ, nếu một function trả về một Int, bạn biết rằng bạn có thể sử dụng giá trị của nó trong một biểu thức toán học. Nhưng điều gì sẽ xảy ra nếu thông tin về kiểu dữ liệu không cụ thể và bạn cần kiểm tra dữ liệu kỹ hơn để xác định cách sử dụng nó?

Trong bài học này, bạn sẽ tìm hiểu lý do tại sao một số dữ liệu chỉ có thể được biểu diễn bằng một kiểu dữ liệu rộng hơn và cách bạn có thể kiểm tra các loại dữ liệu cụ thể trước khi sử dụng nó.

What You'll Learn

  • Cách kết hợp các giá trị thuộc các kiểu dữ liệu khác nhau vào cùng một collection.
  • Cách kiểm tra kiểu dữ liệu cụ thể của giá trị trong một collection không đồng nhất.
  • Cách hạ cấp (downcast) một đối tượng thành một kiểu cụ thể trước khi truy cập các thuộc tính và phương thức của nó.

Vocabulary

Từ vựng Định nghĩa
Type Casting (Ép kiểu) Thao tác chuyển đổi một kiểu dữ liệu sang kiểu dữ liệu khác.
Type Inspection (Kiểm tra kiểu) Thao tác kiểm tra kiểu dữ liệu của một biến hoặc hằng số.
Downcast (Hạ cấp) Ép kiểu một đối tượng từ một kiểu dữ liệu cha xuống một kiểu dữ liệu con.
Conditional Cast (Ép kiểu có điều kiện) Ép kiểu một đối tượng sang một kiểu khác chỉ khi có thể thực hiện được. Toán tử as? được sử dụng cho mục đích này.
as! Toán tử ép kiểu bắt buộc. Nó sẽ cố gắng ép kiểu một đối tượng sang kiểu được chỉ định và gây ra lỗi runtime nếu ép kiểu không thành công.
as? Toán tử ép kiểu có điều kiện. Nó sẽ cố gắng ép kiểu một đối tượng sang kiểu được chỉ định và trả về nil nếu ép kiểu không thành công.
Any Một kiểu dữ liệu đặc biệt có thể đại diện cho bất kỳ kiểu dữ liệu nào, bao gồm cả kiểu dữ liệu structure và class.
AnyObject Một kiểu dữ liệu đặc biệt có thể đại diện cho bất kỳ kiểu dữ liệu class nào.
is Toán tử kiểm tra kiểu. Nó trả về true nếu một instance thuộc kiểu được chỉ định và false nếu không.

Ép Kiểu (Type Casting)

Giả sử công việc của Lou là đến thăm nhà của khách hàng và chăm sóc thú cưng của họ. Khi đến mỗi địa điểm, anh ấy thực hiện các nhiệm vụ khác nhau tùy thuộc vào loại động vật. Nếu thú cưng là rùa, anh ấy dọn dẹp bể cá. Nếu là mèo, anh ấy dọn dẹp hộp vệ sinh. Và nếu là chim, anh ấy dọn dẹp lồng.

Trong Swift, khai báo của một function xác định kiểu dữ liệu được trả về. Vì function không thể trả về ba kiểu dữ liệu cùng một lúc - Turtle, CatBird - nên cách tốt nhất là trả về kiểu dữ liệu cha của cả ba, Animal.

func getClientPet() -> Animal {
    // returns the pet
}

let pet = getClientPet() // pet is of type Animal 

Thật không may, kiểu dữ liệu này quá rộng để Lou có thể sử dụng. Không biết kiểu động vật cụ thể, anh ấy có thể vô tình cố gắng dọn dẹp bể cá của chim, hộp vệ sinh của rùa hoặc lồng của mèo. Hãy xem xét các function hợp lệ sau với các tham số Turtle, CatBird.

func cleanTerrarium(turtle: Turtle) {
    print("Cleaning \(turtle.name)'s terrarium")
}

func cleanLitterBox(cat: Cat) {
    print("Cleaning the \(cat.boxSize) litter box")
}

func cleanCage(bird: Bird) {
    print("Removing the \(bird.featherColor) feathers at the bottom of the cage")
}

Lou cần có thể truy cập một phiên bản của pet thuộc một trong các lớp con của Animal. Bạn có thể sử dụng toán tử as? để thử hạ cấp giá trị xuống một kiểu cụ thể hơn và lưu trữ nó trong một hằng số mới. Thao tác này được gọi là ép kiểu có điều kiện (conditional cast), vì nó ép kiểu instance thành kiểu được chỉ định nếu có thể. Sử dụng cú pháp if-let để kiểm tra các điều kiện trước khi chuyển đổi kiểu:

let pets = allClientAnimals()

for pet in pets {
    if let turtle = pet as? Turtle {
        cleanTerrarium(turtle: turtle)
    } else if let cat = pet as? Cat {
        cleanLitterBox(cat: cat)
    } else if let bird = pet as? Bird {
        cleanCage(bird: bird)
    }
}

Bây giờ Lou có thể chắc chắn rằng anh ấy đang dọn dẹp bể cá của rùa, hộp vệ sinh của mèo và lồng chim.

Cũng có một dạng ép kiểu bắt buộc: as!. Phiên bản này sẽ buộc hạ cấp xuống kiểu được chỉ định. Nhưng nếu bạn chỉ định một kiểu không chính xác, nó sẽ làm crash chương trình của bạn, giống như khi bạn force-unwrap một optional.

Giả sử Laura có một con mèo cưng và cô ấy đến cửa hàng thú cưng để đón nó. Function fetchPet(for customer: String) có thể trả về kiểu Animal.

let laurasCat = fetchPet(for: "Laura")
// laurasCat is inferred as the `Animal` type

Khi bạn biết chắc chắn rằng đối tượng được trả về sẽ là một kiểu cụ thể hơn, bạn có thể sử dụng toán tử as! để ép kiểu giá trị ngay lập tức.

let laurasCat = fetchPet(for: "Laura") as! Cat
// laurasCat is inferred as the `Cat` type

Khi bạn bắt đầu xây dựng ứng dụng, bạn sẽ thấy rằng khi bạn làm việc với UIKit, API có thể trả về các đối tượng rất chung chung như UIViewController. Nhưng với tư cách là nhà phát triển ứng dụng của bạn, bạn biết kiểu cụ thể nên là gì. Ví dụ: nếu nhấn nút trên view của FirstViewController luôn hiển thị SecondViewController, bạn có thể buộc hạ cấp destination thành SecondViewController trong function prepare(for:sender:). Function này được gọi bất cứ khi nào bạn hiển thị một view controller mới bằng cách sử dụng storyboard segues.

class SecondViewController: UIViewController {
    var names: [String]?
}

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let secondVC = segue.destination as! SecondViewController
    secondVC.names = ["Peter", "Jamie", "Tricia"]
}

Chỉ sử dụng as! khi bạn chắc chắn rằng kiểu cụ thể là chính xác.

Any

Bạn đã học rằng mảng, theo mặc định, được đặt để xử lý một kiểu cụ thể, chẳng hạn như Int, String hoặc Animal. Các collection đồng nhất dễ làm việc hơn vì bạn biết kiểu của mọi instance trong đó.

Nhưng điều gì sẽ xảy ra nếu bạn muốn làm việc với các kiểu không cụ thể? Swift cung cấp hai kiểu đặc biệt: AnyAnyObject. Như tên cho thấy, Any có thể đại diện cho một instance của bất kỳ kiểu nào: string, double, function, hoặc bất cứ thứ gì. AnyObject có thể đại diện cho một instance của bất kỳ class nào trong Swift nhưng không phải là structure.

Dưới đây là ví dụ về một mảng có thể chứa bất kỳ kiểu instance nào, hoặc [Any]:

var items: [Any] = [5, "Bill", 6.7, Turtle()]

Vì mảng ở trên có thể bao gồm bất cứ thứ gì, nên không có cách nào để đảm bảo kiểu của bất kỳ mục nào. Ví dụ: nếu bạn sử dụng items[0] để truy cập mục đầu tiên trong mảng sau, nó sẽ trả về giá trị 5 với kiểu Any không cụ thể. Để tiếp tục và sử dụng giá trị của firstItemInt, bạn sẽ sử dụng toán tử as?:

var items: [Any] = [5, "Bill", 6.7, true]
if let firstItem = items[0] as? Int {
    print(firstItem + 4) // 9
}

Mặc dù Any có thể được sử dụng giống như bất kỳ kiểu nào khác, nhưng luôn tốt hơn nếu bạn cụ thể về các kiểu bạn mong đợi làm việc. Trước khi sử dụng Any hoặc AnyObject trong code của bạn, hãy đảm bảo rằng bạn cần hành vi và khả năng mà các kiểu đặc biệt này cung cấp.

Kiểm tra Kiểu (Type Inspection)

Đôi khi bạn cần kiểm tra kiểu của một biến hoặc hằng số trước khi sử dụng nó. Ví dụ, bạn có thể có một mảng chứa các đối tượng thuộc các kiểu khác nhau và bạn muốn thực hiện các hành động khác nhau tùy thuộc vào kiểu của mỗi đối tượng.

Swift cung cấp toán tử is để kiểm tra xem một instance có thuộc một kiểu cụ thể hay không. Toán tử này trả về true nếu instance thuộc kiểu được chỉ định và false nếu không.

func describeObject(object: Any) {
    if object is String {
        print("The object is a String.")
    } else if object is Int {
        print("The object is an Int.")
    } else {
        print("The object is of an unknown type.")
    }
}

Trong ví dụ trên, function describeObject nhận một tham số kiểu Any và sử dụng toán tử is để kiểm tra kiểu của đối tượng. Nếu đối tượng là String, function in ra "The object is a String." Nếu đối tượng là Int, function in ra "The object is an Int." Nếu không, function in ra "The object is of an unknown type."

Lưu ý: Kiểm tra kiểu và ép kiểu thường được sử dụng cùng nhau. Bạn có thể sử dụng toán tử is để kiểm tra kiểu của một đối tượng và sau đó sử dụng toán tử as? hoặc as! để hạ cấp đối tượng xuống kiểu cụ thể nếu cần.

Tóm tắt

Ép kiểu và kiểm tra kiểu là những kỹ thuật mạnh mẽ cho phép bạn làm việc với các kiểu dữ liệu khác nhau trong Swift. Bằng cách hiểu rõ các khái niệm này, bạn có thể viết code linh hoạt và an toàn hơn, xử lý các tình huống phức tạp liên quan đến kiểu dữ liệu một cách hiệu quả.