Mert Özen Her Satırda Daha İleri
Backend C# ile Nesne Yönelimli Programlama — #02

C#'ta Field, Property ve Constructor: Object'i Doğru Kurmak

Mert Özen 7 Kas 2026 10 dk 2 görüntülenme
C#'ta Field, Property ve Constructor: Object'i Doğru Kurmak

Kendi constructor'ını yazınca kaybolan parametresiz constructor, private ve readonly field'lar, doğrulama yapan property, auto-property, get-only, private set, init ve hesaplanan property, constructor zincirleme ve initializer'ların çalışma sırası.

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

Önceki yazıdaki Book'u biraz olgunlaştırdım: field'lar property oldu ve başlıkla fiyatı alan bir constructor geldi. Ardından kitabı tanıdık bir yazımla, object initializer ile oluşturuyorum. Bu kod derlenir mi?

Book dune = new Book { Title = "Dune", Price = 45.90m };
Console.WriteLine(dune.Title);

public class Book
{
    public string Title { get; set; } = "";
    public decimal Price { get; set; }

    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }
}

Property'lerin ikisi de public ve set'leri var; ilk bakışta itiraz edecek bir şey görünmüyor. Cevap bir sonraki bölümde.

Constructor Yazınca Kaybolan Constructor

Derlenmez:

Program.cs(1,17): error CS7036: There is no argument given that corresponds to the required parameter 'title' of 'Book.Book(string, decimal)'

new Book { ... } yazımı aslında new Book() { ... } demektir: önce parametresiz constructor çağrılır, sonra süslü parantezdeki atamalar yapılır. Hiç constructor yazmadığın bir class'a derleyici parametresiz, boş bir constructor ekler; önceki yazıda new Book() bu yüzden çalışıyordu. Kendi constructor'ını yazdığın anda bu hediye geri alınır. Derleyicinin mantığı şu: "Bu tipin nasıl kurulacağına sen karar verdin; başlıksız bir kitap da olabilecekse bunu açıkça söyle."

İki çıkış yolun var. Ya çağıran tarafı constructor'a uydurursun: new Book("Dune", 45.90m). Ya da parametresiz bir constructor'ı bilerek eklersin. Hangisinin doğru olduğu tasarım sorusu: başlıksız bir kitap senin uygulamanda anlamlı mı? Çoğu zaman değil, ve constructor tam olarak bunu zorlamak için var.

Field: Verinin Durduğu Yer

Field, object'in içindeki bir değişkendir. Önceki yazıda field'ları public yaptık; gerçek kodda field neredeyse her zaman private olur ve C# geleneğinde alt çizgiyle başlayan camelCase bir ad taşır: _price, _number. Field object'in iç durumudur; dışarıya ne göstereceğine property'lerle karar verirsin.

Bir field'ı readonly yaparsan ona yalnızca tanımlandığı yerde ya da constructor'da değer atayabilirsin:

Order order = new Order("ORD-1001");
Console.WriteLine(order.Number);

public class Order
{
    private readonly string _number;

    public Order(string number)
    {
        _number = number;
    }

    public string Number => _number;

    public void Renumber(string number)
    {
        _number = number;
    }
}
Program.cs(17,9): error CS0191: A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)

Sipariş numarası bir kez verilir ve değişmez; readonly bunu bir yorum satırı ya da iyi niyet olmaktan çıkarıp derleyicinin denetlediği bir kurala çevirir. Number => _number satırı da ilk property örneğimiz: dışarıya yalnızca okuma kapısı açıyor.

Property: Field'ın Kapı Görevlisi

Property dışarıdan field gibi görünür ama içi iki method'dur: okurken çalışan get ve yazarken çalışan set. Bir apartman kapı görevlisi gibi düşün; daireye giden yol ondan geçer ve kimi içeri alacağına o karar verir. Fiyatın eksiye düşmesini engelleyen bir Book:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Book dune = new Book("Dune", 45.90m);
dune.Price = 39.90m;
Console.WriteLine($"{dune.Title}: {dune.Price}");

try
{
    dune.Price = -10m;
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine(ex.Message);
}

Console.WriteLine($"{dune.Title}: {dune.Price}");

public class Book
{
    private decimal _price;

    public string Title { get; }

    public decimal Price
    {
        get { return _price; }
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), value, "Price cannot be negative.");
            }

            _price = value;
        }
    }

    public Book(string title, decimal price)
    {
        Title = title;
        Price = price;
    }
}
Dune: 39.90
Price cannot be negative. (Parameter 'value')
Actual value was -10.
Dune: 39.90

set'in içindeki value, atanmak istenen değerdir; onu sen tanımlamazsın, dil verir. Eksi fiyat geldiğinde exception fırlatıyoruz ve fiyat 39.90'da kalıyor. Constructor'ın da _price'a değil Price'a atadığına dikkat et: kural tek bir yerde yazılı ve constructor da o kapıdan geçiyor. Culture satırı ise ondalık ayırıcının makineden makineye değişmemesi için.

Neden baştan public field yazıp geçmiyoruz? Çünkü property'nin arkasındaki kodu, onu kullanan hiçbir satıra dokunmadan değiştirebilirsin. Bugün düz bir değer tutan Price'a yarın doğrulama ya da loglama ekleyebilirsin. Public field'ı sonradan property'ye çevirmek ise derlenmiş kodu kullanan diğer assembly'leri kırar. Ayrıca JSON serializer'lar, data binding ve EF Core gibi araçlar varsayılan olarak property'lerle çalışır.

Auto-Property, get-only ve Hesaplanan Property

Her property'nin arkasına elle field yazmak zorunda değilsin. { get; set; } yazdığında derleyici görünmez bir field üretir; buna auto-property denir. Kuralı olmayan property'lerin çoğu böyledir. Erişimi daraltan birkaç varyasyon tek class'ta:

Book dune = new Book("Dune", "Frank Herbert")
{
    Isbn = "978-0441172719"
};
dune.Receive(3);

Console.WriteLine($"{dune.Title} / {dune.Author} / {dune.Isbn} / stock {dune.Stock} / in stock: {dune.IsInStock}");

Book draft = new Book("Untitled");
Console.WriteLine($"{draft.Title} / {draft.Author} / '{draft.Isbn}' / in stock: {draft.IsInStock}");

public class Book
{
    public string Title { get; }
    public string Author { get; }
    public string Isbn { get; init; } = "";
    public int Stock { get; private set; }
    public bool IsInStock => Stock > 0;

    public Book(string title, string author)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            throw new ArgumentException("Title is required.", nameof(title));
        }

        Title = title;
        Author = author;
    }

    public Book(string title) : this(title, "Unknown")
    {
    }

    public void Receive(int quantity)
    {
        Stock += quantity;
    }
}
Dune / Frank Herbert / 978-0441172719 / stock 3 / in stock: True
Untitled / Unknown / '' / in stock: False

Title { get; } get-only bir auto-property: değerini yalnızca constructor'da ya da tanımın yanında alır, sonra kimse değiştiremez. Isbn { get; init; } C# 9 ile gelen init accessor'ını kullanıyor; object oluşturulurken, initializer bloğunda atanabilir, sonrasında kilitlenir. Stock { get; private set; } herkese okuma izni verir ama yazmayı class'ın kendisine bırakır; stok yalnızca Receive gibi method'larla değişir. IsInStock => Stock > 0 ise arkasında field olmayan, her okunduğunda yeniden hesaplanan bir property. Stock değiştikçe IsInStock kendiliğinden doğru kalır; senkron tutman gereken ikinci bir değer yoktur.

Constructor: Object Geçerli Doğsun

Constructor, object oluşturulurken bir kez çalışan özel bir method'dur; adı class adıyla aynıdır ve dönüş tipi yoktur. Asıl işi, object'i kullanılabilir bir durumda teslim etmek. Yukarıdaki iki parametreli constructor boş başlığı reddediyor. Böylece programın hiçbir yerinde "başlığı boş bir kitap olabilir mi?" diye düşünmen gerekmiyor; böyle bir kitap hiç oluşamıyor.

Constructor'lar da overload edilebilir. Tek parametreli constructor kendi doğrulamasını tekrar yazmak yerine : this(title, "Unknown") ile diğerine devrediyor. Bu zincirleme sayesinde doğrulama tek yerde kalır; yarın başlık için yeni bir kural eklediğinde iki constructor'ı birden güncellemeyi unutma riskin olmaz. Gördüğün gibi draft için yazar "Unknown" oldu ve ISBN, property'nin varsayılan değeri olan boş string'de kaldı.

Bir not: C# 12 ile class'lar için primary constructor geldi ve bu tür basit atamaları çok daha kısa yazmak mümkün oldu; onu C# Sürümleri serisinde göreceğiz. C# 10'da constructor body'sini kendin yazarsın.

init ve Object Initializer

Object initializer, constructor çalıştıktan hemen sonra property'lere değer atamanın kısa yazımıdır. Constructor'a her isteğe bağlı değer için bir parametre eklemek yerine zorunlu olanları constructor'dan, isteğe bağlı olanları initializer'dan alırsın. Peki kitap oluştuktan sonra bu property'lere dokunmaya çalışırsan ne olur?

Book dune = new Book("Dune", "Frank Herbert") { Isbn = "978-0441172719" };
dune.Isbn = "000";
dune.Stock = 10;
dune.Title = "Dune Messiah";

public class Book
{
    public string Title { get; }
    public string Author { get; }
    public string Isbn { get; init; } = "";
    public int Stock { get; private set; }

    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }
}
Program.cs(2,1): error CS8852: Init-only property or indexer 'Book.Isbn' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
Program.cs(3,1): error CS0272: The property or indexer 'Book.Stock' cannot be used in this context because the set accessor is inaccessible
Program.cs(4,1): error CS0200: Property or indexer 'Book.Title' cannot be assigned to -- it is read only

Üç satır, üç farklı kilit. init "yalnızca kuruluşta", private set "yalnızca içeriden", get-only ise "yalnızca constructor'da" der. Stock'u initializer içinde atamaya çalışsaydın da aynı CS0272'yi alırdın; private set initializer'a da kapalıdır.

init'in bir zayıf noktası var: atanmasını zorunlu kılmaz. draft'ta ISBN hiç verilmedi ve kimse itiraz etmedi. Zorunlu değerin yeri C# 10'da constructor'dır. C# 11'deki required bu boşluğu kapatıyor; o da C# Sürümleri serisinin konusu.

Hangi Sırayla Çalışır

Field initializer, constructor body'si ve object initializer aynı new expression'ında bir araya geldiğinde sıra şöyle:

Book dune = new Book("Dune") { Format = Log("3. object initializer", "Paperback") };
Console.WriteLine($"{dune.Title}, {dune.Format}, {dune.Currency}");

static string Log(string step, string value)
{
    Console.WriteLine(step);
    return value;
}

public class Book
{
    public string Currency { get; } = Show("1. field/property initializer", "EUR");
    public string Title { get; }
    public string Format { get; init; } = "Hardcover";

    public Book(string title)
    {
        Console.WriteLine("2. constructor body");
        Title = title;
    }

    private static string Show(string step, string value)
    {
        Console.WriteLine(step);
        return value;
    }
}
1. field/property initializer
2. constructor body
3. object initializer
Dune, Paperback, EUR

Önce tanımların yanındaki başlangıç değerleri atanır, sonra constructor body'si çalışır, en son initializer bloğu. Format önce "Hardcover" oldu, sonra initializer onu "Paperback" ile overwrite etti. Bu sıranın pratik sonucu şu: constructor'ın içinde initializer'dan gelecek değerlere güvenemezsin, çünkü o an henüz atanmamışlardır. Constructor'da doğrulamak istediğin her şey constructor parametresi olmalı.

Sık Düşülen Tuzaklar

Property'yi kendi içinden çağırmak. Arkasında field olan bir property yazarken field yerine property'nin adını yazmak kolay bir hata, ve derleyici uyarmaz:

Book dune = new Book();
dune.Title = "Dune";

public class Book
{
    public string Title
    {
        get => Title;
        set => Title = value;
    }
}
Stack overflow.
Repeat 261297 times:
--------------------------------
   at Book.set_Title(System.String)
--------------------------------

set kendini çağırıyor, o da kendini, ta ki stack dolana kadar. Stack overflow'u try/catch ile yakalayamazsın; process doğrudan kapanır. Çıktıdaki set_Title adı, property'nin perde arkasında gerçekten bir method olduğunun kanıtı.

Her property'ye { get; set; } yazmak. Alışkanlıkla açılan her set, object'in durumunu herkesin değiştirebileceği anlamına gelir. Önce get-only ile başla; değişmesi gerekiyorsa init mi, private set mi, yoksa niyeti anlatan bir method mu daha doğru, ona karar ver.

Constructor'da doğrulamayıp property'de doğrulamak ya da tam tersi. Kural iki yerde yazılırsa er geç biri güncellenir, diğeri unutulur. Constructor'dan property'ye atayarak ya da constructor'ları this(...) ile zincirleyerek kuralı tek yerde tut.

Constructor'a iş yükü bindirmek. Constructor object'i kurar; veritabanına bağlanmak, dosya okumak ya da ağ isteği yapmak onun işi değildir. Hem hata yönetimi zorlaşır hem de object'i test etmek için bütün o dünyayı ayağa kaldırman gerekir.

Küçük Bir Deneme

★☆☆ Book'a bir PageCount property'si ekle: get-only olsun, constructor'dan gelsin ve 0 ya da negatif bir sayı geldiğinde ArgumentOutOfRangeException fırlatsın. Hatalı bir değerle dene ve exception mesajını oku.

★★☆ Receive'in yanına bir Sell(int quantity) method'u yaz. Stoktan fazlası satılmak istendiğinde InvalidOperationException fırlatsın. Stock'un private set olmasının bu kuralı nasıl koruduğunu, set'i public yaparak bozmayı deneyerek gör.

★★★ Yukarıdaki sıra örneğine bir Discount property'si ekle; varsayılanı 0, initializer'dan 10 gelsin. Constructor içinde Discount'u yazdır ve hangi değeri gördüğünü tahmin et, sonra çalıştır. Bu sonuç, constructor'da hangi değerlere güvenebileceğin hakkında ne söylüyor?

Bu yazıda bir object'in nasıl doğduğunu ve hangi kapılardan değiştirilebileceğini kontrol altına aldık. Ama private kelimesini daha çok sezgiyle kullandık. Bir sonraki yazıda encapsulation'a ve access modifier'ların tamamına bakıyoruz: public, private, protected, internal ve ikili kombinasyonları, ayrıca get-only bir listenin neden yine de dışarıdan değiştirilebildiği.