Generics trong Swift
Bài đăng này đã không được cập nhật trong 4 năm
Generics là một trong những tính năng mạnh mẽ nhất trong Swift programming language. Nhưng tuy nhiên sẽ hơi khó hiểu lúc mới bắt đầu. Trong bài viết này chúng ta sẽ xem cách mà generics hoạt động trong Swift, và những điều thú vị bạn có thể làm với nó.
Đây sẽ là những điều mà bạn sẽ hiểu sau khi đọc bài viết này:
- Generics dùng để giải quyết vấn đề gì?
- Placeholder types và generic functions
- Generic type và sự ràng buộc với protocols
- Làm việc với associated types
- Kết hợp protocols và generics
Làm sao để sử dụng Generics trong Swift
Bạn cũng biết Swift có một hệ thống Strong type. Một khi bạn declared một variable là một String thì bạn không thể gán integer vào nó. Kiểu như thế này:
var text:String = "Hello world!"
text = 5
// Output: error: cannot assign value of type 'Int' to type 'String'
Một String luôn luôn là String. Bạn không thể gán một giá trị kiểu Int
vào một biến kiểu String
/
Sự nghiêm ngặt này nhìn chung là một điều tốt, vì nó giúp bạn tránh code lỗi. Nhưng nếu bạn muôn làm việc với một kiểu data mà không muốn sự nghiêm ngặt này?
Hãy xem ví dụ sau. Bạn tạo một function thêm một số vào một số khác. Như thế này:
func addition(a: Int, b: Int) -> Int
{
return a + b
}
let result = addition(a: 42, b: 99)
print(result)
// Output: 141
Function lấy 2 parameter a
và b
có kiểu Int
, và return một giá trị của kiểu Int
. Toán tử +
sẽ cộng số và return kết quả.
Còn nếu bạn muốn mở rộng function của bạn để nó cũng có thể thêm một kiểu số khác như Float
và Double
thì sao? Có phải bạn sẽ viết một function mới kiểu:
func addition(a: Double, b: Double) -> Double
{
return a + b
}
Nhìn có vẻ code của bạn đã bị lặp. Nhìn chung điều này khá là tệ vì theo DRY principle thì Don’t repeat yourself.
Vậy để có thể tái sử dụng lại code của bạn mà không cần chỉ định một type cụ thể như addition(a:b:)
thì Generics
được sinh ra.
Làm việc với Generic Functions và Placeholder Types
Với generics bạn có thể viết một cách clear, linh hoạt và có thể tái sử dụng code. Bạn có thể tránh việc viết code trùng lặp.
Hãy lấy lại function cũ addition(a:b)
và sửa nó thành generic function. Như thế này:
func addition<T: Numeric>(a: T, b: T) -> T
{
return a + b
}
Cú pháp <T: Numeeric>
thêm một ràng buộc type vào placeholder. Nó định nghĩ rằng T
cần conform theo protocol Numeric
. Đây là một protocol được tích hợp sẵn trong Swifr cho bất kỳ giá trị số nào như Int
, Double
.
Nói cách khác, bạn không thể sử dụng function addition(a:b:)
để add 2 đối tượng UIViewController
hoặc UILabel
. Nó không có ý nghĩa. Bạn chỉ có thể sử dụng giá trị conform theo Numeric
protocol.
Hãy thử một ví dụ khác. Đây là một generic function có thể tìm index của một value trong array:
func findIndex<T>(of foundItem: T, in items: [T]) -> Int?
{
for (index, item) in items.enumerated()
{
if item == foundItem {
return index
}
}
return nil
}
Function phía trên dùng parameter foundItem
để tìm đến item
trong array bằng cách sử dụng vòng lặp. Khi nó được tìm thấy, nó return index
của item
tìm được. Function return nil
nếu nó không thể tìm thấy item, nên return type của findIndex(of:in:)
sẽ là Int?
Placeholder type T
được sử dụng trong function declaration. Nó nói với Swift rằng function này có thể tìm bất cứ item trong bất cứ array, với điều kiện foundItem
và items trong array là cùng type. Nghĩa là bạn muốn tìm giá trị T
trong mảng T
.
Còn đây sẽ là cách sử dụng function:
let names = ["Ford", "Arthur", "Trillian", "Zaphod", "Deep Thought"]
if let result = findIndex(of: "Zaphod", in: names) {
print(result)
// Output: 3
}
Nhưng có vể function trên không thể comple. Chúng ta cần một ràng buộc type khác trên T
.
Vì chúng ta sử dụng toán tử ==
trong function để xem 2 items có bằng nhau hay không, vậy nên T
cần phải conform theo protocol Equatable
. Nếu không bạn k thể sử dụng ==
operator. Như thế này:
findIndex<T: Equatable>(of foundItem: T, in items: [T]) -> Int?
Giống như tên của nó - Equatable protocol, nó là protocol declares toán tử ==
. Toán tử ==
được dùng để quyết định xem 2 giá trị có bằng nhau hay không.
Swfit cung cấp một vài protocol cơ bản:
Equatable
để so sánh bằng hoặc k bằng của 2 giá trị.Comparable
để so sánh value, giống nhưa > b
Hashable
cho các giá trị có thể được "hased", là một đại diện integer duy nhất của giá trị đó (thường được sử dụng cho các dictionary keys)CustomStringConvertible
cho các giá trị có thể được biểu thị dưới dạng String, một protocol hữu ích để nhanh chóng chuyển các đối tượng tùy chỉnh thành String có thể print được.Numeric
vàSignedNumeric
cho các giá trị là numeric, như42
,3.1415
Strideable
cho các giá trị có thể offset và đo lường, như sequences, steps và ranges
Và tất nhiên bạn cũng có thể tự định nghĩa protocol của chính bạn để generic placeholder có thể conform theo. Tiếp theo chúng ta sẽ thảo luận sâu hơn về protocol và generic.
Kết hợp Generics, Protocols và Associated Types.
Vậy chúng ta đã xem xét:
- Generic function sử dụng placeholder value để chỉ định input và output của function
- protocol thì ràng buộc những điều đó
Bạn đã nghe về protocol trước đây đúng chứ? Một protocol chỉ định các function mà một class khi conform theo sẽ phải áp dụng nó. Khi nó thuông qua các chứ năng được yêu cầu thì class đó được cho là comform theo protocol đó.
Thử xem ví dụ. Tưởng tượng bạn có một nhà hàng bán một số sản phẩm thức ăn. Một khách hàng tới nhà hàng của bạn, và muốn ăn gì đó. Anh ta k cần quan tâm chính xác cái anh ta ăn là gì. Miễn là nó có thể ăn được.
Khách hàng định nghĩa một protocol:
protocol Edible {
func eat()
}
Bất kỳ class nào muốn conform theo Edible
cần phải hiện thược eat()
funciton.
class Apple: Edible
{
func eat() {
print("Omnomnom!")
}
Protocol giúp bạn viết code một cách linh hoạt và có thể tái sử dụng. Nó cũng giúp bạn kết nối code một cách không chặt chẽ. Khách hàng không cần biết chính xác phần hiện thực của cái gì mà anh ta ăn, chỉ cần biết nó có function eat()
. Anh ta có thể ăn bất cứ gì, miễn là Edible
.
Nhưng điều này thì có liên quan gì đến genenric?
Hãy bắt đầu với một tình huống giả định khác. Bạn đang đến một cửa hàng bách hóa, để mua một tủ sách. Và bạn có hai yêu cầu đối với tủ sách đó:
- Bạn không nhất thiết phải đặt sách vào tủ sách
- Nó thậm chí không cần phải là một tủ sách, nó cũng có thể là một hộp lưu trữ, một tủ đựng đồ, một tủ quần áo hoặc một tủ quần áo
- Hmm… bạn chỉ muốn “thứ gì đó” mà bạn có thể đặt “vật phẩm” vào và lấy vật phẩm ra
Vậy chúng ta có một Storage
protocol:
protocol Storage
{
func store(item: Book)
func retrieve(index: Int) -> Book
}
Protocol Storage
khai báo hai function, một để lưu trữ sách và một để truy xuất sách, theo chỉ mục của nó. Hãy giả sử rằng Sách là một Struct đơn giản với tên sách và tác giả.
struct Book {
var title = ""
var author = ""
}
Bất cứ class nào cũng có thể tuân theo Storage
protocol để lưu sách và truy xuất sách. Giống như Bookcase
và Booktrunk
class sau đây:
class Bookcase: Storage
{
var books = [Book]()
func store(item: Book) {
books.append(item)
}
func retrieve(index: Int) {
return books[index]
}
}
Class Bookcasee
trên lưu trữ sách trong mảng sách. Nó thông qua các chức năng từ protocol Storage
để lưu trữ và truy xuất sách. Bạn CHỈ có thể sử dụng Bookcase
để lưu trữ các đối tượng Book
.
Tuy nhiên bạn lại không chỉ muốn lưu Book
, bạn muốn lưu bất cứ thứ gì trong bất cứ storage. Từ đó generic được sử dụng vào.
Chúng ta thay đổi một ít trong code. Đầu tiên ta cần thêm một associated type
trong Storage
. Bạn có thể định nghĩ một generic type trong protocol bằng cách sử dụng associated type
. Nó giống như placeholder type mà chúng ta đã xem xét qua. Vậy nên:
protocol Storage
{
associatedtype Item
func store(item: Item)
func retrieve(index: Int) -> Item
}
Ở đây bạn đã thay đổi:
- Thêm
Item
associated type vớiassociatedtype
keyword. - Các hàm
store(item:)
vàretrieve(index:)
bây giờ có thể sử dụng associated typeItem
Thấy chứ, nó giống với placeholder type. Thay vì chỉ sử dụng Book
, class conform theo Storage
protocol bây giờ có thể lưu bất cứ kiểu này của Item
. Bới vì chúng ta làm việc với protocol, nên class có thể tự quyết định cách để lưu item đó.
Hãy nghĩ rằng associated type giống như liên kết một generic type với một protocol mà k cần định nghĩa type. Bản thân generic chưa được định nghĩa vì nó phụ thuộc vào class mà tuân theo protocol. Nó được hiện thực chi tiết
Bây giờ chúng ta hiện thực nột Storage
protocol trong Trunk
class. Class Trunk
này có thể lưu bất cứ item, không chỉ một book.
class Trunk<Item>: Storage
{
var items:[Item] = [Item]()
func store(item: Item) {
items.append(item)
}
func retrieve(index: Int) -> Item {
return items[index]
}
}
Hãy xem cách định nghĩa class Trunk
đính kèm <Item>
. Nó là một placeholder, và nó sử dụng trong toàn bộ class. Trunk
class có một array đơn giản có thể lưu và truy xuất items.
Chúng ta tạo một trunk
để lưu book:
let bookTrunk = Trunk<Book>()
bookTrunk.store(item: Book(title: "1984", author: "George Orwell"))
bookTrunk.store(item: Book(title: "Brave New World", author: "Aldous Huxley"))
print(bookTrunk.retrieve(index: 1).title)
// Output: Brave New World
Trong dòng đầu tiên của code, kiểu Book
được sử dụng khi khao báo kiểu của bookTrunk
. Lúc này Trunk
class sử dụng Book
struct thay vì Item
placeholder.
Như vậy, chắc chắn code sẽ trở nên linh hoạt hơn. Chúng ta định nghĩa một Shoe
class với size
và brand
. Chúng ta có thể lưu nó trong trunk được k? Tất nhiên là có.
let shoeTrunk = Trunk<Shoe>()
shoeTrunk.store(item: Shoe(size: 42, brand: "Nike"))
shoeTrunk.store(item: Shoe(size: 99, brand: "Adidas"))
print(shoeTrunk.retrieve(index: 0).brand)
// Output: Nike
Và bây giờ chúng ta có thể lưu mọi thứ.
Vậy nói chung:
- Protocol
Storage
định nghĩa một associated type. Type này phải được quyết định bởi class tuân theoStorage
protocol. Trunk
class sử dụng generic placeholder để hiện thực function.
=> Associated type và generic placeholder được cụ thể hoá khi chúng ta xác định được Trunk
sẽ là Book
. Điều này cho biết các type cụ thể mà code sẽ sử dụng. Khi biết phát triển chúng ta có thể tự xác định các type này một cách linh hoạt.
Generic Storage
protocol chỉ xác định khi class nào tuân theo nó sẽ cần phải bao gồm một function để lưu và một function để truy xuất. Nó k chỉ rõ item này được lưu hay truy xuất như thế nào, cũng như type của item đó. Kết quả chúng ta có thể tạo bất kỳ storage nào có thể lưu bất kỳ item.
Từ Swift 5.1 chúng ta có cách tiếp cận khác với để làm việc generic: opaque types. some
keyword giúp bạn ẩn
đi type cụ thể mà property hay function trả về. Kiểu cụ thể được quyết định bới việc hiện thực chính nó. Vậy nên opaque type thỉnh thoảng được gọi là reverse generics.
All rights reserved