Optionals trong Swift: Xử lý dữ liệu "biến mất" một cách an toàn

Swift 3 Th09 2024

Bài viết dựa trên thông tin được trình bày trong sách Develop in Swift Fundamentals bài 3.2 Optionals.

Trong thế giới lập trình, ta thường gặp trường hợp một biến có thể có giá trị hoặc không. Ví dụ, khi lấy thông tin người dùng từ server, kết quả trả về có thể là một User hợp lệ hoặc không tìm thấy gì cả. Để xử lý tình huống "biến mất" này một cách an toàn và hiệu quả, Swift giới thiệu khái niệm Optionals. Hãy tưởng tượng Optionals như những chiếc hộp 🎁 có thể chứa một món quà bên trong, hoặc cũng có thể trống rỗng.

1. Nil: "Không có gì" trong Swift

Trước khi tìm hiểu về Optionals, ta cần làm quen với nil. Trong Swift, nil đại diện cho - sự vắng mặt của giá trị - không tồn tại giá trị. Nó không phải là 0 hay một chuỗi rỗng, mà đơn giản là "không có gì". Giống như việc bạn mở một chiếc hộp quà, nhưng bên trong lại chẳng có gì cả.

Ví dụ, ta có struct Book với property publicationYear (năm xuất bản). Nếu một cuốn sách chưa được xuất bản, publicationYear sẽ là nil, nghĩa là "chưa có năm xuất bản".

struct Book {
    var name: String
    var publicationYear: Int? // Optional Int
}

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

2. Khai báo Optionals: Dấu "?" huyền thoại

Để khai báo một biến Optional, ta thêm dấu "?" sau kiểu dữ liệu. Dấu "?" này báo hiệu cho compiler biết rằng biến này có thể chứa giá trị của kiểu dữ liệu đã khai báo hoặc là nil. Nó giống như việc dán nhãn "Có thể có quà" lên chiếc hộp.

var serverResponseCode: Int? = 404 // Optional Int với giá trị 404
var userAge: Int? = nil // Optional Int với giá trị nil

Lưu ý: Không thể khai báo Optional mà không chỉ định kiểu dữ liệu. Compiler cần biết kiểu dữ liệu để kiểm tra tính hợp lệ của code, giống như bạn cần biết chiếc hộp đựng quà gì để chọn đúng cách mở.

3. Làm việc với Optionals: An toàn là trên hết

Để truy cập giá trị của Optional, ta cần "unwrap" (mở hộp) nó. Hãy tưởng tượng việc unwrap giống như việc mở hộp quà để xem bên trong có gì. Swift cung cấp ba cách để unwrap Optionals, mỗi cách có mức độ an toàn khác nhau:

1. Force-unwrap

Sử dụng toán tử "!" để ép unwrap Optional. Cách này đơn giản nhưng nguy hiểm. Nếu Optional chứa nil, chương trình sẽ crash, giống như việc bạn cố gắng lấy quà từ chiếc hộp rỗng.

let unwrappedYear = publicationYear! // Crash nếu publicationYear là nil

2. Optional Binding

Sử dụng if let hoặc guard let để unwrap Optional một cách an toàn. Code bên trong block if let hoặc guard let chỉ được thực thi nếu Optional chứa giá trị. Giống như việc bạn kiểm tra xem hộp có quà không trước khi mở.

Ví dụ về if let

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

Ví dụ về guard let

func printBookPublicationYear(book: Book) {
    guard let unwrappedPublicationYear = book.publicationYear else {
        print("Cuốn sách không có ngày xuất bản chính thức.")
        return 
    }

    print("Cuốn sách được xuất bản năm \(unwrappedPublicationYear)")
}

Sự khác biệt giữa if letguard let:

  • if let: Thực thi một khối code nếu Optional có giá trị. Nó tập trung vào việc xử lý trường hợp Optional có giá trị.
  • guard let: Thực thi một khối code nếu Optional không có giá trị và thoát khỏi scope hiện tại (ví dụ: function). Nó tập trung vào việc xử lý trường hợp Optional không có giá trị.

Khi nào sử dụng if letguard let:

  • if let: Sử dụng khi bạn muốn thực thi một khối code chỉ khi Optional có giá trị, và không cần làm gì đặc biệt nếu Optional không có giá trị.
  • guard let: Sử dụng khi bạn muốn thoát khỏi scope hiện tại (ví dụ: function) nếu Optional không có giá trị, và chỉ tiếp tục thực thi code nếu Optional có giá trị.

Trong ví dụ cụ thể trên:

  • Sử dụng if let là phù hợp, vì ta chỉ muốn in ra năm xuất bản nếu nó tồn tại. Nếu publicationYearnil, ta chỉ cần in ra thông báo "Cuốn sách không có ngày xuất bản chính thức." và không cần làm gì thêm.
  • Sử dụng guard let cũng hợp lý, nếu function printBookPublicationYear có nhiều logic xử lý khác phụ thuộc vào publicationYear. Trong trường hợp này, ta muốn thoát khỏi function nếu publicationYearnil, và chỉ tiếp tục thực thi code nếu publicationYear có giá trị.

Tóm lại: Cả if letguard let đều là công cụ hữu ích để unwrap Optionals một cách an toàn. Việc lựa chọn sử dụng if let hay guard let phụ thuộc vào logic của code và mục đích của bạn.

3. 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).")
}

Trong ví dụ trên, chúng ta sử dụng toán tử "?." để truy cập property của nested Optionals. Nếu bất kỳ Optional nào trong chuỗi chứa nil, toàn bộ biểu thức sẽ trả về nil. Giống như việc bạn mở một hộp quà, bên trong có một hộp quà khác, và bạn muốn kiểm tra xem hộp quà cuối cùng có quà không.

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ị.

4. Function và Optionals: Linh hoạt và an toàn

Optionals mang đến sự linh hoạt và an toàn khi làm việc với function.

Ví dụ: Giả sử bạn đang xây dựng một ứng dụng mạng xã hội, nơi người dùng có thể đăng bài viết. Mỗi bài viết có thể có hoặc không có hình ảnh đính kèm. Ta có thể sử dụng Optionals để biểu diễn trường hợp này:

struct Post {
    let content: String
    let imageURL: String? // Optional String, vì hình ảnh có thể không tồn tại
}

func displayPost(post: Post) {
    print("Nội dung: \(post.content)")
    if let imageURL = post.imageURL {
        print("Hình ảnh: \(imageURL)") 
    } else {
        print("Bài viết không có hình ảnh.")
    }
}

Trong ví dụ trên, function displayPost nhận một Post làm tham số. Property imageURL là một Optional String, vì bài viết có thể không có hình ảnh. Ta sử dụng if let để kiểm tra xem imageURL có chứa giá trị hay không. Nếu có, ta in ra URL của hình ảnh. Ngược lại, ta in ra thông báo "Bài viết không có hình ảnh."

Ngoài ra, Swift cũng cung cấp Failable Initializer (init?) - một initializer có thể trả về nil nếu quá trình khởi tạo thất bại.

struct Toddler {
    var name: String
    var monthsOld: Int

    init?(name: String, monthsOld: Int) {
        if monthsOld < 12 || monthsOld > 36 {
            return nil // Trả về nil nếu tuổi không hợp lệ
        } else {
            self.name = name
            self.monthsOld = monthsOld
        }
    }
}

Trong ví dụ trên, init? chỉ tạo một Toddler nếu monthsOld nằm trong khoảng từ 12 đến 36. Nếu không, initializer sẽ trả về nil.

5. Implicitly Unwrapped Optionals trong UIKit: Khi bạn "chắc chắn" về sự tồn tại

Trong UIKit, ta thường gặp Implicitly Unwrapped Optionals khi làm việc với các outlet được kết nối từ Interface Builder.

Ví dụ: Giả sử bạn có một UILabel được kết nối với một outlet trong ViewController:

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

Dấu "!" sau kiểu dữ liệu UILabel cho biết myLabel là một Implicitly Unwrapped Optional.

Tại sao lại sử dụng Implicitly Unwrapped Optionals trong trường hợp này?

  • Khởi tạo: Khi ViewController được khởi tạo, outlet myLabel ban đầu chưa được kết nối với UILabel trên Storyboard. Do đó, myLabel có giá trị là nil lúc này.
  • Kết nối sau khởi tạo: Sau khi ViewController được khởi tạo, UIKit sẽ kết nối outlet myLabel với UILabel tương ứng trên Storyboard. Lúc này, myLabel sẽ có giá trị.

Implicitly Unwrapped Optionals cho phép ta sử dụng myLabel như một non-optional sau khi nó được kết nối, mà không cần phải unwrap bằng if let hay guard let. Tuy nhiên, nếu ta cố gắng truy cập myLabel trước khi nó được kết nối (tức là khi nó vẫn là nil), chương trình sẽ crash.

Nên sử dụng Implicitly Unwrapped Optionals một cách cẩn thận! Chỉ sử dụng khi bạn chắc chắn rằng Optional sẽ có giá trị trước khi nó được sử dụng. Việc lạm dụng Implicitly Unwrapped Optionals có thể dẫn đến crash khó debug.

Tóm lại:

  • Implicitly Unwrapped Optionals tự động unwrap khi được sử dụng.
  • Sử dụng trong UIKit khi làm việc với outlet được kết nối từ Interface Builder.
  • Chỉ sử dụng khi bạn chắc chắn Optional sẽ có giá trị trước khi được sử dụng.

6. Kết luận

Optionals là một tính năng quan trọng giúp Swift trở thành ngôn ngữ lập trình an toàn và dễ đọc. Nắm vững Optionals là bước đầu tiên để bạn trở thành một lập trình viên Swift chuyên nghiệp. Hãy tìm hiểu thêm về Optionals trong Swift Language Guide để khám phá thêm nhiều tính năng hữu ích khác.

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.