Generik pada Dart

Jika Anda membaca dokumentasi API tipe array dasar List, Anda akan mengetahui tipe aslinya adalah List<E>. Simbol kurung sudut <...> menandakan tipe generik (tipe berparameter), yaitu tipe yang memiliki parameter tipe formal. Berdasarkan konvensi, hampir semua variabel tipe diberi nama satu huruf saja, misalnya E, T, S, K, V.

Mengapa menggunakan generik?

  • Fungsi utama generik adalah menjamin keamanan tipe
  • Penentuan generik yang benar dapat menghasilkan kode runtime yang lebih optimal;
  • Penggunaan generik dapat mengurangi duplikasi kode secara drastis.

Jika Anda ingin sebuah daftar hanya menyimpan teks, deklarasikan daftar tersebut sebagai List<String>. Ini disebut sebagai daftar string.

Setelah didefinisikan sebagai daftar khusus string, upaya memasukkan nilai tipe lain akan menimbulkan kesalahan kompilasi.

// Kesalahan pemeriksaan tipe statis
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Kesalahan kompilasi: 42 bukan tipe stringCode language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // Kesalahan kompilasi: 42 bukan tipe stringCode language: PHP (php)

Mengurangi redundansi kode

Misalkan Anda memerlukan antarmuka abstrak untuk cache objek:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)

Kemudian jika Anda membutuhkan cache yang hanya menyimpan string, Anda harus membuat antarmuka baru secara terpisah:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Code language: Dart (dart)

Setelah itu Anda akan memerlukan cache khusus untuk angka, sehingga Anda harus menulis ulang kode yang hampir sama berulang kali.

Contohnya, Anda akan menulis kode berikut:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code language: Dart (dart)

Generik menyelesaikan masalah ini dengan satu solusi saja: Anda hanya perlu mendefinisikan satu antarmuka universal yang dilengkapi parameter tipe:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Code language: Dart (dart)

Di sini simbol T adalah tempat penampung tipe, yang mewakili tipe konkret apa pun yang dikirimkan oleh pengembang di kemudian hari.

Contoh penerapan nyata

// Kelas dasar abstrak cache generik
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// Implementasi cache di memori (berbasis Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @Override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @Override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. Contoh cache string
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. Cache bilangan bulat int
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. Cache objek buatan sendiri
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // Uji fungsi penghapusan
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// Kelas entitas buatan sendiri
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code language: Dart (dart)

Menggunakan literal koleksi generik

Literal List, Set dan Map semuanya mendukung generik berparameter:

  • List / Set: tuliskan <Tipe> sebelum kurung siku buka
  • Map: tuliskan <TipeKunci, TipeNilai> sebelum kurung kurawal buka

Contoh kode:

// Daftar string
var names = <String>['Seth', 'Kathy', 'Lars'];

// Himpunan string (menghapus duplikat secara otomatis)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// Kamus dengan kunci dan nilai bertipe string
var pages = <String, String>{
  'index.html': 'Halaman utama',
  'robots.txt': 'Berkas petunjuk untuk crawler mesin pencari',
  'humans.txt': 'Berkas penjelasan untuk pengembang manusia',
};
Code language: PHP (php)

Menjalankan contoh kode

void main() {
  // 1. List<String> Daftar string: terurut, mengizinkan duplikat, akses melalui indeks
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List Daftar ===");
  print(names); // [Seth, Kathy, Lars, Seth] duplikat diperbolehkan
  print(names[0]); // Mengambil elemen pertama melalui indeks: Seth
  names.add("Mike"); // Menambahkan elemen di bagian akhir
  print("Setelah penambahan: $names");

  // 2. Set<String> Himpunan string: tidak terurut, menghapus duplikat otomatis, tidak ada indeks
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set Himpunan (penghapusan duplikat otomatis) ===");
  print(uniqueNames); // {Seth, Kathy, Lars} duplikat Seth dihapus secara otomatis
  print(uniqueNames.contains("Kathy")); // Pemeriksaan keberadaan: true
  uniqueNames.remove("Lars");
  print("Setelah menghapus Lars: $uniqueNames");

  // 3. Map<String, String> Kamus kunci-nilai: kunci bersifat unik, akses melalui kunci
  var pages = <String, String>{
    'index.html': 'Halaman utama',
    'robots.txt': 'Berkas petunjuk untuk crawler mesin pencari',
    'humans.txt': 'Berkas penjelasan untuk pengembang manusia',
  };
  print("\n=== Map Kamus ===");
  print(pages);
  print(pages['index.html']); // Mengambil nilai melalui kunci: Halaman utama
  pages["about.html"] = "Halaman tentang"; // Menambahkan pasangan kunci-nilai baru
  print("Setelah menambahkan about.html: $pages");

  // Contoh perulangan yang sering digunakan
  print("\n=== Perulangan List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Perulangan semua entri Map ===");
  for (var entry in pages.entries) {
    print("Berkas: ${entry.key} → Penjelasan: ${entry.value}");
  }
}Code language: Dart (dart)

Menggunakan parameter generik pada konstruktor

Saat memanggil konstruktor, tentukan tipe generik di dalam kurung sudut setelah nama kelas:

var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)

Kode berikut membuat kamus dengan tipe kunci int dan tipe nilai View:

var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("Daftar asli: $names");
  print("Set tanpa duplikat: $nameSet");

  // SplayTreeMap memerlukan import dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "Halaman pengaturan";
  views[2] = "Halaman utama";
  views[9] = "Halaman saya";
  views[1] = "Layar peluncuran";

  print("\nTreeMap yang terurut:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code language: PHP (php)

Tipe runtime koleksi generik

Reifikasi tipe (reified)

Generik pada Dart bersifat reified: seluruh informasi tipe generik akan tersimpan secara utuh saat program berjalan.

Anda dapat memeriksa tipe generik konkret dari sebuah koleksi secara langsung saat runtime:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // Menampilkan nilai true
}Code language: PHP (php)

Perbandingan tambahan: Generik Java menggunakan sistem penghapusan tipe, semua parameter generik akan dihapus saat runtime. Di Java kita hanya bisa mengecek apakah objek adalah List, tidak bisa membedakan antara List<String> dan List<int>.

* Membatasi tipe parameter generik

Batas tipe (bound). Jika Anda belum mempelajari materi kelas, cukup pahami konsep umum bagian ini saja.

Saat mendefinisikan generik, Anda bisa membatasi tipe yang dikirimkan agar hanya berupa subkelas dari superkelas tertentu. Batasan ini dinamakan batas tipe dan diimplementasikan menggunakan kata kunci extends.

1 Membatasi ke tipe non-null

Memaksa generik hanya menerima tipe yang tidak bernilai null (batas atas bawaan adalah tipe nullable Object?):

class Foo<T extends Object> {
  // Tipe T yang dikirimkan tidak boleh bersifat nullable
}
Code language: JavaScript (javascript)

2 Membatasi ke subkelas dari superkelas tertentu

Gunakan kata kunci extends untuk menentukan kelas dasar, sehingga Anda bisa memanggil semua metode anggota kelas dasar secara langsung:

class Foo<T extends SomeBaseClass> {
  String toString() => "Instansi Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)

Argumen tipe yang valid: superkelas itu sendiri, atau semua subkelasnya

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)

Jika parameter generik tidak dituliskan, secara bawaan akan menggunakan tipe batas:

var foo = Foo();
print(foo); // Menampilkan: Instansi Foo<SomeBaseClass>
Code language: PHP (php)

Mengirimkan tipe yang tidak memiliki hubungan pewarisan akan menimbulkan kesalahan kompilasi statis:

var foo = Foo<Object>(); // Kesalahan kompilasi: Object bukan subkelas dari SomeBaseClass
Code language: JavaScript (javascript)

* Batas tipe referensi diri (generik F-bound)

Saat memasang batasan pada generik, batas dapat mereferensikan parameter tipenya sendiri untuk membuat batasan mandiri, hal ini dikenal sebagai F-bound.

Contoh standar: Antarmuka yang dapat dibandingkan Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T wajib mengimplementasikan Comparable<T>, hanya bisa dibandingkan dengan objek tipe yang sama
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // Implementasi logika perbandingan
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code language: PHP (php)

Batasan T extends Comparable<T> memiliki arti: Tipe T hanya dapat dibandingkan dengan instansi tipe yang sama dengan dirinya sendiri.

* Metode generik

Fungsi biasa dan metode kelas juga mendukung parameter tipe generik:

T first<T>(List<T> ts) {
  // Logika pemeriksaan awal
  T tmp = ts[0];
  // Logika pemrosesan lanjutan
  return tmp;
}
Code language: PHP (php)

Contoh lengkap

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("Daftar tidak boleh kosong");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code language: Dart (dart)

Parameter generik <T> berpengaruh pada tiga bagian:

  1. Tipe kembalian fungsi T
  2. Tipe parameter masukan List<T>
  3. Tipe variabel lokal T tmp

Previous:

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *