Mert Özen Her Satırda Daha İleri
Backend C# Temelleri — #13

C# Nullable: int?, string? ve null'ı Tasarımla Yönetmek

Mert Özen 7 Kas 2026 10 dk 1 görüntülenme
C# Nullable: int?, string? ve null'ı Tasarımla Yönetmek

C#'ta null ile çalışmak: Nullable<T> ve int?, null ile hesap yapan lifted operatörler, C# 8 ile gelen nullable reference types ve uyarıları, flow analysis, ?. ?? ??= ve ! operatörleri ve null'ı tasarımla yönetmenin kuralları.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Müşterinin e-posta adresine göre bir indirim kuponu arıyoruz. Kupon yoksa method null döndürüyor ve dönüş tipini de dürüstçe string? yazmışız. Bu kod derlenir mi? Derlenirse çalışınca ne olur?

string? coupon = FindCoupon("ada@example.com");
Console.WriteLine(coupon.Length);

static string? FindCoupon(string email) => email.EndsWith("@example.org") ? "WELCOME10" : null;
Program.cs(2,19): warning CS8602: Dereference of a possibly null reference.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 2

Kod derlendi; compiler sadece bir uyarı verdi. Çalışınca da NullReferenceException ile çöktü. Bu yazının en önemli cümlesi bu çıktıda saklı: C#'taki nullable reference types bir compile-time uyarı sistemidir, runtime'da seni korumaz. Uyarıyı okursan hatayı daha kod çalışmadan yakalarsın; okumazsan C# 8 öncesindeki gibi production'da yakalarsın.

Null, "burada bir değer yok" demenin yolu. Önceki yazıda gördüğümüz gibi reference type bir değişken hiçbir object'i göstermeyen bir referans, yani null tutabilir. Sorun, o değişkeni kullanan kodun bunu çoğu zaman unutması. Null referansı icat eden Tony Hoare, bunu yıllar sonra "milyar dolarlık hatam" diye anmıştı. C# bu soruna iki taraftan yaklaşıyor: value type'lar için Nullable<T>, reference type'lar için nullable reference types.

Nullable Value Type: int?

Bir int her zaman bir sayıdır, null olamaz. Ama bazen "değer yok" demen gerekir: kitabın henüz puanı yoktur, siparişte indirim tanımlanmamıştır. Bunun için tipin sonuna ? koyarsın:

int? discountPercent = null;
Nullable<int> sameThing = null;

Console.WriteLine(discountPercent.HasValue);
Console.WriteLine(discountPercent.GetValueOrDefault());
Console.WriteLine(discountPercent ?? 0);

discountPercent = 15;
Console.WriteLine(discountPercent.HasValue);
Console.WriteLine(discountPercent.Value);

int certain = discountPercent ?? 0;
Console.WriteLine(certain);
Console.WriteLine(sameThing.HasValue);
False
0
0
True
15
15
False

int?, Nullable<int>'in kısa yazımı; ikisi tamamen aynı tip. HasValue değer olup olmadığını söyler, Value değeri verir. GetValueOrDefault() değer yoksa tipin default değerini (burada 0) döndürür, ?? ise hangi değerin kullanılacağını senin seçmene izin verir. Değer yokken Value'yu okursan exception alırsın:

int? rating = null;
int stars = rating.Value;
Program.cs(2,13): warning CS8629: Nullable value type may be null.
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
   at System.Nullable`1.get_Value()
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 2

int?'i doğrudan bir int'e atamaya kalkarsan compiler seni durdurur, çünkü bu atama bilgi kaybı demek:

int? rating = null;
int stars = rating;
Program.cs(2,13): error CS0266: Cannot implicitly convert type 'int?' to 'int'. An explicit conversion exists (are you missing a cast?)
Program.cs(2,13): warning CS8629: Nullable value type may be null.

Benim tavsiyem: Value'yu neredeyse hiç kullanma. Ya ?? ile bir varsayılan ver ya da önce is int rating gibi bir pattern ile kontrol et.

Null ile Hesap: Lifted Operatörler

int? değerlerle aritmetik ve karşılaştırma yapabilirsin, ama sonuçlar ilk bakışta şaşırtabilir:

int? stock = null;

Console.WriteLine(stock + 5 is null);
Console.WriteLine(stock > 0);
Console.WriteLine(stock <= 0);
Console.WriteLine(stock == null);

bool? isGift = null;
Console.WriteLine(isGift == true);
True
False
False
True
False

null + 5 sonucu null: bilinmeyen bir sayıya 5 eklersen sonuç da bilinmez. Karşılaştırmalarda ise işin içine null girdiğinde sonuç her zaman false. Dikkat et, stock > 0 de stock <= 0 da false döndü; yani null bir değer ne pozitif ne de sıfır ya da negatif. if (stock > 0) { ... } else { /* stok yok */ } diye yazdığın bir kod, null geldiğinde "stok yok" dalına düşer ve bunu fark etmeyebilirsin. bool? ile çalışırken de if (isGift == true) yazman gerekir; if (isGift) derlenmez.

Nullable Reference Types

Reference type'lar zaten null olabiliyordu. C# 8 ile gelen nullable reference types özelliği bunu tersine çeviriyor: proje dosyasında <Nullable>enable</Nullable> varsa (.NET 6'dan beri proje template'leri bunu açık getirir) string "null olmayan string", string? ise "null olabilen string" anlamına gelir. Compiler da bu contract'ı kod boyunca takip eder:

string title = null;
string? subtitle = null;

Console.WriteLine(GetLength(title));
Console.WriteLine(GetLength(subtitle));

static int GetLength(string text) => text.Length;
Program.cs(1,16): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(4,29): warning CS8604: Possible null reference argument for parameter 'text' in 'int GetLength(string text)'.
Program.cs(5,29): warning CS8604: Possible null reference argument for parameter 'text' in 'int GetLength(string text)'.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<<Main>$>g__GetLength|0_0(String text) in /src/Bookstore/Program.cs:line 7
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 4

CS8600, null olmaması gereken bir değişkene null atadığını söylüyor. CS8604, null olabilecek bir değeri null beklemeyen bir parametreye geçtiğini. Kod yine çalıştı ve yine çöktü. string ile string? arasında runtime'da hiçbir fark yok; ikisi de aynı System.String. Fark yalnızca compiler'ın kafasında.

Bu uyarıların en sık göreceğin hâli class'larda:

var book = new Book();
Console.WriteLine(book.Title.Length);

class Book
{
    public string Title { get; set; }
    public string Isbn { get; set; }
    public string? Subtitle { get; set; }
}
Program.cs(6,19): warning CS8618: Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
Program.cs(7,19): warning CS8618: Non-nullable property 'Isbn' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 2

Title ve Isbn'i null olamaz diye işaretledik ama hiçbir yerde değer vermedik; constructor bittiğinde ikisi de null. Mesajdaki required önerisi C# 11 ile geldi, C# 10'da kullanamazsın; bunu C# Sürümleri serisinde göreceğiz. C# 10'da çözüm, property'ye bir başlangıç değeri vermek (= "";), değeri constructor'da almak ya da property gerçekten boş kalabiliyorsa string? yapmak.

Uyarıları gerçekten ciddiye almak istiyorsan, ki bence yeni projelerde istemelisin, onları hataya çevirebilirsin:

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <Nullable>enable</Nullable>
  <WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

Aynı ilk örnek artık derlenmiyor:

Program.cs(2,19): error CS8602: Dereference of a possibly null reference.

Flow Analysis ve Null Kontrolleri

Compiler, bir değişkenin null olup olmadığını kodun akışına bakarak takip eder. Bir kontrol yaptıktan sonra o bloğun içinde artık uyarı vermez:

string? coupon = FindCoupon("ada@example.org");

if (coupon is not null)
{
    Console.WriteLine(coupon.Length);
}

if (string.IsNullOrEmpty(coupon))
{
    return;
}
Console.WriteLine(coupon.ToLowerInvariant());

static string? FindCoupon(string email) => email.EndsWith("@example.org") ? "WELCOME10" : null;
9
welcome10

if (coupon is not null) bloğunun içinde coupon artık null olamaz, compiler bunu biliyor. Daha ilginç olanı ikinci kısım: string.IsNullOrEmpty(coupon) doğruysa return ettik ve altındaki satırda compiler uyarı vermedi. Çünkü .NET'in kendi method'ları "bu method false döndürüyorsa parametre null değildir" bilgisini attribute'larla taşıyor. Bu attribute'ları kendi kodunda nasıl kullanacağını C# İleri Seviye serisinde göreceğiz.

?. ?? ve ??=

Null ile çalışırken en çok kullanacağın üç operatör:

var customer = new Customer { Name = "Ada" };

Console.WriteLine(customer.Address?.City ?? "no city");

customer.Address = new Address { City = "Izmir" };
Console.WriteLine(customer.Address?.City ?? "no city");

string? nickname = null;
nickname ??= customer.Name;
Console.WriteLine(nickname);

int? length = customer.Address?.City?.Length;
Console.WriteLine(length);

class Customer
{
    public string Name { get; set; } = "";
    public Address? Address { get; set; }
}

class Address
{
    public string? City { get; set; }
}
no city
Izmir
Ada
5

?. (null-conditional), solundaki değer null ise zinciri orada keser ve sonucu null yapar; exception fırlatmaz. customer.Address?.City adres yoksa null döner, ?? "no city" da o durumda kullanılacak değeri verir. ??= ise "değişken null ise şu değeri ata" demek. Son örnekte dikkat et: Length bir int, ama customer.Address?.City?.Length expression'ının sonucu int?. Zincir herhangi bir yerde kesilebileceği için ?. her zaman nullable bir sonuç üretir.

! Operatörü ve Ne Zaman Kullanılmaz

! (null-forgiving operator) compiler'a "bunun null olmadığını ben biliyorum, uyarıyı kapat" der:

string? input = Console.ReadLine();
string trimmed = input!.Trim();
Console.WriteLine(trimmed);

Console.ReadLine()'ın dönüş tipi string?; input stream bittiğinde null döner. input! uyarıyı susturdu ve program Dune girdisiyle düzgün çalıştı:

Dune

Ama ! hiçbir kontrol eklemez. Input stream kapalıyken bu kod yine NullReferenceException fırlatır; sadece compiler artık seni uyarmaz. ! gördüğün her yer "burada bir varsayım var" demek. Doğru kullanım alanı dar: compiler'ın bilemeyeceği bir şeyi gerçekten bildiğin durumlar. Çoğu zaman doğru çözüm ! değil, ?? "" ya da açık bir null kontrolü.

default! da aynı ailenin üyesi ve seni yanıltabilir:

string title = default!;
Console.WriteLine(title is null);
True

Tipi string olan, yani "asla null olmaz" dediğimiz bir değişken null tutuyor ve compiler bir şey demiyor.

Null'ı Tasarımla Yönetmek

Her yere ?. ve if (x is null) serpiştirmek yerine, null'ın nerede anlamlı olduğuna baştan karar vermek çok daha sağlıklı. Benim uyduğum birkaç kural var. Birincisi: dışarıdan gelen veriyi giriş kapısında kontrol et. Constructor null kabul etmiyorsa bunu hemen söylesin:

var book = new Book("Dune", "978-0441013593");
Console.WriteLine(book.Title);

class Book
{
    public Book(string title, string isbn)
    {
        ArgumentNullException.ThrowIfNull(title);
        ArgumentNullException.ThrowIfNull(isbn);
        Title = title;
        Isbn = isbn;
    }

    public string Title { get; }
    public string Isbn { get; }
    public string? Subtitle { get; init; }
}
Dune

ArgumentNullException.ThrowIfNull .NET 6 ile geldi. Null bir değer gelirse hatanın sebebi, bozuk object sistemin içinde dolaşmaya başlamadan, tam oluştuğu yerde ortaya çıkar. Subtitle ise gerçekten opsiyonel olduğu için string?.

İkincisi: "bulunamadı" ile "hiç yok" farklı şeylerdir. Tek bir şey arayan method bulamadığında null döndürebilir ve dönüş tipini Book? yaparak bunu dürüstçe söyler. Collection döndüren method ise asla null döndürmemeli; boş bir collection döndürmeli:

var shelf = new Shelf();
Console.WriteLine(shelf.FindByIsbn("978-0441013593")?.Title ?? "not found");
Console.WriteLine(shelf.FindByAuthor("Austen").Count);

foreach (Book book in shelf.FindByAuthor("Herbert"))
{
    Console.WriteLine(book.Title);
}

if (shelf.TryGetStock("978-0441013593", out int stock))
{
    Console.WriteLine(stock);
}

class Shelf
{
    private readonly List<Book> books = new()
    {
        new Book { Isbn = "978-0441013593", Title = "Dune", Author = "Herbert", Stock = 3 }
    };

    public Book? FindByIsbn(string isbn) => books.Find(b => b.Isbn == isbn);

    public List<Book> FindByAuthor(string author) => books.FindAll(b => b.Author == author);

    public bool TryGetStock(string isbn, out int stock)
    {
        Book? book = FindByIsbn(isbn);
        stock = book?.Stock ?? 0;
        return book is not null;
    }
}

class Book
{
    public string Isbn { get; set; } = "";
    public string Title { get; set; } = "";
    public string Author { get; set; } = "";
    public int Stock { get; set; }
}
Dune
0
Dune
3

FindByAuthor("Austen") hiç kitap bulamadı ama Count 0 döndü, foreach da sorunsuz çalıştı. Çağıran hiçbir null kontrolü yazmak zorunda kalmadı. Üçüncüsü: değerin olmayabileceği ve bunun normal olduğu yerlerde "Try" kalıbı (TryGetStock) hem null'dan hem de exception'dan kurtarır.

Sık Düşülen Tuzaklar

Array element'lerini unutmak. Nullable reference types array'lerin içini takip etmez:

string[] titles = new string[2];
Console.WriteLine(titles[0].Length);
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 2

string[] "null olmayan string'lerden oluşan array" diyor, ama new string[2] iki null ile başlıyor ve compiler hiçbir uyarı vermedi. Array'i oluşturduğun anda doldur ya da tipini string?[] yap.

Uyarıları görmezden gelmek. Bir projede yüzlerce nullable uyarısı birikmişse, yeni gelen önemli uyarı aralarında kaybolur. Uyarı sayısını sıfırda tut ya da yukarıdaki gibi hataya çevir.

! ile uyarıları susturmak. Uyarı bir soruya işaret ediyor: "bu null olabilir mi?" ! o soruyu cevaplamaz, sadece sorulmasını engeller.

Value'ya kontrolsüz erişmek. int? için rating.Value yazmak, reference type'ta ! kullanmakla aynı riski taşır. ??, GetValueOrDefault() ya da pattern kullan.

Küçük Bir Deneme

★☆☆ Bir decimal? discount değişkeni tanımla. Fiyatı price - (discount ?? 0m) ile hesapla ve discount null iken ve 5 iken sonuçları yazdır.

★★☆ Yazının başındaki kodu, uyarı vermeyecek ve çökmeyecek şekilde üç farklı yolla düzelt: if ile, ?. ve ?? ile, string.IsNullOrEmpty ile. Hangisi niyeti en iyi anlatıyor?

★★★ Yeni bir console projesine <WarningsAsErrors>nullable</WarningsAsErrors> ekle ve içinde string Title, string? Subtitle ve List<string> Tags olan bir Book class'ı yaz. Projeyi hiçbir ! kullanmadan derlenir hâle getir.

Null kontrolünü unuttuğumuzda ne olduğunu bu yazıda defalarca gördük: program bir exception ile çöktü. Bir sonraki yazıda bu exception'ların kendisini ele alıyoruz: try, catch ve finally, throw ile throw ex farkı, when filtresi, kendi exception tipini yazmak ve ne zaman exception, ne zaman TryXxx kullanmak gerektiği.