Note: see the link below for the English version of this article.

https://duongnt.com/type-erasure-reified

Type erasure và reified trong Kotlin

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 erasurereified 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".

A software developer from Vietnam and is currently living in Japan.

One Thought on “Type erasure và reified trong Kotlin”

Leave a Reply