Hãy xem đoạn mã sau:
/**
* Hàm bậc cao: Phân tích cú pháp JSON an toàn
* @param jsonMessage Chuỗi JSON
* @param onSuccess Callback khi phân tích thành công
* @param onError Callback khi phân tích thất bại (tùy chọn)
*/
private inline fun <reified T> safeParseJson(
jsonMessage: String,
onSuccess: (T) -> Unit,
noinline onError: ((Exception) -> Unit)? = null
) {
try {
val result = gson.fromJson(jsonMessage, T::class.java)
onSuccess(result)
} catch (e: JsonSyntaxException) {
Log.e(TAG, "Lỗi phân tích JSON - Loại: ${T::class.simpleName}, lỗi: ${e.message}")
Log.e(TAG, "JSON có vấn đề: ${jsonMessage.take(300)}")
onError?.invoke(e)
} catch (e: Exception) {
Log.e(TAG, "Lỗi ngoại lệ - Loại: ${T::class.simpleName}, lỗi: ${e.message}")
onError?.invoke(e)
}
}
<reified T> ở đây nghĩa là gì?
Kotlin reified
1. Hiểu vấn đề: Tại sao đôi khi không thể lấy được kiểu của generic?
Bạn biết rằng cả Kotlin và Java đều có generic, ví dụ:
fun <T> printType(item: T) {
println(item::class.java)
}
Bạn có thể nghĩ rằng nó sẽ in ra kiểu, nhưng nếu bạn gọi như sau:
printType(listOf("abc"))
Thì trong một số trường hợp, chương trình không thể lấy được kiểu thực sự của T. Bởi vì — Kotlin (giống như Java) sẽ "xóa generic" khi chạy (gọi là Type Erasure).
Nghĩa là:
Khi bạn viết code, bạn có thông tin về kiểu, Nhưng khi chương trình chạy, kiểu này bị "xóa".
2. Vấn đề gì xảy ra khi "kiểu bị xóa"?
Ví dụ, bạn muốn viết một hàm phân tích cú pháp JSON chung:
fun <T> parse(json: String): T {
return Gson().fromJson(json, T::class.java)
}
Dòng này sẽ báo lỗi, vì:
Bộ biên dịch Kotlin nói: "Tôi không biết T là gì, bạn phải cung cấp Class để phân tích."
3. Kotlin cung cấp giải pháp: reified
Khi bạn thêm reified vào hàm nội tuyến (inline function),
Kotlin sẽ giúp bạn giữ lại kiểu khi biên dịch.
Ví dụ:
inline fun <reified T> parse(json: String): T {
return Gson().fromJson(json, T::class.java)
}
Giờ thì có thể sử dụng!
4. Tại sao phải dùng inline và reified cùng nhau?
Bởi vì:
inline: Làm cho hàm "mở rộng" khi biên dịch, tức là sao chép trực tiếp nội dung hàm vào nơi gọi;reified: Chỉ có thể dùng trong hàminline, vì chỉ như vậy, bộ biên dịch mới biết được kiểu thực sự của T.
Vì vậy,
reifiedphải gắn vớiinline.
5. So sánh ví dụ: Có vs Không có reified
Không có reified:
fun <T> parse(json: String): T {
return Gson().fromJson(json, T::class.java) // ❌ Báo lỗi, không lấy được kiểu T
}
Có reified:
inline fun <reified T> parse(json: String): T {
return Gson().fromJson(json, T::class.java) // ✅ Có thể sử dụng
}
6. Sử dụng trong thực tế (ví dụ như hàm safeParseJson)
private inline fun <reified T> safeParseJson(
jsonMessage: String,
onSuccess: (T) -> Unit,
noinline onError: ((Exception) -> Unit)? = null
)
Ở đây, reified T cho phép bạn viết như sau:
safeParseJson<User>(json) { user ->
println("Tên người dùng: ${user.name}")
}
Không cần truyền User::class.java thủ công, Kotlin sẽ tự động biết T là User.
7. Tóm tắt ngắn gọn
| Đặc tính | Tác dụng | Ví dụ |
|---|---|---|
inline |
Hàm được mở rộng khi biên dịch, tăng hiệu suất | inline fun foo() |
reified |
Làm cho generic vẫn biết được kiểu khi chạy | inline fun <reified t=""> parse() |
| Cả hai | Có thể sử dụng T::class và T::class.java khi chạy |
Phân tích cú pháp JSON, phản chiếu, xác định kiểu, v.v. |
Nếu so sánh với cuộc sống, ta có thể nhớ như sau:
Hàm generic bình thường giống như "hộp bưu kiện", nhãn bị xé, bạn không biết bên trong là gì; Thêm
reifiedthì giống như "viết rõ tên bên ngoài hộp", bạn biết bên trong là gì trước khi mở.
Java generics
1. Java generics cũng "xóa kiểu"
Trong Java, tất cả thông tin generic đều bị "xóa" sau khi biên dịch, chỉ giữ lại kiểu gốc.
Ví dụ:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
Cả hai danh sách này khi chạy thực tế cùng một kiểu, chỉ là ArrayList.
JVM không biết các tham số generic String hoặc Integer, vì:
Java generics là "kiểm tra tại thời điểm biên dịch + xóa kiểu khi chạy".
Ví dụ:
public <T> void printType(T item) {
System.out.println(item.getClass());
}
Gọi:
printType("Hello"); // In ra class java.lang.String
printType(123); // In ra class java.lang.Integer
Dường như không có vấn đề, nhưng thực tế hàm không biết mình đang xử lý <String> hay <Int>, nó chỉ dựa vào giá trị truyền vào để suy đoán kiểu.
Nếu bạn muốn giữ thông tin generic trong Java, bạn phải làm như sau:
public <T> T fromJson(String json, Class<T> clazz) {
return gson.fromJson(json, clazz);
}
Phải truyền thêm Class<T>, vì Java không tự giữ lại kiểu.
Kotlin reified có thể lấy kiểu khi chạy
reified trong Kotlin là một kỹ thuật biên dịch + đường dẫn cú pháp.
inline fun <reified T> parse(json: String): T {
return gson.fromJson(json, T::class.java)
}
Bộ biên dịch Kotlin sẽ ghi trực tiếp kiểu thực sự của T (ví dụ: User::class.java) khi biên dịch.
Vì vậy:
Kotlin với
inline + reifiedcó thể lấy kiểu trực tiếp, không cần truyền như Java.
Bảng so sánh
| Đặc tính | Java generics | Kotlin generics thông thường | Kotlin inline + reified |
|---|---|---|---|
| Thông tin generic | Có tại thời điểm biên dịch, xóa khi chạy | Cũng bị xóa | Giữ lại kiểu khi biên dịch |
Có thể dùng T::class.java? |
Không | Không | Có thể |
| Có thể phân tích cú pháp JSON trực tiếp? | Cần truyền Class<t> |
Cũng cần | Không cần, tự nhận diện |
| Cách biên dịch | Biên dịch thông thường | Biên dịch thông thường | Mở rộng inline, giữ lại thông tin kiểu |
Tóm tắt ngắn gọn:
Thông tin generic trong Java bị "mất" khi chạy;
reifiedgeneric trong Kotlin có thể "nhớ" kiểu của mình khi chạy.
Ví dụ so sánh
Java:
User user = parse(json, User.class);
Kotlin không có reified:
val user = parse(json, User::class.java)
Kotlin có reified:
val user = parse<User>(json)
✅ Đơn giản, trực quan, ít tham số hơn.