Ü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 |
|---|---|---|
| CS8600 | Null olabilecek değer non-nullable değişkene atanıyor | Değişkeni ? yapın ya da değeri önce kontrol edin |
| CS8602 | Null olabilecek referans dereference ediliyor | Null kontrolü ekleyin |
| CS8603 | Null olabilecek değer döndürülüyor | Dönüş tipini ? yapın ya da null durumunu ele alın |
| CS8604 | Null olabilecek argüman non-nullable parametreye veriliyor | Çağıran tarafta kontrol edin |
| CS8618 | Non-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,isise 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:
Nullableayarını öncewarningsile açın: uyarıları görürsünüz ama tipler henüz?ile işaretlenmemiş sayılır.- Yeni yazılan dosyalarda
#nullable enabledirektifini kullanın; eski dosyalar#nullable disableile kalabilir. - Ö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.
- Bir proje temizlendiğinde proje genelinde
enable'a geçin. - 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
requiredkullanın; System.Text.Json .NET 7'den itibarenrequiredüyeleri dikkate alır. - NRT'siz kod: Eski kütüphaneler ve
#nullable disablebö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ğerlerirequiredya 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.