Switch eksaustif

Switch eksaustif dengan pencocokan pattern pada hierarki sealed.

Daftar isi

Switch pada Dart 3 tidak hanya mencocokkan nilai yang sama, tetapi juga mencocokkan pattern, dan sifat eksaustif (exhaustive) dapat diperiksa compiler saat kode dikompilasi. Topik ini membahas dua bentuk switch, yaitu switch pernyataan dan switch ekspresi, pencocokan pattern pada hierarki kelas sealed, serta pemeriksaan eksaustif yang menandai kasus yang hilang sebelum program dijalankan. Setelah membacanya, struktur pencocokan pada hierarki tipe dapat ditulis dengan jaminan penuh dari compiler.

Switch pernyataan dan switch ekspresi#

Switch pernyataan menjalankan aksi pada kasus yang cocok, sedangkan switch ekspresi menghasilkan nilai dari kasus yang cocok. Bentuk keduanya dapat dibandingkan langsung pada program berikut, yang memeriksa kode kelas dengan pernyataan lalu menetapkan kelompok dengan ekspresi.

dart Contoh: switch pernyataan dan switch ekspresi
void main() {
  final kode = 'B';

  switch (kode) {
    case 'A':
    case 'B':
      print('Kelas $kode masuk kelompok atas');
      break;
    default:
      print('Kelas $kode masuk kelompok lain');
  }

  final kelompok = switch (kode) {
    'A' => 'atas',
    'B' => 'atas',
    _ => 'lainnya',
  };
  print('Kelompok $kode: $kelompok');
}
text Keluaran
Kelas B masuk kelompok atas
Kelompok B: atas

Pada switch pernyataan, kasus yang dikosongkan jatuh ke kasus di bawahnya, sehingga 'A' dan 'B' berbagi isi yang sama. Isi kasus yang tidak kosong tidak pernah jatuh ke kasus berikutnya, sehingga break boleh ditulis maupun tidak. Pada switch ekspresi, setiap kasus dipisahkan tanda => dan keseluruhan bentuknya menghasilkan satu nilai, tanpa break dan tanpa baris aksi terpisah. Berbeda dari pernyataan, switch ekspresi selalu menuntut sifat eksaustif, sehingga cabang _ atau kasus yang mencakup semua nilai wajib ada.

Switch ekspresi dapat digunakan di mana pun sebuah ekspresi diperbolehkan, misalnya di sisi kanan penugasan, sebagai argumen fungsi, atau sebagai tubuh fungsi singkat dengan =>. Pemilihan bentuk mengikuti kebutuhan, karena switch pernyataan cocok untuk menjalankan beberapa aksi per kasus, sedangkan switch ekspresi menghasilkan satu nilai yang langsung digunakan.

Penjaga kasus dengan when#

Kasus switch dapat diperketat dengan penjaga kasus (when guard), yaitu syarat boolean yang ditulis setelah pattern dengan kata when. Kasus hanya terpilih apabila patternnya cocok dan penjaganya bernilai true, sehingga satu kasus dapat menyaring isi nilainya tanpa menambah cabang baru. Penjaga bekerja pada switch pernyataan maupun switch ekspresi, dan penempatannya tetap pada baris kasus yang sama sehingga alur pencocokan terbaca berurutan. Contoh berikut mengelompokkan pembacaan sensor suhu dengan dua penjaga, yaitu suhu di bawah nol dan suhu di atas tiga puluh.

dart Contoh: penjaga kasus when pada switch ekspresi
void main() {
  final pembacaan = [-2.5, 18.0, 35.5];
  for (final suhu in pembacaan) {
    final label = switch (suhu) {
      double s when s < 0 => 'beku',
      double s when s > 30 => 'panas',
      _ => 'normal',
    };
    print('Suhu ${suhu.toStringAsFixed(1)} °C: $label');
  }
}
text Keluaran
Suhu -2.5 °C: beku
Suhu 18.0 °C: normal
Suhu 35.5 °C: panas

Keluaran menunjukkan bahwa suhu 18.0 tidak masuk kasus kedua karena penjaganya gagal, sehingga nilai tersebut jatuh ke cabang wildcard. Penjaga diperiksa setelah pattern cocok, sehingga variabel s sudah terikat saat syaratnya dievaluasi. Bentuk yang sama dapat ditulis pada kasus switch pernyataan, misalnya case double s when s < 0:, sehingga penjagaannya konsisten di kedua bentuk switch. Apabila penjaga membuat baris kasus sulit dibaca, pemisahan logikanya ke dalam fungsi bantu biasanya lebih jelas daripada menyembunyikannya pada satu baris kasus.

Pencocokan pattern pada hierarki sealed#

Sifat eksaustif menjadi menonjol apabila subjek switch adalah hierarki kelas sealed. Kelas sealed hanya boleh diturunkan di library yang sama, sehingga compiler mengetahui seluruh subkelas yang mungkin ada dan dapat memeriksa kelengkapan kasusnya. Compiler tidak memiliki pengetahuan penuh ini untuk kelas biasa, karena kelas biasa dapat diturunkan dari berkas mana pun sehingga daftar subkelasnya selalu bisa bertambah. Contoh berikut menggunakan switch ekspresi dengan pattern objek untuk menghitung luas tiga bangun datar.

dart bentuk.dart
// Program luas bangun datar dengan hierarki sealed.
// Switch ekspresi wajib mencakup semua subkelas Bentuk.

import 'dart:math';

sealed class Bentuk {}

class Lingkaran extends Bentuk {
  final double radius;
  Lingkaran(this.radius);
}

class Persegi extends Bentuk {
  final double sisi;
  Persegi(this.sisi);
}

class Segitiga extends Bentuk {
  final double alas;
  final double tinggi;
  Segitiga(this.alas, this.tinggi);
}

double luas(Bentuk bentuk) => switch (bentuk) {
      Lingkaran(:final radius) => radius * radius * pi,
      Persegi(:final sisi) => sisi * sisi,
      Segitiga(:final alas, :final tinggi) => 0.5 * alas * tinggi,
    };

void main() {
  final kumpulan = [Lingkaran(2), Persegi(4), Segitiga(6, 3)];
  for (final bentuk in kumpulan) {
    print('Luas: ${luas(bentuk).toStringAsFixed(2)}');
  }
}
text Keluaran
Luas: 12.57
Luas: 16.00
Luas: 9.00

Setiap kasus menggunakan pattern objek dengan bentuk singkat bidang, misalnya Lingkaran(:final radius) yang mencocokkan subkelasnya sekaligus mengikat bidang radius menjadi variabel. Rumus per bangun berada tepat di samping kasusnya, sehingga logika pencocokan dan perhitungan terbaca dalam satu bentuk. Bentuk singkat tersebut dapat ditulis lengkap sebagai radius: final r apabila nama variabelnya ingin berbeda dari bidangnya. Apabila subkelas baru ditambahkan ke hierarki Bentuk, baris switch di atas langsung ditolak compiler sampai kasusnya ditulis, dan jaminan inilah yang dibahas pada bagian berikut.

Pemeriksaan eksaustif saat kompilasi#

Sifat eksaustif diperiksa compiler, bukan ditemukan saat program berjalan. Blok berikut membuat sebuah berkas yang hilang satu kasus, yaitu Segitiga, pada switch ekspresi dan switch pernyataan sekaligus, lalu menjalankan dart analyze untuk menampilkan pesan errornya.

bash
# buat berkas dengan satu kasus switch yang hilang, lalu jalankan dart analyze
rm -rf /tmp/uji/switch-hilang && mkdir -p /tmp/uji/switch-hilang
cat > /tmp/uji/switch-hilang/hilang.dart <<'EOF'
sealed class Bentuk {}

class Lingkaran extends Bentuk {
  final double radius;
  Lingkaran(this.radius);
}

class Persegi extends Bentuk {
  final double sisi;
  Persegi(this.sisi);
}

class Segitiga extends Bentuk {
  final double alas;
  final double tinggi;
  Segitiga(this.alas, this.tinggi);
}

String nama(Bentuk bentuk) => switch (bentuk) {
      Lingkaran(:final radius) => 'lingkaran berjari-jari $radius',
      Persegi(:final sisi) => 'persegi bersisi $sisi',
    };

String jenis(Bentuk bentuk) {
  switch (bentuk) {
    case Lingkaran():
      return 'lingkaran';
    case Persegi():
      return 'persegi';
  }
}

void main() {
  print(jenis(Persegi(4)));
}
EOF
cd /tmp/uji/switch-hilang && dart analyze
text Keluaran
Analyzing switch-hilang...

  error - hilang.dart:19:31 - The type 'Bentuk' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Segitiga()'. Try adding a wildcard pattern or cases that match 'Segitiga()'. - non_exhaustive_switch_expression
  error - hilang.dart:25:3 - The type 'Bentuk' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Segitiga()'. Try adding a default case or cases that match 'Segitiga()'. - non_exhaustive_switch_statement

2 issues found.

Pesan error menyebutkan pattern yang hilang beserta kasus yang dapat menutupinya, dan kedua kode errornya berbeda antara ekspresi dan pernyataan. Perbaikannya mengikuti saran analyzer, yaitu menambahkan case Segitiga(): pada pernyataan dan Segitiga(...) pada ekspresi, atau menambahkan cabang default. Dengan hierarki sealed, perubahan tipe yang menambah subkelas tidak pernah lolos tanpa terdeteksi, karena compiler menolaknya sampai kasusnya ditulis.

Pemeriksaan yang sama berlaku pada switch pernyataan, yaitu kasus yang hilang wajib ditutup dengan default atau dengan kasus tambahan sebelum analisis berakhir tanpa temuan. Analyzer menilai kedua bentuk secara terpisah, sehingga satu kasus yang hilang pada salah satunya tetap dilaporkan meskipun bentuk lainnya sudah lengkap. Pada switch ekspresi, cabang penutup ditulis dengan wildcard _, sedangkan switch pernyataan menggunakan default, dan keduanya menutup pemeriksaan karena mencakup nilai mana pun.

Ringkasan#

  • Switch pernyataan menjalankan aksi per kasus dengan kasus kosong yang jatuh ke bawah, sedangkan switch ekspresi menghasilkan nilai melalui tanda =>.
  • Kelas sealed membatasi subkelas pada satu library, sehingga compiler mengetahui seluruh subkelas yang mungkin dicocokkan.
  • Pattern objek seperti Lingkaran(:final radius) mencocokkan subkelas sekaligus mengikat bidangnya menjadi variabel.
  • Sifat eksaustif diperiksa saat kompilasi, dan kasus yang hilang dilaporkan sebagai error non_exhaustive_switch_expression atau non_exhaustive_switch_statement.

Referensi#