Contents
Chào mừng bạn đến với Fx Studio. Bài viết này sẽ nói về một khái niệm trong Swift 5.1, tuy cũng là khá cũ rồi nhưng bây giờ mình mới có cơ hội viết về nó. Đó là Opaque Type, hay còn có thể gọi với tên khác là Opaque Return Type. Đây là một khái niệm khá là phức tạp và khó hiểu. Nhưng cách dùng thì lại rất đơn giản & được ứng dụng nhiều trong Swift & SwiftUI.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt lý thuyết, thì đây có thể xem là phần tiếp nối cho khái niệm Generics trong Swift. Do đó, bạn cần chuẩn bị một lượng kiến thức kha khá để có thể hiểu được nó nhanh hơn. Và nếu bạn chưa biết hoặc quên mất Generics là gì thì có thể tham khảo link dưới đây.
Về công cụ và môi trường thì bạn có thể sử dụng cơ bản thôi.
-
- Xcode
- Swift 5.1 (hoặc mới hơn)
Về mặt demo, bạn chỉ cần sử dụng Playground là ổn rồi. Chúng ta sẽ in kết quả ở màn hình console là chính.
Vấn đề
Trước khi đi vào cụ thể khái niệm Opaque Type là gì?. Thì chúng ta sẽ tìm hiểu các ví dụ sau. Vì Opaque Type sẽ liên quan mật thiết với Protocol & Generic. Và đặc biệt là các Protocol có các kiểu liên kết (Associated Type)
Bắt đầu, thì chúng ta có một protocol được khai báo như sau:
protocol P { associatedtype Value var value: Value { get } init(value: Value) }
Trong đó:
- P là một protocol và có 1 associatedtype là Value
- Thuộc tính
value
được tạo ra với kiểu dữ liệu là Value
Mọi thứ vẫn bình thường. Tiếp theo, bạn sẽ khai báo các struct cho việc implement P Protocol kia. Xem ví dụ sau:
struct S1 : P { var value: Int } struct S2: P { var value: String }
Ta có S1 & S2 cùng conform P, nhưng kiểu dữ liệu cho value
lại khác nhau. Đây cũng chính là một trong những thứ rắc rối khi bạn sử dụng các Generic & Associated Type trong vấn đề xác định kiểu dữ liệu thực tế được triển khai.
Function có kiểu trả về là một Protocol
Bạn thử xem ví dụ như sau:
protocol P1 { } struct S11: P1 { } struct S12: P1 { } func foo() -> P1 { return S11() //S12() }
Function foo
có kiểu trả về là P1 Protocol. Có nghĩa là chúng ta phải đặt các đối tượng mà đã implement P1 trong return
của function. Như ví dụ, ta sẽ có thể trả về 1 trong 2 kiểu cụ thể, là S11 hoặc S12.
Bài toán sẽ khá là vui hơn, khi chúng ta sử dụng P (vì nó có Associated Type). Xem ví dụ nhóe!
func foo() -> P { return S1() }
Rất tiếc là trình biên dịch đã cấm bạn thực thi chương trình. Compiler không giữ type identity của return value khi sử dụng protocol như một return type.
Function với kiểu trả về cụ thể
Chúng ta tiếp tục với ví dụ trên nhóe. Vì đã không sử dụng được Protocol với Associated Type. Bây giờ, ta sẽ trả về một kiểu cụ thể của P, tức là S1 hoặc S2 theo ví dụ trên đã khai báo.
func foo() -> S1 { return S1(value: 10) }
Mọi thư đã ổn. Tuy nhiên, vẫn là cách truyền thống. Không có gì mới lạ ở đây hết. Và bạn thử suy nghĩ:
Nếu ta có tới 1 triệu struct/class đã implement P, thì phải viết tới 1 triệu function như vậy sao.
Function với kiểu trả về là Generic
Tiếp tục, bạn sẽ có giải pháp là sử dụng Generic để có thể linh hoạt khi muốn trả về hoặc sử dụng các đối tượng đến từ các thể hiện của Protocol đó. Mà không ràng buộc vào bất cứ kiểu dữ liệu cụ thể nào cả. Ta tiếp tục ví dụ nhóe!
func foo<T: P>(value: T.Value) -> T { return T(value: value) } let s1: S1 = foo(value: 10) let s2: S2 = foo(value: "ahihi")
Nhìn qua, bạn sẽ thấy đây là một cách khá ổn. Đảm bảo hầu hết các yêu cầu đặt ra. Nhưng vẫn còn một điều mà ta cảm thấy khó chịu. Đó là bạn phải chỉ rõ kiểu dữ liệu cho return hoặc tham số của function.
Ở một số ngữ cách khác, bạn sẽ gặp khó khăn hơn nhiều khi xử lý logic với các kiểu Associated Type trong các struct/class của bạn. Vì đôi lúc, bạn cũng cần phải xác định rõ kiểu giá trị cụ thể như thế nào.
Giải pháp cuối
Vòng vòng cũng khá nhiều rồi và bây giờ chúng ta tới giải pháp cuối (hoặc xem như là cuối cùng cũng được nhóe). Đó là cách mà chúng ta viết function với kiểu giá trị trả về là một kiểu mờ đục/kiểu mập mờ/ kiểu mờ ảo … (Opaque Type). Hay còn gọi là:
Opaque return type
Ta sẽ xem ví tiếp ví dụ trên với Opaque Type nhóe!
func foo() -> some P { return S1(value: 11) // S2() }
Với Opaque Type cho return type thì cuối cùng chúng ta đã có thể return về một Protocol (hay 1 lớp cha nào đó). Nó đại diện chính cho kiểu giá trị trả về của hàm. Trình biên dịch sẽ duy trì định danh return type đó. Khi gọi thực thi thì không cần biết giá trị cụ thể nào trả về từ function, miễn là nó có implement/kế thừa Protocol đó mà thôi.
Thực tế cuộc sống thì Opaque Type này phục vụ việc return lại 1 kiểu giá trị nào đó (mập mờ), miễn là kiểu đó xuất thân từ 1 kiểu xác định nào trước (super class).
Opaque Type
Khái niệm
Sau khi, bạn đã biết được các vấn đề xảy ra và dẫn dắt bạn tới với kiểu dữ liệu mới (khái niệm mới cho kiểu dữ liệu). Chúng ta sẽ tìm hiểu khái niệm của Opaque Type nhóe.
Opaque Return Type là 1 kiểu trả về cho functon một cấp mập mờ. Mập mờ ở đây là không chỉ rõ ra kiểu cụ thể, mà chỉ cần đó là 1 trong các kiểu được thừa kế từ 1 kiểu chung nào đó.
Từ khóa bạn sẽ dùng để khai báo một kiểu Opaque Type là some
.
Và bạn thấy nó hơi rắc rối và khó hiểu, thì mình sẽ cung cấp cho bạn 1 định nghĩa đơn giản hơn nữa.
Đảo ngược lại 1 Generic khi sử dụng.
Ví dụ: với -> some P
, thì các class con của P đều được chấp nhận.
Lưu ý
Và bạn cũng đường quá ảo tưởng về sức mạnh của em nó nha. Có khi ôm hận đó. Xem tiếp ví dụ nhỏ nhỏ sau đây.
func bar(_ x: Int) -> some P { if x > 10 { return S1(value: 12) } else { return S2(value: "ahihi") } }
Với ví dụ trên, bạn sẽ nhận được Error mà thôi. Cần lưu ý, trong function lại là bắt trả về 1 kiểu xác định (tức là 1 trong các sub-class của Qpaque Type kìa hoặc chính nó).
Đúng là cuộc đời vẫn không như cuộc sống.
Bạn cũng nhanh chóng nhận ra Opaque Type không phải là vạn năng. Và nó sẽ được dùng như thế nào, ta lại tiếp tục tìm hiểu các phần sau để được giải đáp thêm nhóe.
Opaque Type với PATs
Trong đó, PATs có nghĩa là Protocols with Associated Types. Nghĩa là một Protocol với các kiểu liên kết được khai báo trong nó. Ta có điểm lợi ích đầu tiên là bạn sử dụng PATs trong kiểu giá trị trả về của function mà không cần biết kiểu giá trị cụ thể của nó là gì.
Chúng ta sẽ không còn bị giới hạn từ các version của thư viện nữa, vì lúc này ta không cần phải xác định cụ thể kiểu giá trị của chúng ta phải đảm bảo tồn tại trong version của bạn hay không. Cũng khá hay đó.
Ví dụ tiếp nha!
func giveMeACollection() -> some Collection { return [1, 2, 3] } let collection = giveMeACollection() print(collection.count) // 3
Trong đó, Collection là một PATs và nó phụ thuộc vào kiểu Array mà mình truyền cho nó. Như vậy, function giveMeACollection
sẽ hoạt động được trong mọi hoàn cảnh và thời gian. Ahihi!
Opaque Type với kiểu xác định
Khi bạn muốn triển khai với nhiều đối tượng trả về cho nhiều kiểu giá trị trả về xác định, thì phải như thế nào? Nhưng trước tiên, bạn cần biết các kiểu Opaque Type khi thực thi một kiểu cụ thể duy nhất được trả về, trình biên dịch biết rằng hai lệnh gọi đến cùng một hàm phải trả về hai giá trị cùng kiểu.
Xem code ví dụ nha, do ở trên mình từ khá khó hiểu.
func fooo() -> some Equatable { return 5 } let f1 = fooo() let f2 = fooo() print(f1 == f2)
Ví dụ, trên là chúng ta cùng gọi 1 function nhưng thực hiện 2 lần. Thì kết quả sẽ so sánh được vì cả f1 & f2 lúc này sẽ được hiểu là cùng kiểu giá trị. Tuy nhiên, điều này sẽ không thực hiện được khi bạn có 2 function, khi chúng cùng trả về 1 kiểu Opaque Type với sử dụng PATs. Xem qua ví dụ tiếp nha.
func foo1() -> some P { S1(value: 12) } func foo2() -> some P { S2(value: "ahuhu") } let s11 = foo1() let s22 = foo2() print(s11 == s22) // error
Khi so sánh như vậy thì sẽ không được. Vì khi dùng với some P,
với mỗi function thì nó được hiểu 1 kiểu riêng. Trong ví dụ thì : là kiểu foo1P và foo2P. Mặc dù, về code thì hợp lý, nhưng trình biên dịch sẽ dịch ngược lại các placeholder Generic cho các đầu ra với các kiểu giá trị khác nhau được sử dụng.
Do đó, bạn vẫn cần phải xác định rõ kiểu giá trị nhất là khi chúng là 1 PATs trong các function sử dụng Opaque Return Type.
Kết hợp với Generic Placeholders
Sự kết hợp của Opaque Type với Generic placeholders vẫn rất hiệu quả. Khi bạn muốn cho function của bạn vừa đa năng với các kiểu dữ liệu khác nhau, vừa có không cần trả về một kiểu cụ thể nào cả.
Xem qua ví dụ sau nhóe!
protocol P2 { var i: Int { get } } struct S : P2 { var i: Int } func makeP() -> some P2 { return S(i: .random(in: 0 ..< 10)) } func bar<T : P2>(_ x: T, _ y: T) -> T { return x.i < y.i ? x : y } let p1 = makeP() let p2 = makeP() print(bar(p1, p2))
Kết quả của makeP
vẫn sử dụng tốt cho bar
với khai báo Generic và placeholder là T. Tất cả, các đối tượng và kết quả đều xoay quanh các kiểu dữ liệu cùng họ hàng với P2 mà thôi. Và khi bạn so sánh với một kiểu mới T (cùng conform P2) thì function bar
vẫn hoạt động được. Xem tiếp ví dụ nha!
struct T : P2 { var i: Int } let t1 = makeP() print(bar(p1, t1))
Như vậy, với cách sử dụng Opaque Type với Generic placeholders như trên. Thì T sẽ hiểu có kiểu dữ liệu là makeP và function hoạt động được mặc dù kiểu trả về thực sự có thể là S hoặc T đi nữa.
Dành cho các bạn thích khám tiếp thì xem qua tiếp function sau:
func makeP2() -> some P2 { return S(i: .random(in: 0 ..< 10)) } let t2 = makeP2() print(bar(p1, t2)) //error
Lần này, ta có function makeP2
cùng kiểu trả về là some P
, nhưng không thể thực được function bar
. Với Opaque Type, thì trình biên dịch sẽ hiểu là makeP & makeP2 là 2 kiểu dữ liệu khác nhau. Do đó, khi bạn truyền vào placeholder T cho function Generic thì chúng nó không cùng kiểu dữ liệu.
Tại sao bạn nên sử dụng Opaque Type?
Chúng ta đã đi qua một lượt về khái niệm, ưu và nhược, cũng như các đặc trưng của Opaque Type rồi. Thì nhiều bạn sẽ thắc mắc là tại sao phải dùng nó.
Phức tạp vãi cả ra.
Câu trả lời thì:
Đơn giản là để linh hoạt hơn khi dùng với Protocol.
Nếu code như thế này:
func makeP() -> S { return S(i: 0) }
Bạn chỉ có thể dùng với cụ thể loại S cho function makeP. Với cách này, chúng ta đang tự giới hạn tâm trí của chúng ra nhiều. Nếu ta tiếp tục thay đổi lại 1 chút:
func makeP() -> some P2 { return S(i: 0) } func makeP() -> some P2 { return T(i: 1) }
Với 1 function makeP với some P
thì bạn tùy ý viết thêm và không có phá vỡ code nó ra. Không quan tâm tới kiểu dữ liệu cụ thể cho các function. Áp dụng cho các trường hợp khi bạn triển khai các PATs, hoặc là khi bạn update các API cho cách thư viện nhằm tránh việc trùng lặp các function.
Tất cả mọi thứ trong project sẽ vẫn chạy ổn, ngay cả khi code chính của bị phá vỡ. Nhưng đảm bảo việc giá trị trả về khi nó là một Opaque Type.
Opaque Type trong SwiftUI
Khi nhắc tới Opaque Type thì nó được ứng dụng nhiều nhất trong SwiftUI. Và chắc bạn cũng đã nhận ra các đoạn code huyền thoại như thế này.
struct ContentView: View { var body: some View { Text("Hello, world!") } }
Các đối tượng UI Control trong SwiftUI đều kế thừa lại View Protocol và nó có một thuộc tính body
huyền thoại. Với kiểu là some View
, nghĩa là bạn có thể cung cấp cho nó bất kỳ gì, miễn đối tượng đó là con của View Protocol.
Và nếu không sử dụng Opaque Type thì kiểu dữ liệu cho body
lai khá là phức tạp. Ví dụ, bạn có HStack > Text & Image. Thì kiểu cho body sẽ là: HStack<TupleView<(Text, Image)>> … Độ phức tạp sẽ tăng lên khi View của bạn có quá nhiều cấp lồng nhau.
Ngoài ra, bạn sẽ thấy kiểu Opaque Type cũng áp dụng được cho các thuộc tính cũng rất hiệu quả. Chứ không chỉ đơn giản dùng vào cho kiểu giá trị trả về của function mà thôi. Nhất là khi bạn có quán nhiều kế thừa và Generic phức tạp.
Ví dụ tổng kết
Để tránh cho các tín đồ SwiftUI cứ nghĩ là Opaque Type và some
chỉ dành riêng cho SwiftUI. Mình sẽ đưa ra cho bạn một ví dụ cuối cùng, mang tính chất tổng hợp lại tất cả các điểm lý thuyết trên. Xem ví dụ nhóe!
// ---- World of Bird ---- // protocol Bird {} final class LeLe: Bird {} final class Eagle: Bird {} final class Penguin: Bird {} // ---- somewhere in the world ---- // protocol Place { associatedtype B: Bird var bird: Self.B { get } } struct VietNam : Place { var bird: some Bird { return Eagle() } } struct China : Place { var bird: some Bird { return LeLe() } }
Ý nghĩa của bài toán này lại rất đơn giản. Ở tất cả các nơi trên thế giới này, thì đều có một loại chim. Chúng nó có thể giống nhau hoặc không giống nhau. Nhưng bạn luôn biết được chim sẽ tồn tại. Luôn luôn là như vậy.
Tạm kết
- Opaque Type là một kiểu Generic ngược
- Từ khóa sử dụng là some
- Sử dụng khi bạn muốn không trả về nhiều kiểu cụ thể trong cùng 1 function.
- Opaque Type sẽ tự động kết hợp với function để suy luận ra kiểu trả về.
- Có thể sử dụng với các Protocol Generic. Giúp mình thoải mái hơn trong lập trình.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Opaque Type 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
- 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
- Strategy Pattern trong 10 phút
You may also like:
Archives
- December 2024 (3)
- 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)