Vì sao cần generic
Bạn sẽ sớm gặp những kiểu lẽ ra phải dùng được với nhiều kiểu phần tử. Một "cặp" hai giá trị không quan tâm các giá trị đó là số nguyên, chuỗi hay một shape do người dùng định nghĩa. Generic cho phép bạn viết kiểu một lần và tạo instance với bất kỳ kiểu phần tử nào nơi gọi cần.
Cách làm thay thế, viết IntPair, StringPair, BytePair, cứ thế tiếp tục, sẽ nhanh chóng trở nên nhàm chán và không kết hợp được với nhau. Generic trong Zero là công cụ chuẩn cho việc này.
Hàm generic
Khai báo tham số kiểu trong dấu ngoặc nhọn, giữa tên hàm và danh sách tham số:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T và U là các kiểu giữ chỗ: nơi gọi quyết định chúng là gì.
Lời gọi thường không cần ghi rõ chúng; trình biên dịch suy ra từ kiểu của đối số:
let pair = makePair(40, 2_u8)
Ở đây T được suy ra là i32 (mặc định cho hằng số nguyên không có hậu tố) và U là u8 (từ hậu tố _u8). Biến pair thu được có kiểu Pair<i32, u8>.
Nếu suy luận sẽ chọn sai kiểu, chẳng hạn vì các hằng số không rõ ràng, bạn có thể chỉ định tham số ngay tại chỗ gọi:
let pair = makePair<u8, u8>(1, 2)
(Cú pháp gọi với ngoặc nhọn có đúng như trên hay không có thể khác nhau giữa các phiên bản Zero; hãy xem tài liệu hiện hành để biết cách viết chính xác. Hành vi ưu tiên suy luận mới là phần ổn định.)
Shape generic
Shape nhận tham số kiểu theo cùng cách:
shape Pair<T, U> {
left: T,
right: U,
}
Kiểu của mỗi trường có thể nhắc đến các tham số. Instance thì cố định chúng lại:
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
Một ví dụ hoàn chỉnh kết hợp shape generic và hàm generic. Nhấn Chạy để xem suy luận kiểu hoạt động:
Một khai báo shape generic, một hàm generic, một chỗ gọi có kiểu chặt chẽ. Không cần code lặp kiểu IntBytePair.
Type alias
Khi cùng một kiểu tham số hóa xuất hiện lặp đi lặp lại, hãy đặt tên cho nó bằng type:
type BytePair = Pair<u8, u8>
Giờ BytePair có thể thay thế cho Pair<u8, u8> ở bất cứ chỗ nào bạn viết được một kiểu:
Alias chỉ là tính năng đặt tên: nó không tạo ra một kiểu riêng biệt. Một hàm nhận BytePair sẽ vui vẻ nhận một giá trị kiểu Pair<u8, u8> (và ngược lại).
Generic trong thư viện chuẩn
Cùng cơ chế này vận hành rất nhiều phần của thư viện chuẩn. Một vài thứ bạn sẽ gặp trong code Zero thực tế:
Maybe<T>: một giá trị tùy chọn, chứa mộtThoặc không có gì.Span<T>: một lát cắt mượn trên các giá trịT.Span<u8>là cách nhìn chuẩn vào một bộ đệm byte.ref<T>vàmutref<T>: các kiểu tham chiếu tường minh cho trường hợp bạn cần chia sẻ dữ liệu mà không sao chép.
Bạn không cần học hết những thứ này cùng lúc. Điểm mấu chốt của generic là cùng một hình dạng dùng được cho bất kỳ kiểu phần tử nào bạn đang có.
Khi nào generic đáng dùng (và khi nào không)
Hãy dùng generic khi bạn thấy mình viết cùng một hàm hoặc shape hai lần với các kiểu phần tử khác nhau. Hãy dùng kiểu cụ thể khi:
- Logic của hàm chỉ có nghĩa với một kiểu cụ thể (chẳng hạn một bộ phân tích cú pháp cho
String). - Bạn muốn kiểu xuất hiện trong thông báo lỗi để gỡ lỗi dễ hơn.
- Đặc tính hiệu năng phụ thuộc vào một kích thước cụ thể trong bộ nhớ.
Generic có cái giá thật: tệp nhị phân lớn hơn (mỗi lần tạo instance sinh ra code mới) và thời gian biên dịch chậm hơn một chút. Với hầu hết code ứng dụng, cái giá đó không đáng kể, nhưng nên biết khi bạn viết code kiểu nhúng, nơi kích thước tệp nhị phân là quan trọng.
Ghi chú về ràng buộc
Một số hệ thống generic cho phép ràng buộc tham số kiểu ("T phải hỗ trợ ==", "T phải cài đặt Iterator"). Câu chuyện ràng buộc của Zero trước 1.0 vẫn đang thay đổi: các ví dụ trong repo chính thức dùng generic ở dạng đơn giản, không có giới hạn phức tạp. Khi ngôn ngữ ổn định, hãy chờ cú pháp ràng buộc xuất hiện ở dạng nhỏ gọn, đều đặn, nhất quán với phần còn lại của ngôn ngữ. Còn bây giờ, hãy viết generic hoạt động với mọi T mà bạn thực sự truyền vào, và để trình biên dịch báo cho bạn khi một thao tác không được hỗ trợ.
Tiếp theo: enum
Generic cho phép bạn tham số hóa theo kiểu. Viên gạch tiếp theo nằm ở đầu kia của phổ: enum, kiểu liệt kê đơn giản của Zero cho những trường hợp mà các biến thể không mang dữ liệu thêm.
Câu hỏi thường gặp
Generic hoạt động thế nào trong Zero?
Khai báo tham số kiểu trong dấu ngoặc nhọn sau tên hàm hoặc tên shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> hoặc shape Pair<T, U> { left: T, right: U }. Nơi gọi hoặc chỉ định tham số tường minh (Pair<i32, u8>), hoặc để trình biên dịch suy ra từ các đối số của lời gọi.
Shape trong Zero có thể là generic không?
Có. Một shape có thể nhận tham số kiểu với cùng cú pháp ngoặc nhọn như hàm: shape Pair<T, U> { left: T, right: U }. Mỗi trường có thể dùng các tham số trong kiểu của nó. Instance được tạo bằng cách viết kiểu đã tham số hóa, ví dụ Pair<i32, u8>.
Có cần chỉ định tham số kiểu khi gọi hàm generic không?
Thường là không. Trình biên dịch suy ra chúng từ kiểu của đối số. Gọi makePair(40, 2_u8) là đủ: T thành i32 và U thành u8. Bạn có thể chỉ định tham số tường minh khi suy luận sẽ chọn sai kiểu hoặc khi muốn ghi rõ chúng ngay tại chỗ gọi.
Type alias trong Zero là gì?
Type alias là tên viết tắt cho một biểu thức kiểu dài hơn. type BytePair = Pair<u8, u8> cho phép bạn viết BytePair ở mọi chỗ lẽ ra phải viết Pair<u8, u8>. Alias thuần túy là tính năng đặt tên: nó không tạo ra kiểu mới, chỉ là cách ngắn hơn để gọi một kiểu đã có.
Generic xuất hiện ở đâu trong thư viện chuẩn của Zero?
Ở khắp nơi, bất cứ chỗ nào một kiểu cần chứa hoặc thao tác trên một kiểu phần tử tùy ý. Maybe<T> cho giá trị tùy chọn, Span<u8> cho lát cắt byte, các kiểu container được tham số hóa theo phần tử của chúng. Cùng một cơ chế generic xử lý cả kiểu do người dùng định nghĩa lẫn kiểu của thư viện chuẩn.