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
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:
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:
- Setelah cek null di baris sebelumnya (walau compiler tidak mempromosikan, mis. properti class):
class A {
String? nama;
void sapa() {
if (nama != null) {
print('Halo ${nama!}'); // aman: baru dicek
}
}
}-
Inisialisasi dua tahap yang terkontrol (init dipanggil sekali di awal lifecycle).
-
Test:
expect(x!.length, 2)wajar karena test memang boleh crash saat asumsi salah.
Alternatif yang lebih aman
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
// 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
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 valuetidak memberi tahu variabel mana. Saat debugging, cari!di stack trace line tersebut. Linter punya aturanavoid-non-null-assertionuntuk 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>!'.
// 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
}