İçeriğe geç
Yazılım ve Teknoloji

Nullable reference types ile null hatalarına son

Rıfat Akarca 👁️ 7 okunma 📅 ⏱️ 6 dk okuma
İçindekiler

Üretimde en sık görülen hatalardan biri hâlâ NullReferenceException. Tony Hoare'un "milyar dolarlık hata" dediği null referans, C# 8'e kadar dilin hiçbir şekilde yardım etmediği bir alandı. Nullable reference types (NRT) bu durumu değiştirdi: bir referansın null olup olamayacağı artık tipin parçası ve derleyici bunu sizin yerinize takip ediyor.

Bu yazıda NRT'nin nasıl çalıştığını, en sık görülen uyarıların nasıl çözüleceğini, ! operatörünün neden son çare olması gerektiğini ve mevcut bir projeye kademeli geçişin nasıl yapılacağını göreceksiniz.

Nullable reference types nasıl çalışır?

NRT'nin fikri basit: string artık "asla null olmayacak string", string? ise "null olabilecek string" demektir. Derleyici veri akışını analiz eder ve null olabilecek bir değere kontrol yapmadan eriştiğinizde uyarı üretir.

Özellik C# 8'de geldi ve proje bazında Nullable ayarıyla açılır. .NET 6 ve sonrasındaki proje şablonlarında varsayılan olarak açıktır; eski projelerde elle eklemeniz gerekir.

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <Nullable>enable</Nullable>
</PropertyGroup>

Şimdi iki satırlık bir örneğe bakalım:

string title = null;
string? subtitle = null;
Console.WriteLine(subtitle.Length);

Birinci satır CS8600 uyarısı verir, çünkü non-nullable bir değişkene null atanıyor. Üçüncü satır CS8602 verir, çünkü subtitle null olabilir ve kontrol edilmeden dereference ediliyor.

Burada kritik nokta şu: NRT bir derleme zamanı analizidir. Çalışma zamanında string ile string? aynı tiptir; ek bir kontrol ya da performans maliyeti yoktur. Ayrıca bunlar varsayılan olarak hata değil uyarıdır. Uyarıları görmezden gelirseniz kod yine derlenir ve yine patlar.

En sık karşılaşılan uyarılar ve çözümleri

UyarıAnlamıTipik çözüm
CS8600Null olabilecek değer non-nullable değişkene atanıyorDeğişkeni ? yapın ya da değeri önce kontrol edin
CS8602Null olabilecek referans dereference ediliyorNull kontrolü ekleyin
CS8603Null olabilecek değer döndürülüyorDönüş tipini ? yapın ya da null durumunu ele alın
CS8604Null olabilecek argüman non-nullable parametreye veriliyorÇağıran tarafta kontrol edin
CS8618Non-nullable alan/özellik constructor sonunda atanmamışrequired, constructor parametresi ya da ?

CS8618: başlatılmamış özellikler

Bu uyarı, NRT'yi ilk açtığınızda en çok göreceğiniz uyarıdır. Yanlış refleks, özelliği = null! ile susturmaktır. Doğrusu, "bu değer zorunlu mu, değil mi?" sorusunu cevaplamaktır.

public class Customer
{
    public string Name { get; set; }
    public string? Phone { get; set; }
}

C# 11'den itibaren required anahtar kelimesiyle zorunluluğu nesne oluşturma anında dayatabilirsiniz:

public class Customer
{
    public required string Name { get; init; }
    public string? Phone { get; init; }
}

Artık new Customer() yazıp Name vermezseniz derleme hatası alırsınız. Değişmez veri taşıyan tipler için record ve primary constructor da aynı güvenceyi daha kısa bir yazımla sağlar.

CS8602: kontrolsüz erişim

Geriye null dönebilen bir metodu imzasında dürüstçe belirtmek, NRT'nin en değerli kullanımıdır:

public User? FindByEmail(string email) =>
    _users.FirstOrDefault(u => u.Email == email);

public IResult GetName(string email)
{
    var user = FindByEmail(email);
    return Results.Ok(user.Name);
}

Dönüş tipi User? olduğu için derleyici user.Name satırında uyarı verir. Çözüm, null durumunu açıkça ele almaktır:

public IResult GetName(string email)
{
    var user = FindByEmail(email);
    if (user is null)
    {
        return Results.NotFound();
    }

    return Results.Ok(user.Name);
}

if bloğundan sonra derleyici user'ın null olmadığını bilir. Buna flow analysis denir ve ekstra bir cast ya da değişken gerektirmez.

Null kontrolü yazmanın doğru yolları

NRT açıkken elinizdeki araçları bilmek uyarıları gürültü olmaktan çıkarır:

  • is null / is not null: Null kontrolü için tercih edilen yazım. == operatörü aşırı yüklenebilir, is ise yüklenemez. Daha geniş bir bakış için pattern matching yazısına göz atabilirsiniz.
  • ?. (null-conditional): Zincirin herhangi bir halkası null ise sonuç null olur.
  • ?? ve ??=: Null ise varsayılan değer kullanır ya da atama yapar.
  • ArgumentNullException.ThrowIfNull: .NET 6'dan itibaren parametre doğrulaması için standart yoldur.
public void Send(string recipient, User? user)
{
    ArgumentNullException.ThrowIfNull(recipient);

    string display = user?.Nickname ?? user?.Name ?? "Misafir";
    Console.WriteLine($"{display} -> {recipient}");
}

NRT açık olsa bile public API'lerde parametre doğrulamasını tamamen bırakmayın. Kütüphanenizi NRT'siz bir proje, reflection ya da başka bir dil çağırabilir; derleyici o durumda sizi koruyamaz.

C# 14 (.NET 10) ile null-conditional operatör artık atamanın sol tarafında da kullanılabiliyor:

customer?.LastSeen = DateTime.UtcNow;

customer null ise atama hiç çalışmaz ve sağ taraf değerlendirilmez. Eskiden bunun için ayrı bir if yazmak gerekiyordu.

Null-forgiving operatörü ve nullable attribute'ları

Bazen derleyicinin göremediği bir bilgiyi siz biliyorsunuzdur. Sonuna eklenen ! operatörü (null-forgiving), derleyiciye "bunun null olmadığına söz veriyorum" der. Uyarı kaybolur ama çalışma zamanı güvencesi gelmez.

string apiKey = Environment.GetEnvironmentVariable("API_KEY")!;

Bu satır susar ama değişken tanımlı değilse uygulama daha sonra, anlaşılmaz bir yerde patlar. Daha dürüst hali:

string apiKey = Environment.GetEnvironmentVariable("API_KEY")
    ?? throw new InvalidOperationException("API_KEY tanımlı değil.");

Kural basit: ! kullanıyorsanız, neden güvenli olduğunu bir cümleyle açıklayabilmelisiniz. Açıklayamıyorsanız gerçek bir kontrol yazın.

Derleyiciye bilgi vermek: NotNullWhen

TryXxx desenindeki metotlarda derleyici, dönüş değerinin out parametresi hakkında ne söylediğini bilmez. System.Diagnostics.CodeAnalysis altındaki attribute'lar bu boşluğu doldurur:

using System.Diagnostics.CodeAnalysis;

public static bool TryParseCode(string? input, [NotNullWhen(true)] out string? code)
{
    if (string.IsNullOrWhiteSpace(input))
    {
        code = null;
        return false;
    }

    code = input.Trim();
    return true;
}

if (TryParseCode(raw, out var code))
{
    Console.WriteLine(code.Length);
}

[NotNullWhen(true)] sayesinde true dalında code null kabul edilmez. Sık kullanılan diğer attribute'lar: [MaybeNullWhen(false)] (örneğin Dictionary.TryGetValue bunu kullanır), [NotNull] ve [MemberNotNull]. Sonuncusu, bir metot çağrıldıktan sonra belirli alanların dolu olduğunu bildirmek için kullanılır.

Mevcut projeye kademeli geçiş

Büyük bir kod tabanında NRT'yi tek seferde açmak yüzlerce uyarı üretir. Bu yüzden geçişi parça parça yapmak gerekir. Önerilen sıra:

  1. Nullable ayarını önce warnings ile açın: uyarıları görürsünüz ama tipler henüz ? ile işaretlenmemiş sayılır.
  2. Yeni yazılan dosyalarda #nullable enable direktifini kullanın; eski dosyalar #nullable disable ile kalabilir.
  3. Önce bağımlılık zincirinin en altındaki projeleri (domain, ortak kütüphaneler) dönüştürün; null bilgisi yukarı doğru akar.
  4. Bir proje temizlendiğinde proje genelinde enable'a geçin.
  5. Son adımda nullable uyarılarını hata yapın.
<PropertyGroup>
  <Nullable>enable</Nullable>
  <WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

Uyarıları hataya çevirmek, takımın NRT'yi "sonra bakarız" listesine atmasını engeller. Yeni bir proje başlıyorsanız bunu ilk günden yapmak en ucuz yoldur.

Nullable reference types'ın sınırları

NRT güçlü ama mutlak bir garanti değildir. Derleyicinin kontrol edemediği kapılar vardır:

  • Dizi elemanları: new string[10] ifadesindeki elemanlar başlangıçta null'dır ve derleyici bunu uyarmaz.
  • Deserialization: JSON'da alan eksikse non-nullable bir özellik yine null kalabilir. Zorunlu alanlar için required kullanın; System.Text.Json .NET 7'den itibaren required üyeleri dikkate alır.
  • NRT'siz kod: Eski kütüphaneler ve #nullable disable bölgeleri "oblivious" kabul edilir; oradan gelen değerler için derleyici uyarı vermez.
  • Reflection ve ORM: Nesneyi constructor'ı atlayarak dolduran araçlar, derleyicinin varsayımlarını dışarıdan bozabilir.

Entity Framework Core ile birlikte kullanım

EF Core, NRT açıkken özelliğin nullability'sini şemaya yansıtır: string Name zorunlu (NOT NULL) bir kolon, string? Phone ise opsiyonel bir kolon olur. Yani nullable annotation'lar yalnızca derleyici için değil, veri modeliniz için de bir sözleşmedir.

Navigation property'lerde sık görülen kalıp = null! kullanımıdır:

public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public Category Category { get; set; } = null!;
}

Burada ! bilinçli bir tercih: EF Core, ilişkili veriyi nesne oluşturulduktan sonra doldurur. Ancak Include yapmadan Category'ye erişirseniz değer yine null gelir. Bu yüzden sorgu tarafında neyi yüklediğinizi bilmeniz gerekir. Sorgu hatalarıyla ilgili daha fazlası için Entity Framework hataları yazısına bakabilirsiniz.

Sık Sorulan Sorular

Nullable reference types, int? gibi nullable value type ile aynı şey mi?

Hayır. int? aslında Nullable<int> struct'ıdır ve çalışma zamanında farklı bir tiptir. string? ise yalnızca derleyiciye yönelik bir annotation'dır; çalışma zamanında string ile aynıdır.

NRT açıkken NullReferenceException tamamen biter mi?

Tamamen bitmez, ama büyük ölçüde azalır. Dizi elemanları, deserialization, reflection ve ! operatörü gibi açık kapılar kalır. Yine de sınıfın hatalarının büyük kısmı derleme zamanında yakalanır.

Performansa etkisi var mı?

Yok. Analiz derleme zamanında yapılır ve çalışma zamanına ek bir kod eklenmez. Etki yalnızca derleme sırasında üretilen uyarılar ve sizin yazdığınız ek kontroller kadardır.

Uyarıları tamamen kapatmak mantıklı mı?

Geçiş sürecinde dosya ya da proje bazında geçici olarak #nullable disable kullanmak makul. Ancak kalıcı olarak kapatmak, özelliğin değerini sıfırlar. Hedef, uyarıları hata seviyesine çıkarmak olmalı.

required ile constructor parametresi arasında nasıl seçim yapmalıyım?

Nesnenin geçerli olması için mutlaka gereken değerler ve değişmezlik önemliyse constructor tercih edilir. Nesne başlatıcı (object initializer) ile okunabilir bir oluşturma isteniyorsa ya da DTO/yapılandırma sınıfları söz konusuysa required daha pratiktir.

Sonuç

Nullable reference types, null kontrolünü bir alışkanlıktan derleyici sözleşmesine çevirir. Üç noktayı aklınızda tutun:

  • Null olabilen her şeyi tipte ? ile belirtin; zorunlu değerleri required ya da constructor ile garanti edin.
  • ! operatörünü susturma aracı değil, gerekçesi olan istisna olarak kullanın.
  • Yeni projede nullable uyarılarını baştan hataya çevirin; eski projede dosya dosya, katman katman ilerleyin.