Tipe extension

Extension types sebagai wrapper tanpa biaya yang diperkenalkan di Dart 3.3.

Daftar isi

Extension type adalah fitur Dart 3.3 yang membungkus sebuah tipe dengan antarmuka baru yang hanya berlaku saat kompilasi, tanpa membuat objek pembungkus saat program berjalan. Topik ini membahas deklarasi representasi (representation declaration) beserta anggotanya, konstruktor extension type, penyamaran tipe saat program berjalan, batasan penugasan antartipe, dan perbedaannya dengan extension biasa serta wrapper class. Setelah membacanya, disiplin tipe dapat diterapkan pada data kuantitatif tanpa biaya alokasi.

Deklarasi representasi dan anggota#

Extension type dideklarasikan dengan kata kunci extension type, nama tipenya, dan deklarasi representasi di dalam kurung, misalnya extension type Kilogram(double berat). Deklarasi representasi menetapkan tipe dasar yang dibungkus, yaitu double, beserta nama bidangnya, yaitu berat. Deklarasi yang sama memperkenalkan dua anggota secara implisit, yaitu getter berat yang mengembalikan nilai representasi dan konstruktor tanpa nama Kilogram(berat). Anggota lain dideklarasikan eksplisit di dalam tubuhnya, sebagaimana contoh berikut.

dart Contoh: deklarasi representasi dan anggota
extension type Kilogram(double berat) {
  double get gram => berat * 1000;
  String get deskripsi => '${berat.toStringAsFixed(1)} kg';
}

void main() {
  final benda = Kilogram(3.5);
  print('Deskripsi: ${benda.deskripsi}');
  print('Setara gram: ${benda.gram}');
  print('Bidang representasi: ${benda.berat}');
}
text Keluaran
Deskripsi: 3.5 kg
Setara gram: 3500.0
Bidang representasi: 3.5

Keluaran memperlihatkan bahwa benda.deskripsi dan benda.gram diakses seperti anggota kelas biasa, sedangkan benda.berat membuka nilai representasinya. Anggota yang tidak dideklarasikan tidak tersedia, sehingga operasi double seperti penjumlahan tidak muncul otomatis pada Kilogram kecuali dideklarasikan ulang. Anggota yang dapat dideklarasikan mencakup metode, getter, setter, dan operator, ditambah anggota statis, tetapi bukan bidang instansi baru di luar representasinya. Seleksi anggota ini terjadi sepenuhnya saat kompilasi, karena antarmuka extension type hanya berlaku pada ekspresi yang tipe statisnya extension type tersebut. Disiplin seperti ini berguna untuk tipe kuantitatif, misalnya membedakan kilogram dari gram agar penjumlahan lintas satuan tidak lolos tanpa sengaja.

Konstruktor extension type#

Deklarasi representasi sekaligus menjadi konstruktor tanpa nama, sehingga Kilogram(3.5) mengisi bidang representasi secara langsung. Konstruktor bernama dapat ditambahkan di dalam tubuh extension type, dan setiap konstruktor generatif wajib mengisi bidang representasi, baik melalui daftar inisialisasi maupun parameter formal. Contoh berikut membuat konstruktor bernama yang menghitung nilai akhir ujian sebelum mengisi skor.

dart Contoh: konstruktor bernama extension type
extension type NilaiUjian(int skor) {
  NilaiUjian.sesuaiKurva(int mentah, int kurva) : skor = mentah + kurva;
}

void main() {
  final hasil = NilaiUjian.sesuaiKurva(85, 3);
  print('Skor: ${hasil.skor}');
}
text Keluaran
Skor: 88

Konstruktor NilaiUjian.sesuaiKurva menghitung mentah + kurva saat program berjalan, sehingga logika pembentukan nilai tetap dapat dieksekusi. Perbedaannya dengan konstruktor kelas terletak pada hasilnya, karena pemanggilan konstruktor tidak mengalokasikan objek apa pun dan hasilnya tetap nilai int yang dibungkus secara statis. Konstruktor bernama boleh lebih dari satu, dan pemanggilannya menggunakan nama tipe diikuti titik serta nama konstruktor, sama seperti kelas biasa. Seluruh perhitungan pada daftar inisialisasi dievaluasi setiap kali konstruktor dipanggil, sehingga dua pemanggilan dengan masukan yang sama menghasilkan nilai representasi yang sama pula. Bidang representasi dapat diberi nama privat, misalnya extension type NilaiUjian(int _skor), apabila nilai representasinya tidak ingin terbuka di luar library. Konstruktor tanpa nama tetap dapat dipanggil untuk kasus sederhana, yaitu NilaiUjian(88), tanpa perhitungan tambahan.

Erasure saat kompilasi#

Extension type mengalami erasure (type erasure), yaitu pembuangan tipenya saat kompilasi, sehingga yang tersisa saat program berjalan hanyalah tipe representasinya. Nilai Meter(3) tidak memiliki objek pembungkus, dan program berjalan seolah-olah bekerja dengan nilai int biasa. Contoh berikut memeriksa tipe saat berjalan dan kesamaan identitas dua extension type.

dart Contoh: extension type tanpa objek pembungkus
extension type Meter(int nilai) {}

void main() {
  final jarak = Meter(3);
  print('Diprint langsung: $jarak');
  print('Tipe saat berjalan: ${jarak.runtimeType}');
  print('Dua Meter(3) identical: ${identical(jarak, Meter(3))}');

  Object? nilaiObjek = jarak;
  print('Ditampung sebagai Object?: $nilaiObjek');
}
text Keluaran
Diprint langsung: 3
Tipe saat berjalan: int
Dua Meter(3) identical: true
Ditampung sebagai Object?: 3

Properti runtimeType menjawab int, dan dua pembungkusan nilai 3 dianggap identical karena keduanya adalah nilai int yang sama tanpa alokasi. Perbandingan dengan wrapper class memperlihatkan bedanya secara langsung, karena kelas pembungkus benar-benar membuat objek baru pada setiap pemanggilan konstruktor.

dart Contoh: wrapper class mengalokasikan objek baru
extension type Meter(int nilai) {}

class MeterKelas {
  final int nilai;
  const MeterKelas(this.nilai);
}

void main() {
  print('Extension type, dua Meter(3) identical: ${identical(Meter(3), Meter(3))}');
  print('Wrapper class, dua MeterKelas(3) identical: ${identical(MeterKelas(3), MeterKelas(3))}');
}
text Keluaran
Extension type, dua Meter(3) identical: true
Wrapper class, dua MeterKelas(3) identical: false

Perbedaan alokasi ini berdampak pada kinerja untuk data dalam jumlah besar, karena sejuta pembungkusan extension type tidak membuat sejuta objek. Wrapper class tetap tepat apabila identitas objek memang diperlukan, misalnya untuk perubahan status yang dilacak per objek. Dengan demikian, pemilihan antara keduanya bergantung pada kebutuhan disiplin tipe, bukan pada kebiasaan menulis kelas pembungkus.

Tipe yang berdiri sendiri#

Extension type tidak berada dalam hubungan subtyping dengan tipe representasinya, dan penugasan di kedua arah ditolak compiler. Nilai Meter tidak dapat ditugaskan ke variabel int, dan nilai int tidak dapat ditugaskan ke variabel Meter, karena tipenya berdiri sendiri. Penugasan hanya diterima untuk tipe puncak seperti Object? dan dynamic, sedangkan akses ke nilai representasi dilakukan melalui bidangnya sendiri atau melalui cast as int yang diperiksa saat program berjalan. Blok berikut menuliskan penugasan yang ditolak, lalu menjalankan dart analyze.

bash
# buat berkas dengan penugasan extension type ke int, lalu jalankan dart analyze
rm -rf /tmp/uji/tipe-tipe && mkdir -p /tmp/uji/tipe-tipe
cat > /tmp/uji/tipe-tipe/tidak_cocok.dart <<'EOF'
extension type Meter(int nilai) {}

void main() {
  final jarak = Meter(3);
  int kembali = jarak;
  print(kembali);
}
EOF
cd /tmp/uji/tipe-tipe && dart analyze
text Keluaran
Analyzing tipe-tipe...

  error - tidak_cocok.dart:5:17 - A value of type 'Meter' can't be assigned to a variable of type 'int'. Try changing the type of the variable, or casting the right-hand type to 'int'. - invalid_assignment

1 issue found.

Kode error invalid_assignment menegaskan bahwa pemeriksaan disiplin tipe terjadi saat kompilasi, tepat sebelum program dijalankan. Perilaku ini sekaligus menjadi nilai utamanya, karena pencampuran Meter dengan int biasa tidak dapat lolos tanpa sengaja. Sifat berdiri sendiri ini berlaku dua arah, sehingga Meter tidak dapat digunakan di tempat int dituntut dan int tidak dapat digunakan di tempat Meter dituntut. Satu-satunya pengecualian adalah tipe puncak, karena setiap tipe dapat ditugaskan ke Object? tanpa pemeriksaan tambahan. Apabila konversi memang diperlukan, penulisannya dilakukan secara eksplisit melalui bidang jarak.nilai atau cast as int, sehingga setiap lintas satuan tercatat jelas pada kode.

Perbandingan dengan extension dan wrapper class#

Extension biasa menambahkan anggota pada tipe lain tanpa membuat tipe baru, sedangkan extension type menciptakan tipe baru yang antarmukanya berdiri sendiri. Wrapper class juga menciptakan tipe baru, tetapi dengan objek nyata saat program berjalan. Contoh berikut memperlihatkan extension biasa yang menambahkan anggota langsung pada int.

dart Contoh: extension biasa menambahkan anggota pada int
extension EkstraInt on int {
  int get kuadrat => this * this;
}

void main() {
  print(5.kuadrat);
}
text Keluaran
25

Setelah extension EkstraInt dideklarasikan, setiap nilai int memiliki anggota kuadrat, dan tipenya tetap int tanpa tipe baru. Tabel berikut merangkum perbedaan ketiga bentuk tersebut.

AspekExtensionExtension typeWrapper class
Tipe baruTidak dibuatDibuatDibuat
Objek saat berjalanTanpa objekTanpa objekSatu objek per nilai
AnggotaMenumpang tipe lainHanya yang dideklarasikanSemua anggota kelas
Cocok untukMenambah utilitasDisiplin antarmukaEnkapsulasi penuh

Menyatukan konversi dalam satu program#

Bagian-bagian di atas bekerja dengan baik dalam satu program. Contoh berikut membungkus suhu Celsius menjadi extension type, lalu menyediakan konversi kelvin dan fahrenheit beserta metode kenaikan suhu, tanpa satu pun objek pembungkus yang dibuat saat program berjalan.

dart satuan.dart
// Program konversi satuan suhu menggunakan extension type.
// Menunjukkan representasi, anggota, dan penggunaan tanpa wrapper.

extension type SuhuCelsius(double celsius) {
  double get kelvin => celsius + 273.15;

  double get fahrenheit => celsius * 9 / 5 + 32;

  SuhuCelsius naik(double kenaikan) => SuhuCelsius(celsius + kenaikan);
}

void main() {
  final ruangan = SuhuCelsius(26.0);
  print('Suhu ruangan: ${ruangan.celsius} °C');
  print('Setara kelvin: ${ruangan.kelvin}');
  print('Setara fahrenheit: ${ruangan.fahrenheit}');
  print('Setelah naik 2 °C: ${ruangan.naik(2).celsius} °C');
}
text Keluaran
Suhu ruangan: 26.0 °C
Setara kelvin: 299.15
Setara fahrenheit: 78.8
Setelah naik 2 °C: 28.0 °C

Nilai ruangan saat program berjalan hanyalah double 26.0, tetapi tipe statisnya menutup operasi double lain sampai dideklarasikan di dalam extension type. Metode naik mengembalikan extension type baru, sehingga rantai konversi tetap berada di dalam disiplin tipe yang sama. Konversi kelvin dan fahrenheit mengembalikan double biasa, karena keluaran konversi antarsatuan memang dinyatakan secara eksplisit.

Tipe kembalian setiap anggota ditulis secara eksplisit pula, sehingga pembaca mengetahui mana yang keluar dari disiplin tipe dan mana yang tetap di dalamnya. Bentuk seperti ini menempatkan seluruh aturan satuan di satu tempat, dan penggunanya tidak pernah menyentuh angka mentah tanpa melalui anggota yang dideklarasikan. Perluasan berikutnya dapat menambah satuan lain, misalnya SuhuKelvin, tanpa mengubah pemanggil yang sudah ada.

Ringkasan#

  • Deklarasi representasi menetapkan tipe dasar beserta getter dan konstruktor implisitnya, sedangkan anggota lain dideklarasikan eksplisit.
  • Konstruktor bernama wajib mengisi bidang representasi dan tetap dieksekusi saat program berjalan, tetapi tidak mengalokasikan objek.
  • Erasure membuat tipe extension hilang saat kompilasi, sehingga tipe saat berjalan adalah tipe representasinya dan identitasnya sama dengan nilai murni.
  • Extension type berdiri sendiri dari tipe representasinya, dapat menerima tipe puncak seperti Object?, dan berbeda dari extension biasa maupun wrapper class.

Referensi#