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:
1 2 3 4 | <span class="token keyword">var</span> text<span class="token punctuation">:</span><span class="token builtin">String</span> <span class="token operator">=</span> <span class="token string">"Hello world!"</span> text <span class="token operator">=</span> <span class="token number">5</span> <span class="token comment">// Output: error: cannot assign value of type 'Int' to type 'String'</span> |
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:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">func</span> <span class="token function">addition</span><span class="token punctuation">(</span>a<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">,</span> b<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Int</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token function">addition</span><span class="token punctuation">(</span>a<span class="token punctuation">:</span> <span class="token number">42</span><span class="token punctuation">,</span> b<span class="token punctuation">:</span> <span class="token number">99</span><span class="token punctuation">)</span> <span class="token function">print</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span> <span class="token comment">// Output: 141</span> |
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:
1 2 3 4 5 | <span class="token keyword">func</span> <span class="token function">addition</span><span class="token punctuation">(</span>a<span class="token punctuation">:</span> <span class="token builtin">Double</span><span class="token punctuation">,</span> b<span class="token punctuation">:</span> <span class="token builtin">Double</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Double</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> |
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:
1 2 3 4 5 | <span class="token keyword">func</span> addition<span class="token operator"><</span>T<span class="token punctuation">:</span> <span class="token builtin">Numeric</span><span class="token operator">></span><span class="token punctuation">(</span>a<span class="token punctuation">:</span> T<span class="token punctuation">,</span> b<span class="token punctuation">:</span> T<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> T <span class="token punctuation">{</span> <span class="token keyword">return</span> a <span class="token operator">+</span> b <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">func</span> findIndex<span class="token operator"><</span>T<span class="token operator">></span><span class="token punctuation">(</span>of foundItem<span class="token punctuation">:</span> T<span class="token punctuation">,</span> <span class="token keyword">in</span> items<span class="token punctuation">:</span> <span class="token punctuation">[</span>T<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Int</span><span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token keyword">for</span> <span class="token punctuation">(</span>index<span class="token punctuation">,</span> item<span class="token punctuation">)</span> <span class="token keyword">in</span> items<span class="token punctuation">.</span><span class="token function">enumerated</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> item <span class="token operator">==</span> foundItem <span class="token punctuation">{</span> <span class="token keyword">return</span> index <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token constant">nil</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 7 | <span class="token keyword">let</span> names <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"Ford"</span><span class="token punctuation">,</span> <span class="token string">"Arthur"</span><span class="token punctuation">,</span> <span class="token string">"Trillian"</span><span class="token punctuation">,</span> <span class="token string">"Zaphod"</span><span class="token punctuation">,</span> <span class="token string">"Deep Thought"</span><span class="token punctuation">]</span> <span class="token keyword">if</span> <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token function">findIndex</span><span class="token punctuation">(</span>of<span class="token punctuation">:</span> <span class="token string">"Zaphod"</span><span class="token punctuation">,</span> <span class="token keyword">in</span><span class="token punctuation">:</span> names<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">print</span><span class="token punctuation">(</span>result<span class="token punctuation">)</span> <span class="token comment">// Output: 3</span> <span class="token punctuation">}</span> |
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:
1 2 | findIndex<span class="token operator"><</span>T<span class="token punctuation">:</span> <span class="token builtin">Equatable</span><span class="token operator">></span><span class="token punctuation">(</span>of foundItem<span class="token punctuation">:</span> T<span class="token punctuation">,</span> <span class="token keyword">in</span> items<span class="token punctuation">:</span> <span class="token punctuation">[</span>T<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Int</span><span class="token operator">?</span> |
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:
1 2 3 4 | <span class="token keyword">protocol</span> <span class="token builtin">Edible</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">eat</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Bất kỳ class nào muốn conform theo Edible
cần phải hiện thược eat()
funciton.
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">Apple</span><span class="token punctuation">:</span> <span class="token builtin">Edible</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">eat</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"Omnomnom!"</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 | <span class="token keyword">protocol</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">func</span> <span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token builtin">Book</span><span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>index<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Book</span> <span class="token punctuation">}</span> |
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ả.
1 2 3 4 5 | <span class="token keyword">struct</span> <span class="token builtin">Book</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> title <span class="token operator">=</span> <span class="token string">""</span> <span class="token keyword">var</span> author <span class="token operator">=</span> <span class="token string">""</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Bookcase</span><span class="token punctuation">:</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> books <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">Book</span><span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token builtin">Book</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> books<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>index<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> books<span class="token punctuation">[</span>index<span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 7 | <span class="token keyword">protocol</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> associatedtype <span class="token builtin">Item</span> <span class="token keyword">func</span> <span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token builtin">Item</span><span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>index<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Item</span> <span class="token punctuation">}</span> |
Ở đâ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.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Trunk</span><span class="token operator"><</span><span class="token builtin">Item</span><span class="token operator">></span><span class="token punctuation">:</span> <span class="token builtin">Storage</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> items<span class="token punctuation">:</span><span class="token punctuation">[</span><span class="token builtin">Item</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">Item</span><span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token builtin">Item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> items<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>index<span class="token punctuation">:</span> <span class="token builtin">Int</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">Item</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> items<span class="token punctuation">[</span>index<span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
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:
1 2 3 4 5 6 | <span class="token keyword">let</span> bookTrunk <span class="token operator">=</span> <span class="token builtin">Trunk</span><span class="token operator"><</span><span class="token builtin">Book</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span> bookTrunk<span class="token punctuation">.</span><span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token function">Book</span><span class="token punctuation">(</span>title<span class="token punctuation">:</span> <span class="token string">"1984"</span><span class="token punctuation">,</span> author<span class="token punctuation">:</span> <span class="token string">"George Orwell"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> bookTrunk<span class="token punctuation">.</span><span class="token function">store</span><span class="token punctuation">(</span>item<span class="token punctuation">:</span> <span class="token function">Book</span><span class="token punctuation">(</span>title<span class="token punctuation">:</span> <span class="token string">"Brave New World"</span><span class="token punctuation">,</span> author<span class="token punctuation">:</span> <span class="token string">"Aldous Huxley"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">print</span><span class="token punctuation">(</span>bookTrunk<span class="token punctuation">.</span><span class="token function">retrieve</span><span class="token punctuation">(</span>index<span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">.</span>title<span class="token punctuation">)</span> <span class="token comment">// Output: Brave New World</span> |
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ó.
1 2 3 4 5 6 | 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.