Bài 3.2 Optionals - Develop in Swift - Fundamentals

Swift 3 Th09 2024

Giới thiệu

Một trong những điểm mạnh nhất của Swift là khả năng đọc code và nhanh chóng hiểu dữ liệu. Khi một function có thể trả về dữ liệu hoặc không, Swift buộc bạn phải xử lý đúng cả hai trường hợp.

Swift sử dụng một cú pháp độc đáo, được gọi là Optionals, để xử lý kiểu trường hợp này. Trong bài học này, bạn sẽ học cách sử dụng Optionals để xử lý chính xác các tình huống khi dữ liệu có thể tồn tại hoặc không.

Nil

Optionals hữu ích trong các tình huống khi một giá trị có thể có hoặc không. Một Optional đại diện cho hai khả năng: Hoặc một giá trị và bạn có thể sử dụng nó, hoặc không có giá trị nào cả.

Hãy tưởng tượng bạn đang xây dựng một ứng dụng cho hiệu sách liệt kê các cuốn sách để bán. Bạn có một model object Book với các property namepublicationYear.

struct Book {
    var name: String
    var publicationYear: Int
}

let firstDickens = Book(name: "A Christmas Carol",
    publicationYear: 1843)
let secondDickens = Book(name: "David Copperfield",
    publicationYear: 1849)
let thirdDickens = Book(name: "A Tale of Two Cities",
    publicationYear: 1859)

let books = [firstDickens, secondDickens, thirdDickens]

Đến giờ, mọi thứ đều ổn. Giờ hãy tưởng tượng bạn đang xây dựng một màn hình hiển thị danh sách các cuốn sách đã được công bố nhưng chưa xuất bản.

Làm thế nào để bạn khởi tạo những cuốn sách đó mà không có ngày xuất bản? Bạn gán gì cho publicationYear?

let unannouncedBook = Book(name: "Rebels and Lions", 
    publicationYear: 0)

Không, vì 0 không chính xác, bởi vì điều đó có nghĩa là cuốn sách đã hơn 2.000 năm tuổi.

let unannouncedBook = Book(name: "Rebels and Lions",
    publicationYear: 2024)

Năm nay hoặc thậm chí năm sau cũng không chính xác, bởi vì nó có thể được phát hành sau hai năm nữa. Không có ngày phát hành đã biết.

nil đại diện cho sự vắng mặt của một giá trị, hoặc không có gì. Vì chưa có publicationYear, publicationYear nên là nil.

let unannouncedBook = Book(name: "Rebels and Lions",
    publicationYear: nil)

Trông ổn hơn rồi đấy, nhưng compiler lại báo lỗi. Tất cả các instance property phải được đặt trong quá trình khởi tạo, và bạn không thể truyền nil cho tham số publicationYear vì nó mong đợi một giá trị Int.

Optionals giải quyết vấn đề này bằng cách cung cấp một lớp bao bọc xung quanh một giá trị có thể tồn tại. Bạn có thể nghĩ về một Optional như một chiếc hộp mà khi mở ra, sẽ chứa một instance của kiểu dự kiến hoặc không có gì cả (nil).

Xác định kiểu của Optional

Lưu ý rằng bạn không thể tạo Optional mà không chỉ định kiểu. Hãy xem điều gì sẽ xảy ra nếu bạn cố gắng để Swift suy luận kiểu.

Trong ví dụ này, type inference sẽ giả định biến của bạn là non-optional:

var serverResponseCode = 404 // Int, not Int?

Trong ví dụ này, type inference không có bất kỳ thông tin nào để xác định kiểu dữ liệu nếu dữ liệu không phảinil:

var serverResponseCode = nil // Error, no type specified when not `nil` 

Vì những lý do này, trong hầu hết các trường hợp, bạn sẽ cần sử dụng type annotation để xác định kiểu khi tạo một biến hoặc hằng số Optional. Hãy xem các cách tiếp cận chính xác sau đây đối với Optional Int?:

var serverResponseCode: Int? = 404 // Set to 404, but could be `nil` later

var serverResponseCode: Int? = nil // Set to `nil`, but could hold an `Int` later

Làm việc với giá trị Optional

Làm thế nào để bạn xác định liệu một Optional có chứa giá trị hay không? Làm cách nào để truy cập giá trị?

Bạn có thể bắt đầu bằng cách so sánh Optional với nil bằng câu lệnh if. Nếu giá trị không phảinil, bạn có thể unwrap giá trị bằng toán tử force-unwrap, !.

if publicationYear != nil {
    let actualYear = publicationYear!
    print(actualYear)
}

Nếu bạn bỏ qua bước so sánh Optional với nil và bạn force-unwrap một Optional không chứa giá trị, code sẽ tạo ra lỗi và crash khi bạn cố gắng chạy nó.

let unwrappedYear = publicationYear!
print(unwrappedYear) // runtime error

Có vẻ như so sánh Optional với nil trước khi cố gắng sử dụng giá trị chứa bên trong là một cách thực hành tốt, nhưng nó cũng có vẻ dư thừa. Như bạn đã biết, tính an toàn và rõ ràng là các mục tiêu thiết kế chính của Swift, vì vậy cú pháp súc tích được cung cấp để sử dụng an toàn giá trị của Optional nếu nó có, và tránh lỗi nếu nó không có.

Optional binding unwrap Optional và nếu nó chứa giá trị, gán giá trị cho hằng số với kiểu non-optional, giúp nó an toàn khi làm việc. Cách tiếp cận này loại bỏ nhu cầu tiếp tục làm việc với sự mơ hồ về việc bạn đang làm việc với giá trị hay với nil.

Cú pháp cho optional binding như sau:

if let constantName = someOptional {
    // constantName đã được unwrap an toàn để sử dụng trong dấu ngoặc nhọn
}

Nếu someOptional có giá trị, giá trị được gán cho constantName và chỉ khả dụng trong dấu ngoặc nhọn.

Hãy xem cách optional binding hoạt động trên ví dụ Book trước đó:

if let unwrappedPublicationYear = book.publicationYear {
    print("Cuốn sách được xuất bản năm \(unwrappedPublicationYear)")
}

Giống như các câu lệnh if khác, bạn có thể thêm mệnh đề else.

if let unwrappedPublicationYear = book.publicationYear {
    print("Cuốn sách được xuất bản năm \(unwrappedPublicationYear)")
} else {
    print("Cuốn sách không có ngày xuất bản chính thức.")
}

Function và Optional

Swift đi kèm với nhiều function trả về giá trị Optional.

Hãy xem xét ví dụ bạn có một String có giá trị được đặt thành "123". Bạn có thể thấy ở đây rằng string có thể được chuyển đổi thành Int.

let string = "123"
let possibleNumber = Int(string)

Nhưng điều gì sẽ xảy ra nếu chuỗi không thể chuyển đổi?

let string = "Edwina"
let possibleNumber = Int(string)

Swift suy luận possibleNumber là kiểu Int? bởi vì initializer cho Int nhận String làm tham số có thể hoặc không thể chuyển đổi thành công String thành Int. Nếu string có thể được chuyển đổi thành Int, possibleNumber sẽ giữ giá trị đó. Nếu không, possibleNumber sẽ là nil.

Nếu bạn muốn viết một function nhận Optional làm argument, chỉ cần cập nhật kiểu trong danh sách tham số. Hãy xem xét function print này nhận firstName, middleNamelastName, nhưng cho phép middleNamenil vì không phải ai cũng sử dụng tên đệm.

func printFullName(firstName: String, middleName: String?,
    lastName: String)

Điều tương tự cũng đúng đối với function trả về Optional: Chỉ cần cập nhật kiểu trả về. Ví dụ: URL trang web trả về văn bản từ trang đó. Văn bản trả về là Optional vì URL có thể không hoạt động hoặc có thể không trả về bất kỳ văn bản nào.

func textFromURL(url: URL) -> String?

Failable Initializer

Bất kỳ initializer nào có thể trả về nil được gọi là failable initializer. Trước đó trong bài học này, bạn đã thấy cách initializer Int cố gắng tạo Int từ String và trả về nil nếu nó không thể chuyển đổi String.

Để kiểm soát và an toàn hơn, bạn có thể muốn tạo failable initializer của riêng mình và xác định logic để trả về một instance, hoặc nil.

Hãy xem xét định nghĩa sau cho Toddler:

struct Toddler {
    var name: String
    var monthsOld: Int
}

Trong ví dụ này, mọi Toddler phải được đặt tên, cũng như tuổi tính bằng tháng. Tuy nhiên, bạn có thể không muốn tạo một instance của Toddler nếu đứa trẻ nhỏ hơn 12 tháng hoặc lớn hơn 36 tháng. Để cung cấp tính linh hoạt này, bạn có thể sử dụng init? để xác định failable initializer. Dấu hỏi chấm (?) cho Swift biết rằng initializer này có thể trả về nil và nó sẽ trả về một instance kiểu Toddler?.

Trong thân của initializer, bạn có thể kiểm tra xem tuổi được cung cấp có nhỏ hơn 12 hoặc lớn hơn 36 hay không. Nếu một trong hai là đúng, initializer sẽ trả về nil thay vì gán giá trị cho monthsOld:

struct Toddler {
    var name: String
    var monthsOld: Int

    init?(name: String, monthsOld: Int) {
        if monthsOld < 12 || monthsOld > 36 {
            return nil
        } else {
            self.name = name
            self.monthsOld = monthsOld
        }
    }
}

Khi bạn sử dụng failable initializer để tạo instance Toddler, một Optional sẽ luôn được trả về. Để unwrap an toàn giá trị trước khi sử dụng nó, bạn có thể sử dụng optional binding:

let toddler = Toddler(name: "Evania", monthsOld: 14)

if let myToddler = toddler {
    print("\(myToddler.name) is \(myToddler.monthsOld) months old")
} else {
    print("Tuổi bạn chỉ định cho toddler không nằm trong khoảng từ 1 đến 3 tuổi")
}

Optional Chaining

Một giá trị Optional cũng có thể có các thuộc tính Optional, mà bạn có thể nghĩ là một chiếc hộp trong một chiếc hộp. Những thứ này được gọi là nested Optionals.

Trong ví dụ sau, lưu ý rằng mọi Person đều có age và có thể có residence. Một Residence có thể có address, và không phải mọi Address đều có apartmentNumber.

struct Person {
    var age: Int
    var residence: Residence?
}

struct Residence {
    var address: Address?
}

struct Address {
    var buildingNumber: String
    var streetName: String
    var apartmentNumber: String?
}

Unwrapping nested Optionals có thể yêu cầu rất nhiều code. Trong ví dụ sau, bạn đang kiểm tra địa chỉ của một cá nhân để tìm hiểu xem họ có sống trong căn hộ hay không. Để làm điều này cho một object Person nhất định, bạn phải unwrap Optional residence, Optional address và Optional apartmentNumber. Sử dụng cú pháp if-let, bạn sẽ phải thực hiện khá nhiều bước unwrap có điều kiện:

if let theResidence = person.residence {
    if let theAddress = theResidence.address {
        if let theApartmentNumber = theAddress.apartmentNumber {
            print("Họ sống ở căn hộ số \(theApartmentNumber).")
        }
    }
}

Nhưng có một cách tốt hơn để làm điều này. Thay vì gán tên cho mọi Optional, bạn có thể unwrap có điều kiện từng thuộc tính bằng cách sử dụng cấu trúc được gọi là optional chaining. Nếu người đó có nơi cư trú, địa chỉ có số căn hộ và nếu số căn hộ đó có thể được chuyển đổi thành số nguyên, thì bạn có thể tham chiếu đến số đó bằng theApartmentNumber, như được thấy ở đây:

if let theApartmentNumber = person.residence?.address?.apartmentNumber {
    print("Họ sống ở căn hộ số \(theApartmentNumber).")
}

Khi xâu chuỗi các Optional, một dấu ? xuất hiện trước mỗi Optional trong chuỗi.

Nếu giá trị nil phá vỡ chuỗi tại bất kỳ điểm nào, câu lệnh if let sẽ thất bại. Do đó, không có giá trị nào được gán cho hằng số và code bên trong dấu ngoặc nhọn không bao giờ được thực thi. Nếu không có giá trị nào là nil, code bên trong dấu ngoặc nhọn sẽ được thực thi và hằng số có giá trị.

Implicitly Unwrapped Optionals

Một object không thể được khởi tạo cho đến khi tất cả các non-optional property của nó được gán giá trị. Nhưng trong một số trường hợp, đặc biệt là với phát triển iOS, một số thuộc tính là nil trong một thời gian ngắn cho đến khi giá trị có thể được chỉ định sau khi khởi tạo. Ví dụ: bạn đã sử dụng Interface Builder để tạo outlet để bạn có thể truy cập một phần cụ thể của giao diện trong code.

class ViewController: UIViewController {
    @IBOutlet var label: UILabel!
}

Nếu bạn là nhà phát triển của lớp này, bạn sẽ biết rằng bất cứ khi nào ViewController được tạo và trình bày cho người dùng, sẽ luôn có label trên màn hình, vì bạn đã thêm nó vào Storyboard. Nhưng trong phát triển iOS, các thành phần Storyboard không được kết nối với các outlet tương ứng của chúng cho đến sau khi quá trình khởi tạo diễn ra. Do đó, label phải được phép là nil trong một khoảng thời gian ngắn.

Sử dụng Optional thông thường, UILabel?, cho kiểu thì sao? Standard Optional sẽ yêu cầu cú pháp if-let để liên tục unwrap giá trị, cung cấp cơ chế an toàn cho dữ liệu có thể không tồn tại. Nhưng bạn biết rằng label sẽ có giá trị sau khi Storyboard kết nối các outlet, vì vậy việc unwrap Optional mà không thực sự là "Optional" có vẻ rườm rà.

Để giải quyết vấn đề này, Swift cung cấp cú pháp cho implicitly unwrapped Optional, sử dụng dấu chấm than ! thay vì dấu hỏi chấm ?. Đúng như tên gọi, kiểu Optional này tự động unwrap bất cứ khi nào nó được sử dụng trong code. Điều này cho phép bạn sử dụng label như thể nó không phải là Optional, đồng thời cho phép ViewController được khởi tạo mà không cần nó.

Implicitly unwrapped Optional chỉ nên được sử dụng trong một trường hợp đặc biệt: khi bạn cần khởi tạo một object mà không cần cung cấp giá trị, nhưng bạn biết rằng bạn sẽ gán giá trị cho object trước khi bất kỳ code nào khác cố gắng truy cập nó. Có vẻ tiện lợi khi lạm dụng implicitly unwrapped Optional để bạn không phải sử dụng cú pháp if let, nhưng bằng cách đó, bạn sẽ loại bỏ một tính năng an toàn quan trọng khỏi ngôn ngữ. Do đó, nhiều nhà phát triển Swift coi việc có quá nhiều dấu ! trong code là một dấu hiệu cảnh báo. Nếu bạn cố gắng truy cập giá trị của implicitly unwrapped Optional mà giá trị là nil, chương trình của bạn sẽ crash.

Tags

Tony Phạm

Là một người thích vọc vạch và tò mò với tất cả các lĩnh vực từ khoa học tự nhiên, lập trình, thiết kế đến ... triết học. Luôn mong muốn chia sẻ những điều thú vị mà bản thân khám phá được.