Bài 3.2 Optionals - Develop in Swift - Fundamentals
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 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 name
và publicationYear
.
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ải là nil
:
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ải là nil
, 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
, middleName
và lastName
, nhưng cho phép middleName
là nil
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.