dartnull-safetyoperatorMahir3 mnt baca

Operator Null Assertion (!)

Kapan ! aman dipakai, kapan berbahaya, dan alternatifnya.

Pedang bermata dua

Operator ! (null assertion) adalah caramu berkata ke compiler: "aku JAMIN ini tidak null, percayalah". Compiler percaya, tapi jika jaminanmu salah, aplikasi crash dengan Null check operator used on a null value.

Analogi: ! itu seperti menandatangani surat pernyataan. Cepat dan praktis, tapi kalau isinya bohong, kamu yang tanggung akibatnya.

Kenapa ! ada?

Karena compiler tidak maha tahu. Ada situasi di mana kamu tahu nilai tidak null tapi compiler tidak bisa membuktikannya: setelah validasi di fungsi lain, data dari platform channel, atau inisialisasi dua tahap. ! menjembatani kesenjangan itu.

Bentuk dasar

dart
void main() {
  String? nama = 'Budi';

  // Tanpa !: error compile karena length butuh String
  // print(nama.length);

  // Dengan !: "aku jamin tidak null"
  print(nama!.length); // 4

  String? kosong;
  // print(kosong!.length); // CRASH runtime!
}

nama! mengubah String? menjadi String secara paksa. Tidak ada pengecekan runtime "apakah benar tidak null" sebelum crash; crash-nya justru terjadi karena dipakai saat null.

Contoh kedua, pola yang umum tapi berisiko:

dart
class Form {
  String? email;

  void validasi() {
    if (email == null || !email!.contains('@')) {
      throw ArgumentError('Email tidak valid');
    }
  }

  void kirim() {
    validasi(); // setelah ini email pasti tidak null...
    print('Mengirim ke ${email!}'); // ...tapi compiler tidak tahu
  }
}

void main() {
  var f = Form()..email = '[email protected]';
  f.kirim();
}

Manusia tahu email tidak null setelah validasi(), tapi compiler tidak mengikuti alur antar-method. ! diperlukan di sini, walau idealnya desainnya diperbaiki (lihat alternatif di bawah).

Kapan ! boleh dipakai

Aturan praktis, dari paling aman ke paling berisiko:

  1. Setelah cek null di baris sebelumnya (walau compiler tidak mempromosikan, mis. properti class):
dart
class A {
  String? nama;
  void sapa() {
    if (nama != null) {
      print('Halo ${nama!}'); // aman: baru dicek
    }
  }
}
  1. Inisialisasi dua tahap yang terkontrol (init dipanggil sekali di awal lifecycle).

  2. Test: expect(x!.length, 2) wajar karena test memang boleh crash saat asumsi salah.

Alternatif yang lebih aman

dart
void main() {
  String? nama;

  // 1. Local variable promotion (paling disukai)
  var n = nama;
  if (n != null) {
    print(n.length); // tanpa !
  }

  // 2. ?? dengan default
  print(nama?.length ?? 0);

  // 3. Early return / throw
  String panjang(String? s) {
    if (s == null) throw ArgumentError('wajib ada');
    return 'Panjang: ${s.length}'; // s terpromosi, tanpa !
  }

  // 4. late untuk inisialisasi tertunda yang pasti
  // late String token; // janji diisi sebelum dipakai
}

Trik variabel lokal (nomor 1) bekerja karena compiler bisa mempromosikan variabel lokal, tapi tidak properti class (bisa berubah dari mana saja).

Kesalahan umum

1. ! sebagai kebiasaan, bukan pilihan sadar

dart
// BURUK: ! di mana-mana menandakan desain nullable yang salah
void proses(String? a, String? b, int? c) {
  // print(a!.length + b!.length + c!);
}

Jika semua parameter "dijamin tidak null", kenapa nullable? Perbaiki tipenya, bukan tambahkan !.

2. ! pada hasil yang memang bisa null

dart
void main() {
  var kamus = {'a': 1};
  // print(kamus['b']! + 10); // CRASH: memang tidak ada
  print((kamus['b'] ?? 0) + 10); // BENAR
}

3. ! berantai

a!.b!.c! = tiga peluang crash. Tiap ! harus bisa kamu justifikasi satu per satu.

Kesimpulan

! = jaminan manual ke compiler, berguna saat compiler tidak bisa membuktikan tapi kamu bisa. Pakai hemat, hanya setelah cek atau di konteks terkontrol. Utamakan alternatif: promotion, ??, early throw, atau perbaiki desain tipenya.

Catatan teknis: Pesan error Null check operator used on a null value tidak memberi tahu variabel mana. Saat debugging, cari ! di stack trace line tersebut. Linter punya aturan avoid-non-null-assertion untuk melarang ! di codebase yang ketat.

Tantangan

Amankan tanda seru

Fungsi sapa(nama) di bawah memakai ! secara berbahaya. Tulis ulang TANPA ! memakai early-throw: jika null/empty, throw ArgumentError; jika valid, kembalikan 'Halo, <nama>!'.

text
// BERBAHAYA, jangan tiru:
String sapaBahaya(String? nama) => 'Halo, ${nama!}';

// TODO: tulis ulang tanpa !
String sapa(String? nama) {
  return '';
}

void main() {
  print(sapa('Budi'));
  // print(sapa(null)); // harus throw
}