Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 2 — Konu 9: Null Safety Temelleri (`?`, `!`, `late`'e Giriş)

4 dk okuma #dart

Bölüm 2'nin son konusundayız — ve bu, Dart'ın en önemli özelliklerinden birine giriş: sound null safety. Bu konu, Dart'ın diğer birçok dile göre öne çıktığı en kritik noktalardan biri, o yüzden dikkatlice ilerleyelim.

"Null Reference" Sorunu Nedir?

Programlamanın en yaygın hatalarından biri, olmayan (null) bir değer üzerinde işlem yapmaya çalışmaktır. Java, JavaScript gibi birçok dilde şuna benzer bir hata sıkça karşına çıkar:

metin
NullPointerException: Cannot invoke method on null object

Bu hatalar genelde çalışma zamanında, hiç beklenmedik bir anda (genelde üretimde, kullanıcı karşısında) ortaya çıkar. Bu o kadar yaygın bir problemdir ki, bunu icat eden bilgisayar bilimci Tony Hoare bunu "bir milyar dolarlık hatam" olarak tanımlamıştır.

Dart, 2.12 sürümünden itibaren (2021) bu problemi derleme zamanında çözen bir sistem getirdi: sound null safety.

Varsayılan Olarak Hiçbir Şey Null Olamaz

Dart'ta, null safety ile birlikte, bir değişken varsayılan olarak null değer alamaz:

dart
void main() {
  String isim = "Ahmet";
  isim = null; // ❌ HATA! String tipi null kabul etmez
}

Bu kod derlenmez. String isim yazdığında, Dart bunu "bu değişken kesinlikle bir String tutacak, asla null olmayacak" olarak yorumlar. Bu, birçok hatayı daha kod çalışmadan yakalamanı sağlar.

? — Bir Tipin Null Olabileceğini Belirtmek

Eğer gerçekten bir değişkenin null olabilmesini istiyorsan, tipin sonuna ? eklersin:

dart
void main() {
  String? isim = "Ahmet"; // isim, String VEYA null olabilir
  isim = null; // ✅ şimdi geçerli
  print(isim); // null
}

String? ile String birbirinden tamamen farklı tiplerdir aslında — String?, "ya String ya da null" anlamına gelir. Bu, Dart'ın tip sisteminde Null tipinin de Object? hiyerarşisine dahil edilmesiyle mümkün olur (bir önceki derste gördüğümüz tip ağacını hatırla).

Nullable Bir Değişkenle Çalışırken Dikkat Edilmesi Gerekenler

dart
void main() {
  String? isim = "Ahmet";
  print(isim.length); // ❌ HATA! isim null olabilir, doğrudan .length çağıramazsın
}

Dart, isim değişkeninin potansiyel olarak null olabileceğini bildiği için, üzerinde doğrudan metod/özellik çağırmana izin vermez — çünkü eğer gerçekten null olsaydı, bu çalışma zamanında çökerdi. Dart bunu, çalışmadan önce sana bildiriyor.

Bunu güvenli şekilde kullanmanın birkaç yolu var:

1. Null kontrolü (if) ile

dart
void main() {
  String? isim = "Ahmet";

  if (isim != null) {
    print(isim.length); // ✅ artık isim, bu blok içinde String (null değil) olarak biliniyor
  }
}

Bu, Dart'ın type promotion (tip daraltma) özelliğidir — if (isim != null) kontrolünden sonra, o blok içinde Dart, isim'in artık kesinlikle String (null olmayan) olduğunu bilir.

2. ?. — Null-aware erişim operatörü

dart
void main() {
  String? isim = "Ahmet";
  print(isim?.length); // isim null değilse .length'i çağırır, null ise sonucu null olur

  isim = null;
  print(isim?.length); // null — hata vermez, sadece null döner
}

?., "eğer bu değer null değilse şu işlemi yap, null ise tüm ifadeyi null olarak değerlendir" anlamına gelir. Bu, çok satırlı if kontrolleri yazmak yerine tek satırda güvenli erişim sağlar.

3. ?? — Varsayılan değer belirleme

dart
void main() {
  String? isim;
  print(isim ?? "İsimsiz"); // isim null olduğu için "İsimsiz" yazdırılır

  isim = "Ahmet";
  print(isim ?? "İsimsiz"); // isim null değil, "Ahmet" yazdırılır
}

??, sol taraftaki değer null ise sağ taraftaki değeri kullanır. Bu, bir önceki dersimizde (Bölüm 3'te göreceğiz aslında, burada önizleme) operatörler konusunda daha detaylı işlenecek ama null safety ile doğrudan bağlantılı olduğu için burada da bahsetmek gerekiyordu.

4. ! — "Bana Güven, Bu Null Değil" (Null Assertion)

dart
void main() {
  String? isim = "Ahmet";
  print(isim!.length); // "isim'in null olmadığından eminim" diyorsun
}

! operatörü (null assertion / bang operator), Dart'a "bu değerin null olmadığından eminim, kontrolü atla" demektir. Bu tehlikeli bir operatördür — eğer yanılıyorsan (değer gerçekten null ise), program çalışma zamanında çöker:

dart
void main() {
  String? isim;
  print(isim!.length); // ❌ RUNTIME HATASI! isim gerçekten null, ! seni kurtaramaz
}

Genel kural: ! operatöründen mümkün olduğunca kaçın. Bunu kullanmak, aslında null safety'nin sana sağladığı güvenlik ağını manuel olarak devre dışı bırakmak demektir. ! kullanman gerektiğini düşündüğün her yerde, önce "gerçekten bu değerin null olamayacağından %100 emin miyim, yoksa ?. veya ?? ile daha güvenli bir yol var mı" diye sor.

late — "Şimdi Değil Ama Kesinlikle Sonra Atanacak"

Bazen bir değişkeni tanımladığın anda değer atayamazsın, ama yine de o değişkenin non-nullable (null olmayan) bir tip olmasını istersin. İşte late burada devreye girer:

dart
class Kullanici {
  late String isim; // şimdi değer yok ama String (null değil) olacak

  void ismiAyarla(String yeniIsim) {
    isim = yeniIsim;
  }
}

late ile işaretlenen bir değişken, tanımlandığı anda değer almak zorunda değildir, ama kullanılmadan önce mutlaka bir değer atanmış olmalıdır. Eğer değer atanmadan kullanmaya çalışırsan:

dart
void main() {
  late String mesaj;
  print(mesaj); // ❌ RUNTIME HATASI! mesaj hiç atanmadı
}

Bu hata alınır: LateInitializationError.

late'in gerçek kullanım senaryolarını (özellikle sınıflarda constructor ile ilişkili kullanımını) Bölüm 6'da sınıflar konusuna geldiğimizde çok daha net göreceğiz — çünkü late'in en yaygın kullanım alanı, bir sınıfın alanının constructor'da değil, başka bir metodda (örneğin initState benzeri bir yerde, Flutter'da sıkça karşına çıkacak) atanmasıdır.

Özet Tablo — Null Safety Araçları

Araç Ne Yapar Risk Seviyesi
Tip? Değişkenin null olabileceğini belirtir Güvenli — açık ve dürüst
if (x != null) Null kontrolü yapar, type promotion sağlar Çok güvenli
?. Null ise işlemi atlar, sonucu null yapar Güvenli
?? Null ise varsayılan değer kullanır Güvenli
! "Null değil" diye zorla kabul ettirir ⚠️ Riskli — dikkatli kullan
late Geç atamaya izin verir ama non-nullable kalır Kullanım yerine göre değişir

Neden Bu Kadar Önemli?

Bu konuyu bu kadar detaylı işlememizin sebebi şu: null safety, Dart'ın günlük yazdığın hemen hemen her satırını etkiler. Flutter'da her widget, her API cevabı, her kullanıcı girdisi bu sistemle iç içe çalışacak. Bu temeli şimdi sağlam kurmak, ileride "neden bu hata çıkıyor" diye saatlerce debug etmeni engelleyecek.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Dart'ta varsayılan olarak hiçbir tip null olamaz; nullable yapmak için tipin sonuna ? eklenir.
  • ?.: null-aware erişim, null ise işlemi atlar.
  • ??: null ise varsayılan değer sağlar.
  • !: "bu null değil" diye zorlar — riskli, dikkatli kullanılmalı.
  • late: değişkeni hemen atamadan tanımlamana izin verir ama kullanılmadan önce mutlaka atanmış olmalıdır.

📝 Ödevler

  • [ ] Nullable bir String? değişken tanımla, if (x != null) ile kontrol ederek güvenli şekilde .length özelliğine eriş.
  • [ ] Aynı işlemi ?. operatörüyle tekrar yaz, iki yaklaşımı karşılaştır.
  • [ ] ?? operatörünü kullanarak, null olan bir kullanıcı adı yerine "Misafir" yazdıran bir örnek yaz.
  • [ ] ! operatörünü bilerek yanlış kullanarak (gerçekten null olan bir değişkende) bir RuntimeError oluştur, hatayı oku ve yorumla.
  • [ ] late ile bir değişken tanımla, değer atamadan önce kullanmayı dene, LateInitializationError hatasını gözlemle; sonra değer atayıp düzelt.

🏁 Bölüm 2 Tamamlandı!

Artık değişken tanımlama yollarını (var/final/const), temel tipleri, tip çıkarımını, dynamic/Object farkını ve null safety temellerini biliyorsun. Bu, Dart'ın en kritik temel taşlarıydı.

Sıradaki durak Bölüm 3 — Operatörler & Kontrol Akışı: aritmetik/mantıksal operatörler, if/else, switch, döngüler ile devam edeceğiz — burada gerçek anlamda "mantık kurmaya" başlayacağız.