Note: see the link below for the English version of this article.
https://duongnt.com/type-erasure-reified
Generic là một tính năng thường gặp trong những ngôn ngữ lập trình hiện đại. Vì Kotlin là một ngôn ngữ tương đối mới nên nó cũng hỗ trợ generic typing. Tuy nhiên, vì tôi đã quen với C# .NET nên cách Kotlin xử lý type information tại runtime làm tôi đôi chút ngạc nhiên. Hôm nay, chúng ta sẽ tìm hiểu về type erasure và reified trong Kotlin.
Type erasure là gì?
C# cho phép ta sử dụng type information tại runtime
Trong C#, ta có thể tạo Type
instance của generic type bằng từ khoá typeof
. Dưới đây là một hàm generic đơn giản để in ra tên của kiểu sử dụng trong argument.
public string getInfo<T>(T input) =>
$"Type of <{input}> is: {typeof(T).AssemblyQualifiedName}" ?? "Cannot get type name";
Hàm này hoạt động đúng như dự tính.
Console.WriteLine(getInfo(17));
// Type of <1> is: System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
Console.WriteLine("This is a text");
// Type of <This is a text> is: System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
Nhưng trong Kotlin ta không thể sử dụng type information tại runtime
Hàm Kotlin dưới đây sẽ gặp lỗi khi compile.
fun <T> getInfo(input: T): String =
"The type of <$input> is: ${T::class.qualifiedName}"
Thông báo lỗi là như sau.
Cannot use 'T' as reified type parameter. Use a class instead.
Type erasure trong Kotlin
Cũng giống như Java, Kotlin cũng thực hiện type erasure. Có nghĩa là tại runtime, thông tin về type argument của hàm generic sẽ không được bảo tồn. Đó là nguyên nhân compiler sinh ra lỗi ở trên. Tại runtime, JVM không còn thông tin gì về kiểu T
. Vì thế nó không thể tạo instance KClass
, và cũng không thể lấy tên của T
.
Ta sẽ thay đổi getInfo
và xoá phần code sử dụng type information.
fun <T> getInfo2(input: T): String = "Cannot get type name"
Giờ đây ta có thể compile và gọi hàm trên mà không gặp lỗi.
getInfo2(17) // Kết quả là "Cannot get type name"
Hãy thử xem qua bytecode của hàm getInfo
(chọn Tools -> Kotlin -> Show Kotlin bytecode trong IntelliJ). Nếu các bạn tự chạy thí nghiệm này thì lưu ý là số dòng của các bạn có thể khác của tôi.
public final static getInfo2(Ljava/lang/Object;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;()
L0
LINENUMBER 184 L0
LDC "Cannot get type name"
ARETURN
L1
LOCALVARIABLE input Ljava/lang/Object; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
Như ta thấy, kiểu của input
trong bytecode chỉ đơn giản là Ljava/lang/Object
. JVM không hề biết rằng kiểu của ta thực ra là integer. Và tại chỗ ta gọi hàm, ta chỉ invoke hàm getInfo2
như dưới đây.
L0
LINENUMBER 15 L0
BIPUSH 17
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKESTATIC org/hellokotlin/MainKt.getInfo2 (Ljava/lang/Object;)Ljava/lang/String; // <-- Invoke getInfo2
POP
Giữ type information bằng từ khoá reified
Hàm inline với kiểu generic đã được reified
Có cách nào để sử dụng được type information tại runtime hay không? Thực ra là có, ta có thể thực hiện việc này bằng từ khoá reified
bên trong hàm inline. Hàm dưới đây không bị lỗi khi compile.
inline fun <reified T> getInfoFix(input: T): String =
"The type of <$input> is: ${T::class.qualifiedName}"
Và nó chạy đúng như ta mong đợi.
println(getInfoFix(17))
// The type of <17> is: kotlin.Int
Reified có tác dụng gì?
Hãy thử xem bytecode của hàm getInfoFix
(như đã nói, số line number của bạn có thể khác của tôi)
public final static synthetic getInfoFix(Ljava/lang/Object;)Ljava/lang/String;
L0
LDC 0
ISTORE 1
L1
LINENUMBER 181 L1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "The type of <"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC "> is: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ICONST_4
LDC "T"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.reifiedOperationMarker (ILjava/lang/String;)V
LDC Ljava/lang/Object;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE input Ljava/lang/Object; L0 L2 0
LOCALVARIABLE $i$f$getInfo I L1 L2 1
MAXSTACK = 3
MAXLOCALS = 2
Đoạn byte code này tương đối dài. Nhưng phần thú vị là ở đây, nơi ta lấy tên lớp của T
.
LDC Ljava/lang/Object;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
Khi thoạt nhìn qua thì có vẻ ta vẫn dùng Ljava/lang/Object
như trong bản không dùng reified. Nhưng nhớ là hàm của ta còn được inline nữa.
Inline hàm generic có dùng từ khoá reified
Đây là bytecode để gọi hàm getInfoFix
.
L0
LINENUMBER 15 L0
BIPUSH 17
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 0
L1
ICONST_0
ISTORE 1
L2
LINENUMBER 704 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "The type of <"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC "> is: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC Ljava/lang/Integer;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
L3
POP
Vì hàm của ta đã được inline nên thay vì invoke getInfoFix
, bytecode của nó được chèn thẳng vào chỗ gọi hàm. Và tại chỗ gọi hàm ta biết kiểu của T
là gì. Những dòng code trong phần trước bây giờ trở thành như sau.
LDC Ljava/lang/Integer;.class
INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
INVOKEINTERFACE kotlin/reflect/KClass.getQualifiedName ()Ljava/lang/String; (itf)
Rõ ràng là ta đã tải Ljava/lang/Integer;.class
lên trên stack. Và đây là lý do tại sao getInfoFix
có thể sử dụng được type information tại runtime.
Kết thúc
Trong khi làm quen với Kotlin/Java, tôi dùng tới kinh nghiệm với C# .NET của mình. Không may là những ngôn ngữ này có những khác biệt khó nhận ra. Đôi khi chúng khiến tôi gặp khó khăn. Nhưng mặt khác, đây cũng là dịp tốt để tôi tìm hiểu sâu hơn về ngôn ngữ mới.
Trong phần lớn các trường hợp, tôi coi việc cần sử dụng tới type information tại runtime là dấu hiệu của code có vấn đề. Tuy nhiên khi thực sự cần thiết thì tôi sẽ inline hàm và sử dụng từ khoá reified
. Bởi vì như người ta đã nói, "only a Sith deals in absolutes".
One Thought on “Type erasure và reified trong Kotlin”