Contents
Chào mừng bạn đến với Fx Studio. Chủ đề lần này của chúng ta khá lại là quen thuộc đối với bạn. Đó là Generics trong Swift. Có lẽ, tất cả mọi người đã dùng tới nó rồi. Nhưng mà vẫn còn nhiều bạn chưa hiểu về nó lắm, dùng nó như một cách bản năng mà thôi. Bênh cạnh đó, chúng ta còn nhiều vấn đề liên quan tới chính Generics nữa. Thôi thì ta sẽ gom hết vào 1 bài viết này.
Nếu như mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Đây là một bài viết thuần lý thuyết chay, nên chúng ta sẽ không cần demo. Thay vào đó bạn có thể dùng Playground để xem kết quả của các đoạn code là được rồi. Môi trường thì cũng đơn giản.
-
- Xcode
- Swift
Về mặt lý thuyết thì lại liên quan tới nhiều điểm lý thuyết, mình sẽ liệt kê ở các link dưới đây.
Generics sẽ liên quan nhiều tới các khai báo cho các kiểu dữ liệu, function và protocol của bạn. Do đó, bạn cũng phải nắm được cơ bản về tụi nó trước, thì sẽ dễ hiểu hơn về Generics.
Vấn đề
Chúng ta sẽ xem qua ví dụ code cho 3 function sau nhóe:
func swapTwoInts(_ a: inout Int, _ b: inout Int) { let temporaryA = a a = b b = temporaryA } func swapTwoStrings(_ a: inout String, _ b: inout String) { let temporaryA = a a = b b = temporaryA } func swapTwoDoubles(_ a: inout Double, _ b: inout Double) { let temporaryA = a a = b b = temporaryA }
Trong đó:
- 3 function với cùng một kiểu chức năng tương tự nhau
- Hoán vị các giá trị cho các đối số truyền vào các tham số của function
- Sử dụng
inout
để có thể thay đổi biến ở bên ngoài.
Ta xem tiếp cách dùng của chúng như thế nào nhóe!
var someInt = 3 var anotherInt = 107 swapTwoInts(&someInt, &anotherInt) print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
Bạn cũng dễ dàng nhận ra được vấn đề rồi. Tuy là 3 function khác nhau, nhưng chúng lại giống nhau về tham số, logic. Khác nhau về kiểu dữ liệu của các tham số mà thôi.
Tại sao chúng ta phải cần tới nhiều function khác nhau để làm một công việc đơn giản như vậy?
Chúng ta sẽ tiết kiệm đi khá nhiều thời gian và dòng code khi có 1 kiểu dữ liệu nào đó sẽ đại diện chung cho tất cả. Vì vậy, khái niệm về Generics được đưa ra để giải quyết các bài toàn như vậy.
Generics là kiểu đại diện cho bất kỳ kiểu dữ liệu khác trong Swift.
Generics Functions
Đây sẽ là cách giải quyết vấn đề cho 3 function trên nhóe.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
Bạn chú ý <T>, nó sẽ là việc bạn thông báo cho trình biên dịch biết kiểu dữ liệu bạn sẽ dùng. Kiểu dữ liệu mà bạn sẽ dùng trong function swapTwoValue
sẽ được hiểu là một kiểu ngầm định T. Các kiểu dữ liệu cho tham số a
& b
sẽ cũng là T. Nó tương tự như Int, Double … nhưng nó sẽ có thể là Int hoặc Double hoặc bất cứ kiểu gì.
Dễ hiểu hơn thì T được gọi là một placeholder trong Generic.
Chúng ta sẽ xem cách dùng của nó là như thế nào nhóe.
var someString = "hello" var anotherString = "world" swapTwoValues(&someString, &anotherString) print(someString + " " + anotherString)
Trình biên dịch sẽ dựa vào kiểu dữ liệu của các tham số truyền vào cho function. Từ đó, Swift sẽ xác định kiểu T chính xác lúc run-time là gì. Như vậy, bạn truyền giá trị Int thì T sẽ là Int, hoặc Double thì T là Double. Okay, Done!
Đây cũng được xem là cách bạn dùng Generics đầu tiên trong sự nghiệp dev của bạn nhóe!
Type Parameters
Ở ví dụ trên, T là kiểu placeholder cho function. Nó sẽ xác định bằng kiểu của giá trị truyền cho các tham số của function. Bạn có thể dùng T cho các biến trong hàm và kiểu dữ liệu trả về của hàm. Và mỗi khi được gọi tới, thì kiểu T sẽ được thay thế bằng kiểu thực sự của giá trị truyền vào.
Khi bạn muốn dùng nhiều hơn 1 kiểu Generics trong function, thì có thể khai báo thêm vào trong dấu <>
. Các kiểu cách nhau bởi dấu ,
.
Ví dụ, với function có 2 tham số cùng là Generics nhưng xác định chúng với 2 kiểu dữ liệu khác nhau.
func swapTwoValues2<T, K>(_ a: T, _ b: K) { // .... }
Như vậy, bạn sẽ có kiểu dữ liệu của a
sẽ T và b
là K. EZ Game!
Naming Type Parameters
Bạn đã có kiểu dữ liệu rồi, thì đặt tên thế nào cho chúng. Câu trả lời đơn giản sẽ là:
Vô tư đi!
Nhưng một số trường hợp bạn cần ghi rõ nghĩa, điều này giúp cho tính tường minh cho code của bạn mà thôi. Ví dụ như với Dictionary<Key, Value>. Thì Key & Value là tên cho các Generics được dùng trong Dictionary.
Hoặc cách truyền thống là bạn có thể dùng các chữ cái in hoa để đặt cũng không sao. Ví dụ như: T, U, V hay A, B, C ... đều được. Chú ý một điều là cách đặt tên vẫn phải theo quy tắt Camel nhóe.
Generics Types
Chúng ta đã tìm hiểu về cách dùng Generics trong function để xác định các kiểu dữ liệu của các tham số. Bây giờ, ta sẽ tiếp tục nâng cấp việc sử dụng các Generics là các kiểu dữ liệu cho các struct/class/enum …
Chúng ta lấy một ví dụ sau:
struct IntStack { var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } }
Đây là một struct mô tả một ngăn xếp với kiểu dữ liệu chính được dùng Int. Ta có một Int Array được tạo ra. Các phần tử sẽ được push
vào. Và với pop
sẽ lấy ra phần tử cuối cùng trong mãng đó. Cũng khá đơn giản nha.
Chúng ta sẽ áp dụng Generic chung cho cả struct mà ta vừa mới định nghĩa trên. Thay vì chỉ áp dụng cho từng function một mà thôi. Xem ví dụ code nha.
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }
Khai báo với Generic sẽ áp dụng cho toàn bộ struct, với từ khóa <Element> sau tên của struct. Nó cũng tương tự như <T> ở ví dụ trên. Và bên trong struct, bạn sẽ dùng Element với tư cách là một kiểu dữ liệu. Nó giống như Int, Double ….
Với ví dụ Stack<Element> thì kiểu Generic sẽ được xác định ở vị trí trong struct:
- Kiểu cho array items
- Tham số cho function push
- Kiểu trả về cho function pop
Từ đó, Stack của chúng ta sẽ áp dụng được cho bất kì kiểu dữ liệu nào trong Swift cũng được hết. Xem ví dụ nhóe.
var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro")
Bạn chỉ cần quan tâm tới cách khởi tạo Stack, lúc này ta dùng với kiểu String cho Element thì sẽ khai báo như sau Stack<String>(). Và bạn thấy nó quen quen với Array hay Dictionary không nào.
Struct trong ví dụ trên được gọi là một Generic Type.
Extending a Generic Type
Với một Generic Type, khi bạn muốn mở rộng nó (extension) thì sẽ không cần khai báo lại các kiểu hay tên Generic nữa. Bạn sẽ extension bình thường như bao kiểu dữ liệu khác trong Swift mà thôi.
Chúng ta lấy tiếp ví dụ cho Stack nhóe.
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }
Ta có thuộc tính topItem
sẽ là kiểu Element. Nó sẽ phụ thuộc vào kiểu chính xác mà ta khởi tạo Stack. Với Element sẽ được hiểu ngầm định từ việc khai báo struct Stack ở trên rồi. Do đó, bạn chỉ cần sử dụng Element như bao kiểu dữ liệu khác trong phần extension mà thôi.
Xem qua ví dụ cách dùng cho topItem
đó nhóe.
if let topItem = stackOfStrings.topItem { print("The top item on the stack is \(topItem).") }
Lúc này, topItem
của bạn sẽ là kiểu dữ liệu String. Giống với kiểu dữ liệu mà ta cung cấp tại lúc khởi tạo Stack.
Type Constraints
Nếu việc gì cũng thỏa mái sử dụng quá thì cũng sẽ là điều bất lợi. Chúng ta sẽ phải có những quy định hay ràng buộc cụ thể với các kiểu dữ liệu mà chúng ta sử dụng. Điều này cũng áp dụng với Generic. Nó có nghĩa:
Bạn có thể cung cấp bất kỳ kiểu dữ liệu này cho Generics đều được. Nhưng sẽ phải tuân theo một số điều kiện. Như là phải kế thừa từ một kiệu dữ liệu nào đó. Hay phải tuân thủ theo một Protocol nào đó …
Ví dụ, với Stack của chúng ta sẽ là thỏa mái khi bạn sử dụng bất cứ kiểu dữ liệu nào cũng được hết. Tuy nhiên, nếu bài toán yêu cầu chỉ sử dụng với các kiểu dữ liệu là số (bao gồm Int, Float, Double …) thì Element của bạn phải conform với Number Protocol.
Đó chính là các Type Constraints mà sẽ ảnh hưởng tới các Generics của bạn.
Type Constraint Syntax
Cú pháp cho việc sử dụng cho Type Constraint vào các Generic thì cũng trương tự như việc bạn kế thừa hoặc conform một Protocol nào đó cho class/struct của bạn. Và ta sẽ đặt kiểu dữ liệu ràng buộc vào sau tên của Generic và cách nhau bởi dấu :
.
Xem ví dụ nhóe!
class SomeClass {} protocol SomeProtocol {} func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
Trong đó:
- Khai báo
someFunction
với 2 kiểu Generic là T & U - T sẽ là sub-class của SomeClass
- U sẽ conform với SomeProtocol
- Tham số
someT
sẽ có kiểu là T, tức là sub-class của SomeClass - Tham số
someU
sẽ có kiểu dữ liệu là U, tức là một kiểu nào đó mà conform với SomeProtocol
Ví dụ
Có thể bạn sẽ có thể là hơi mơ hồ tí. Nhằm làm sáng tỏ mọi thứ một cách đơn giản hơn thì bạn sẽ xem qua ví dụ sau:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil } let strings = ["cat", "dog", "llama", "parakeet", "terrapin"] if let foundIndex = findIndex(ofString: "llama", in: strings) { print("The index of llama is \(foundIndex)") }
Hàm findIndex
trên sẽ tìm thứ tự của value
trong một arrray kiểu [String]. Đây là một function với non-generic. Bạn thử suy nghĩ chúng ta sẽ tạo một function mới của nó và áp dụng Generic vào xem sao.
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
Vẫn là cách khai báo Generic như trên. Nhưng function sẽ bị trình biên dịch báo lỗi. Đơn giản là chúng không thể so sánh giá trị với nhau (toán tử ==). Nên Generic của chúng ta phải đảm bảo việc so sánh. Mà việc so sánh được các giá trị của một kiểu dữ liệu, thì cần phải conform Equatable Protocol.
Do đó, bạn sẽ có function mới như sau:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { for (index, value) in array.enumerated() { if value == valueToFind { return index } } return nil }
Lúc này, chúng ta sẽ sử dụng được function với nhiều kiểu dữ liệu khác nhau trong Swift. Với một yêu cầu là kiểu dữ liệu của chúng ta sử dụng vào Generic phải conform Equatable Protocol. Xem ví dụ cách dùng như sau:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) // doubleIndex is an optional Int with no value, because 9.3 isn't in the array let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"]) // stringIndex is an optional Int containing a value of 2
Associated Types
Chúng ta sẽ tìm hiểu về Associated Types tiếp theo. Mặc dù Associated Types không phải Generic Type. Nhưng về mặt ý nghĩa, thì cũng khá tương đồng. Bạn cũng sẽ thấy sự móc nối của chúng với nhau khá nhiều. Nên tìm hiểu về nó sẽ đỡ vất vả khi bạn đọc code.
Về Associated Types thì được dụng trong việc khái báo các Protocol. Khi bạn muốn khai báo 1 hoặc nhiều kiểu liên kết trong các phần của Protocol của bạn. Bạn sẽ cần cung cấp cho nó 1 cái tên (tương tự như Generic) và gọi là placeholder. Kiểu thực sự sẽ được chỉ rõ cho Associated Types khi bạn triển khai Protocol đó.
Từ khóa cho Associated Types là associatedtype
.
Cách sử dụng
Sau đây, là ví dụ khai báo một Protocol có sử dụng Associated Type.
protocol Container { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Trong đó:
- Item là tên cho Associated Type được dùng trong Container Protocol
- Item sẽ được dùng cho 3 nơi, chính là các functions và properties của Container
Cách triển khai Container Protocl thì như sau:
struct IntStack2: Container { // original IntStack implementation var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol typealias Item = Int mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
Chúng ta lấy lại ví dụ ở trên với InnStack nhóe. Nó là một struct không sử dụng Generic. Khi bạn implement Container cho IntStack thì cần chỉ ra kiểu dữ liệu của Item là gì. Bằng cách sử dụng typealias
trong IntStack là được.
Lúc này, Item được hiểu là Int. Và Int đảm bảo đầy đủ 3 điều kiện của Container Protocol yêu cầu cho Item. Item sẽ liên kết với IntStack qua kiểu Int nhóe.
Ơn trời, Swift cũng khá chìu fan khi bạn chỉ cần đảm báo việc triển khai các method & property của Protocol, thì Swift sẽ tự kiểu Associated Type là kiểu gì. Không cần phải thêm typealiase
nữa. Ta có ví dụ với việc xóa đi typealias
như sau:
struct IntStack3: Container { // original IntStack implementation var items: [Int] = [] mutating func push(_ item: Int) { items.append(item) } mutating func pop() -> Int { return items.removeLast() } // conformance to the Container protocol mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
Sử dụng với Generics
Còn với việc áp dụng Associated Type trong Generic Type thì sẽ như thế nào. Chúng ta cũng sẽ lấy ví dụ Stack trên luôn nhóe.
struct Stack<Element>: Container { // original Stack<Element> implementation var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } // conformance to the Container protocol mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] } }
Lúc này, Element ở Generic sẽ sử dụng là kiểu cho các method & properties của Container Protocol. Do đó, Swift có thể suy ra rằng Element là Associated Type để sử dụng làm Item cho Container Protocol.
Mở rộng kiểu dữ liệu với Associated Type
Và cũng như biết bao Protocol khác. Protocol mà bạn có sử dụng Associated Type thì vẫn dùng để mở rộng cho các kiểu dữ liệu khác của bạn vẫn được.
extension Array: Container {}
Như ví dụ, ta sẽ có Array được mở rộng với Container Protocol. Và kiểu Array cũng đáp ứng đủ 3 yêu cầu của Container Protocol với Associated Type của chính nó. Swift cũng tự động suy luận ra kiểu Associated Type là Array, tương tự như với cách dùng với struct Stack ở trên.
Sau khi xác định phần mở rộng này, bạn có thể sử dụng bất kỳ Array như một Container.
Thêm các Constraints vào Associated Type
Tương tự với cách bạn thêm các ràng buộc cho Generics, thì với Associated Type cũng như vậy. Xem qua ví dụ là hiểu liền.
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }
Lúc này, Item là Associated Type cho Container. Chúng ta có thêm một điều kiện là Item sẽ so sánh được. Vì bạn đã ràng buộc Item với Equatable Protocol.
Nên lúc bạn triển khai Container Protocol mới này, thì bạn cần chú ý tới việc truyền kiểu dữ liệu cụ thể cho Item. Sẽ phải đảm bảo kiểu dữ liệu đó so sánh được (hay conform với Equatable Protocol).
Sử dụng Protocol với các Associated Type đã ràng buộc
Câu chuyện sẽ càng rắc rối hơn khi bạn lại suy nghĩ đưa 1 kiểu Associated Type trong một Protocol. Và định nghĩa Protocol mới lại kế thừa cái Protocol đó.
Oan nghiệt thật!
Nhưng mà với Swift thì mọi việc vẫn có thể làm được hết. Bạn xem tiếp ví dụ sau:
protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix }
Ví dụ trên bạn sẽ thấy SuffixableContainer thừa kế lại Container. Tại đó, bạn cũng cần phải khai báo thêm một kiểu Associated Type cho SuffixableContainer chính là Suffix. Nhưng sẽ phải ràng buộc thêm điều kiện tiếp theo là Suffix == Item.
Đó là điều hiển nhiên, vì mỗi khi bạn conform SuffixableContainer Protocol, cũng tức là bạn conform Container Protocol. Bạn phải xác định rõ kiểu dữ liệu Item lúc này sẽ mang kiểu dữ liệu thực tế nào. Các Associated Type của các Protocol sẽ ràng buộc với nhau qua mệnh đề Where.
extension Stack: SuffixableContainer { func suffix(_ size: Int) -> Stack { var result = Stack() for index in (count-size)..<count { result.append(self[index]) } return result } mutating func append(_ item: Element) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Element { return items[i] } } var stackOfInts = Stack<Int>() stackOfInts.append(10) stackOfInts.append(20) stackOfInts.append(30) let suffix = stackOfInts.suffix(2)
Với ví dụ trên thì Associated type Suffix là Stack. Mà nó lại là một Generic với kiểu dữ liệu truyền vào là Int. Quá nhiều thứ móc nối trong này. Và kiểu Item của Container sẽ cùng kiểu Stack luôn. Còn với ví dụ cho các stuct không phải là Generic cũng như vậy.
extension IntStack: SuffixableContainer { func suffix(_ size: Int) -> Stack<Int> { var result = Stack<Int>() for index in (count-size)..<count { result.append(self[index]) } return result } mutating func append(_ item: Int) { self.push(item) } var count: Int { return items.count } subscript(i: Int) -> Int { return items[i] } }
Trong trường hợp này thì bạn sẽ thay Element là Int nhóe.
Generics Where Clauses
Như bạn thấy ở các phần trên, thì các kiểu ràng buộc (hay gọi là Type Constraints), cho phép bạn xác định các yêu câu đối với các Type parameters associated với a generic function, subscript, hoặc type. Các điều kiệu ràng buộc đó thì Swift gọi là mệnh đề Where. Từ khóa sử dụng là where
và bạn đặt sau chỗ khai báo tên struct/class/extension/function … và trước dấu {
.
Xem ví dụ nhóe, cho dễ hiểu!
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { // Check that both containers contain the same number of items. if someContainer.count != anotherContainer.count { return false } // Check each pair of items to see if they're equivalent. for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All items match, so return true. return true }
Trong đó:
allItemsMatch
là function được khai báo với 2 kiểu Generic là C1 và C2- C1 & C2 đều conform với Container Protocol
- Chúng ta thêm điều kiện
where
là C1.Item == C2.Item. Nghĩa là cả 2 phải cùng kiểu dữ liệu với nhau - Kèm theo điều kiện thứ 2 là C1.Item : Equatable, nghĩa là có thể so sánh được.
var stackOfStrings2 = Stack<String>() stackOfStrings2.push("uno") stackOfStrings2.push("dos") stackOfStrings2.push("tres") var arrayOfStrings = ["uno", "dos", "tres"] if allItemsMatch(stackOfStrings2, arrayOfStrings) { print("All items match.") } else { print("Not all items match.") }
Khi bạn khởi tạo đối tượng mới của Stack với String, thì bạn dùng nó vào allItemsMatch
. Tại đó, Stack sẽ có các kiểu ràng buộc trùng với tham số thứ 2 là arrayOfStrings
. Do arrayOfStrings
là String Array.
Tóm lại, bạn sẽ có câu lệnh điều kiện Where dùng trong việc khai báo với các Generic. Bạn hãy bình tĩnh và sử dụng nó thật chính xác. Vấn đề đơn giản chỉ là kết nối các kiểu dữ liệu với nhau sau cho trùng mà thôi.
Extensions with a Generic Where Clause
Cách phổ biến khi sử dụng mệnh đề Where là sử dụng với các extension. Bạn sẽ đỡ phải đau đầu suy nghĩ hơn là với khai báo. Xem ví dụ nhóe.
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }
Chúng ta có một extension cho Stack với 1 function mới là isTop
. Nó dùng để kiểm tra một phần tử có phải là phần tử trên cùng của Stack hay là không. Và khi bạn dùng các toán tử so sánh thì kiểu dữ liệu của bạn phải conform với Equatable Protocol. Và cách nhanh nhất là hãy thêm điều kiện vào extension như trên là Element: Equatable. Có nghĩa là đảm bảo kiểu dữ liệu của Element là một kiểu có thể so sánh được.
Khi có điều kiện vào thì trình biên dịch sẽ kiểm tra ngay từ lúc biên dịch và báo lỗi. Giúp bạn hạn chế đi nhiều bugs nhóe. Ví dụ cho lỗi nhóe.
struct NotEquatable { } var notEquatableStack = Stack<NotEquatable>() let notEquatableValue = NotEquatable() notEquatableStack.push(notEquatableValue) notEquatableStack.isTop(notEquatableValue) // Error
Ngoài ra, nó lại càng hữu ích khi bạn muốn mở rộng các Protocol mà tránh đi việc thay đổi các điều kiệu ràng buộc trong cách kiểu liên kết của chúng.
// #1 extension Container where Item: Equatable { func startsWith(_ item: Item) -> Bool { return count >= 1 && self[0] == item } } if [9, 9, 9].startsWith(42) { print("Starts with 42.") } else { print("Starts with something else.") } // #2 extension Container where Item == Double { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += self[index] } return sum / Double(count) } } print([1260.0, 1200.0, 98.6, 37.0].average()) // Prints "648.9"
Ở trên, là 2 ví dụ mở rộng thêm cho Container Protocol. Tại đó, bạn sẽ có được các function khác nhau và mỗi function sẽ phù hợp với một số các điều kiện ràng buộc riêng cho chúng. Mặc dù kiểu mà chúng đề cập tới, thì đều định danh là Item hết. Đây cũng là một ưu điểm của mệnh đề Where trong khai báo với các Generic Type.
Contextual Where Clauses
Ngữ cảnh khi bạn đặt mệnh đề Where sẽ ảnh hưởng tới kiểu Generics của bạn. Như khi bạn đặt Where cho function, hoặc khai báo một kiểu dữ liệu, hoặc extension của 1 thứ gì đó. Thì sẽ xác định được phạm vi ảnh hưởng của Where cho đối tượng mà mình đặt vào.
Ví dụ như sau:
extension Container { func average() -> Double where Item == Int { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } func endsWith(_ item: Item) -> Bool where Item: Equatable { return count >= 1 && self[count-1] == item } } let numbers = [1260, 1200, 98, 37] print(numbers.average()) // Prints "648.75" print(numbers.endsWith(37)) // Prints "true"
Trong đó, bạn có một extension cho Container với 2 function. Mỗi function đều xử dụng tới kiểu dữ liệu là Item. Tuy nhiên, tại mỗi function chúng ta lại cung cấp 1 điều kiện khác nhau. Với,
average
là IntendsWith
là conform Equatable
Lúc này, phạm vi ảnh hưởng của câu điều kiện where cho Item là trong phạm vi các function đó mà thôi. Bạn hãy xem tiếp ví dụ khác với mệnh đề Where cho cả extension nhóe.
// #1 extension Container where Item == Int { func average2() -> Double { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } } // #2 extension Container where Item: Equatable { func endsWith2(_ item: Item) -> Bool { return count >= 1 && self[count-1] == item } }
Chúng ta vẫn có các function như trên, tuy nhiên chúng nó lại ở các extension khác nhau. Mỗi extension đó lại chứa một mệnh đề Where khác nhau cho Item. Qua đó, bạn sẽ thấy được phạm vi ảnh hưởng khi chúng ta đặt mệnh đề Where cho các Generics nhóe.
Associated Types with a Generic Where Clause
Còn với các kiểu liên kết (Associated Types) thì cũng áp dụng mệnh đề Where tương tự như với Generic. Vì các Associated Types thuộc trong nội dung của các Protocol nên phạm vi ảnh hưởng của chúng sẽ không có.
Xem ví dụ nhóe.
protocol Container5 { associatedtype Item mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } associatedtype Iterator: IteratorProtocol where Iterator.Element == Item func makeIterator() -> Iterator }
Trong đó, bạn sẽ có 2 Associated Types cho Container Protocol. Mà với Iterator thì lại có ràng buộc mệnh đề Where là kiểu dữ liệu của Element cùng kiểu với Item.
protocol ComparableContainer: Container where Item: Comparable { }
Và khi kế thừa các Protocol có các Associated Types, thì bạn cũng có thể sử dụng mệnh đề Where cho các Associated Types đó tại khai báo của Protocol mới.
Generic Subscripts
Trước tiên, bạn sẽ đọc về khái niệm Subscripts trong Swift trước nhóe.
Subscript được sử dụng để truy cập thông tin của một collection, sequence và một list trong Classes, Structures và Enumerations. Đặc biệt những Subscript có thể lưu trữ và truy xuất các giá trị bằng index mà không sử dụng một method riêng biệt.
Trong phạm vi bài viết, bạn có thể áp dụng các Generic cho subscript. Bên cạnh đó, các kiểu liên kết và các mệnh đề Where cũng sẽ được áp dụng như đã trình bày ở các phần trên. Lúc này, bạn sẽ gọi chúng với một cái tên thân thiện hơn.
Đó là Generic Subscripts.
Mình trình bày phần này là mang tính chất giới thiệu thôi. Chi tiết thì bạn sẽ tự tìm hiểu nhóe. Còn sau đây là ví dụ cho Generic Subscripts.
extension Container { subscript<Indices: Sequence>(indices: Indices) -> [Item] where Indices.Iterator.Element == Int { var result: [Item] = [] for index in indices { result.append(self[index]) } return result } }
Trong đó:
- Ta có một
subscript
cho Container Protocol. - Sử dụng một kiểu Generic là Indices.
- Indices ràng buộc với Sequence Protocol. Nghĩa là đối tượng mình áp dụng subscript này là 1 mãng.
- Mệnh đề Where nhằm xác định Indices.Iterator.Element là kiểu Int. Hay
index
truyền vàosubscript
là 1 số nguyên.
Như vậy, bạn sẽ có 1 cách mới để lấy giá trị từ subscript
cho đối tượng conform Container Protocol. Là truyền 1 Int Array thay cho 1 giá trị Int. Kết quả trả về là một mãng mới, với các phần tử theo các giá trị trong mãng gốc tương ứng với các index
là tham số truyền vào cho subscript
.
Xem cách dùng nhóe!
let array = ["cat", "dog", "llama", "parakeet", "terrapin"] let old1 = array[0] // cat let old2 = array[3] // parakeet let new = array[[0, 3]] // ["cat", "parakeet"]
Tạm kết
- Tìm hiểu về Generic trong Swift.
- Cách sử dụng nó vào khai báo cho các function, hay các struct/class/enum, hay protocol.
- Các ràng buộc dữ liệu với các Generic.
- Các kiểu liên kết (Associated Types) và các mối quan hệ giữa chúng với Generic.
- Mệnh đề Where cho các kiểu Generic & cách sử dụng chúng trong function hay khai báo với các Generic.
- Phạm vi ảnh hưởng của các mệnh đề Where đối function hay Protocol.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Generics trong Swift. Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
You may also like:
Archives
- December 2024 (4)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)