Optionals trong Swift: Xử lý dữ liệu "biến mất" một cách an toàn
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 let
và guard 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 let
và guard 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ếupublicationYear
lànil
, 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 functionprintBookPublicationYear
có nhiều logic xử lý khác phụ thuộc vàopublicationYear
. Trong trường hợp này, ta muốn thoát khỏi function nếupublicationYear
lànil
, và chỉ tiếp tục thực thi code nếupublicationYear
có giá trị.
Tóm lại: Cả if let
và guard 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, outletmyLabel
ban đầu chưa được kết nối vớiUILabel
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 outletmyLabel
vớiUILabel
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.