Mert Özen Her Satırda Daha İleri
Backend C# İleri Seviye — #09

C#'ta ref return, ref local, in Parametreleri ve ref struct

Mert Özen 7 Kas 2026 10 dk 6 görüntülenme
C#'ta ref return, ref local, in Parametreleri ve ref struct

Değeri kopyalamak yerine referansıyla taşımak: ref local, ref return, ref readonly, in parametrelerinde defensive copy tuzağı, readonly struct ve kendi ref struct tipini yazmak.

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

BookStock bir struct; içinde ISBN ve stok sayısı var. Array'deki ilk kitabı bir değişkene alıp stoğunu bir azaltıyoruz, ikinci kitabı ref ile alıp onu da bir azaltıyoruz. Ekranda hangi iki sayı çıkar?

var books = new BookStock[]
{
    new("978-0441013593", 5),
    new("978-0141439518", 3),
};

BookStock copy = books[0];
copy.Stock--;

ref BookStock slot = ref books[1];
slot.Stock--;

Console.WriteLine($"{books[0].Stock} {books[1].Stock}");

public struct BookStock
{
    public BookStock(string isbn, int stock)
    {
        Isbn = isbn;
        Stock = stock;
    }

    public string Isbn { get; }
    public int Stock { get; set; }
}

ref local: Kopyası Değil, Kendisi

Çıktı:

5 2

İlk kitabın stoğu hâlâ 5. Struct bir value type; BookStock copy = books[0] array'deki değerin bir kopyasını aldı ve copy.Stock-- sadece kopyayı değiştirdi. İkinci satırda ise ref BookStock slot = ref books[1] yazdık. slot yeni bir değer değil, array'in ikinci hücresinin kendisine verilmiş başka bir isim. Ona ne yaparsan doğrudan o hücreye yapılır; stok 3'ten 2'ye indi.

Ref local'i bir takma ad gibi düşün. Normal bir değişken bir kutudur ve atama, içeriği yeni bir kutuya kopyalar. Ref local ise kutunun kendisini gösteren bir etikettir; kutu bir array hücresi, bir field ya da başka bir değişken olabilir. Bu C# 7.0'dan beri var ve iki yerde değerlidir: büyük struct'ları kopyalamamak ve bir değeri bulunduğu yerde değiştirmek.

Atamanın iki tarafında da ref olduğuna dikkat et. Sağdaki ref books[1] "bu hücrenin referansını ver" der, soldaki ref BookStock "ben bir referans tutacağım" der. Birini unutursan compiler hata verir; sessizce kopyaya düşmez.

ref return: Çağırana Bir Adres Vermek

Ref'in asıl gücü method'dan döndürülebilmesi. Bir envanter class'ı düşün: kitapları içeride bir struct array'inde tutuyor ve ISBN ile arama yapıyor. Aranan kaydı değer olarak döndürürse çağıran sadece bir kopya alır. Referans döndürürse çağıran kaydı yerinde güncelleyebilir:

var inventory = new Inventory(
    new BookStock("978-0441013593", 5),
    new BookStock("978-0141439518", 3));

ref BookStock dune = ref inventory.Find("978-0441013593");
dune.Stock -= 2;

inventory.Find("978-0141439518").Stock += 10;

foreach (BookStock item in inventory.All)
    Console.WriteLine($"{item.Isbn}: {item.Stock}");

public sealed class Inventory
{
    private readonly BookStock[] _items;

    public Inventory(params BookStock[] items) => _items = items;

    public ReadOnlySpan<BookStock> All => _items;

    public ref BookStock Find(string isbn)
    {
        for (int i = 0; i < _items.Length; i++)
        {
            if (_items[i].Isbn == isbn)
                return ref _items[i];
        }

        throw new KeyNotFoundException(isbn);
    }
}

public struct BookStock
{
    public BookStock(string isbn, int stock)
    {
        Isbn = isbn;
        Stock = stock;
    }

    public string Isbn { get; }
    public int Stock { get; set; }
}
978-0441013593: 3
978-0141439518: 13

Find'ın dönüş tipi ref BookStock ve return ref _items[i] array hücresinin referansını veriyor. Çağıran taraf onu ref BookStock dune ile bir ref local'e bağlayabilir ya da ikinci kullanımda olduğu gibi hiç değişkene almadan doğrudan değiştirebilir: inventory.Find(...).Stock += 10. Hiçbir kopya yok, iki güncelleme de array'in içinde oldu.

All property'si de bir önceki yazıdan tanıdık: array'i ReadOnlySpan<BookStock> olarak dışarı açıyor. Çağıran dolaşabiliyor ama değiştiremiyor. Span'in indexer'ı da aslında bir ref return: Span<T>'in this[int]'i ref T döner, bu yüzden span[0] = 5 ya da span[0].Stock++ doğrudan alttaki belleği değiştirir.

Compiler Ne Döndürmene İzin Verir

Referans döndürmek, method bittikten sonra da yaşayacak bir şeyi göstermek zorunda. Method'un kendi local değişkeni method bitince yok olur; ona referans döndürmek, boşalmış bir adresi vermek olurdu. Compiler bunu compile time'da yakalar:

static ref int Broken()
{
    int stock = 5;
    return ref stock;
}

static ref int AlsoBroken(int[] items)
{
    var copy = items[0];
    return ref copy;
}

Console.WriteLine(Broken());
Console.WriteLine(AlsoBroken(new[] { 1 }));
Program.cs(4,16): error CS8168: Cannot return local 'stock' by reference because it is not a ref local
Program.cs(10,16): error CS8168: Cannot return local 'copy' by reference because it is not a ref local

İkinci örnek ilginç: items array'i çağıranın, yani güvenli. Ama var copy = items[0] element'i bir local'e kopyaladı ve artık döndürmeye çalıştığımız şey o local. Doğrusu return ref items[0];. Kabaca kural şu: bir field'ın, bir array element'inin, bir ref parametresinin ya da bunlardan türemiş bir ref local'in referansını döndürebilirsin; method'un kendi local'inin referansını döndüremezsin. C# spesifikasyonu bu analizi "ref safe context" kuralları olarak tanımlar ve span'lerin güvenli olmasının sebebi de bu kurallar.

ref readonly: Okumaya İzin, Yazmaya Değil

Bazen referans döndürmenin sebebi değiştirmek değil, kopyalamamaktır. Büyük bir struct'ı kopyasız vermek ama çağıranın onu değiştirmesine izin vermemek istiyorsan ref readonly döndürürsün:

var prices = new PriceTable(new[] { 9.99m, 12.50m });

ref readonly decimal first = ref prices.At(0);
Console.WriteLine(first);

first = 0m;
prices.At(1) = 0m;

public sealed class PriceTable
{
    private readonly decimal[] _prices;

    public PriceTable(decimal[] prices) => _prices = prices;

    public ref readonly decimal At(int index) => ref _prices[index];
}
Program.cs(6,1): error CS0131: The left-hand side of an assignment must be a variable, property or indexer
Program.cs(7,1): error CS8331: Cannot assign to method 'At' or use it as the right hand side of a ref assignment because it is a readonly variable

Çağıran değeri ref readonly decimal first ile kopyasız okuyabiliyor, ama ne local üzerinden ne de method çağrısı üzerinden yazabiliyor; iki atama da derlenmedi. Bu, bir collection'ın içini dışarı açarken kullanılan klasik yol. ReadOnlySpan<T>'in indexer'ı da ref readonly T döner.

in Parametreleri ve Gizli Kopya

Ref'in parametre tarafında üç keyword var: ref (okur ve yazar), out (yazmak zorunda) ve C# 7.2 ile gelen in. in, argümanı referansla ama salt okunur olarak geçirir. Fikir, büyük bir struct'ı her çağrıda kopyalamamak:

using System.Globalization;

var order = new OrderTotals(1200m, 18m, 50m, 25m);
Console.WriteLine(GrandTotal(in order).ToString(CultureInfo.InvariantCulture));
Console.WriteLine(GrandTotal(order).ToString(CultureInfo.InvariantCulture));

static decimal GrandTotal(in OrderTotals t)
{
    return t.WithTax() + t.Shipping - t.Discount;
}

public readonly struct OrderTotals
{
    public OrderTotals(decimal subtotal, decimal taxRate, decimal shipping, decimal discount)
    {
        Subtotal = subtotal;
        TaxRate = taxRate;
        Shipping = shipping;
        Discount = discount;
    }

    public decimal Subtotal { get; }
    public decimal TaxRate { get; }
    public decimal Shipping { get; }
    public decimal Discount { get; }

    public decimal WithTax() => Subtotal * (1 + TaxRate / 100);
}
1441.00
1441.00

OrderTotals dört decimal taşıyor, yani 64 byte. in ile geçirildiğinde kopyalanmaz, sadece referansı gider. Çağrı yerinde in yazmak isteğe bağlı; ikinci çağrı da aynı method'a gidiyor. Method içinde parametreye atama yapmaya çalışırsan compiler durdurur:

var order = new OrderTotals(1200m, 18m, 50m, 25m);
Reset(in order);

static void Reset(in OrderTotals t)
{
    t = default;
}

public readonly struct OrderTotals
{
    public OrderTotals(decimal subtotal, decimal taxRate, decimal shipping, decimal discount)
    {
        Subtotal = subtotal;
        TaxRate = taxRate;
        Shipping = shipping;
        Discount = discount;
    }

    public decimal Subtotal { get; }
    public decimal TaxRate { get; }
    public decimal Shipping { get; }
    public decimal Discount { get; }
}
Program.cs(6,5): error CS8331: Cannot assign to variable 't' or use it as the right hand side of a ref assignment because it is a readonly variable

Burada bir ayrıntı kritik: OrderTotals'ı readonly struct yaptım. Neden önemli olduğunu sıradan bir struct'la görelim:

var counter = new Counter();
Console.WriteLine(Use(in counter));
Console.WriteLine(Use(in counter));
Console.WriteLine(counter.Value);

counter.Next();
Console.WriteLine(counter.Value);

static int Use(in Counter c)
{
    c.Next();
    return c.Value;
}

public struct Counter
{
    public int Value { get; private set; }

    public void Next() => Value++;
}
0
0
0
1

Use içinde c.Next() çağırdık ama değer 0 kaldı ve orijinal counter da değişmedi. Compiler, in parametresinin değişmeyeceğine söz vermişti. Counter readonly olmadığı için Next()'in struct'ı değiştirip değiştirmediğini bilemez; sözünü tutmak için çağrıdan önce sessizce bir kopya oluşturur ve Next()'i o kopya üzerinde çağırır. Buna defensive copy denir. Hem davranış şaşırtıcı hem de in ile kurtulmaya çalıştığın kopya, her method çağrısında geri gelmiş olur.

Çözüm, in ile geçireceğin struct'ları readonly struct yapmak. O zaman compiler hiçbir member'ın state'i değiştirmediğini bilir ve kopya oluşturmaz. Struct'ı tamamen readonly yapamıyorsan, C# 8'den beri tek tek member'ları readonly işaretleyebilirsin. Pratik kural: küçük struct'lar (birkaç field) için in kullanma, değer olarak geçirmek zaten ucuz; büyük ve readonly struct'larda kullan.

ref struct: Kendi Span Benzeri Tipin

Bir önceki yazıda span'in bir class field'ında duramadığını görmüştük. Ama bir ref struct'ın field'ı olabilir, çünkü ref struct da aynı kurallara tabi: sadece stack'te yaşar. Bu, span'leri saran küçük, allocation'sız yardımcı tipler yazmanın yolu:

var tokens = new FieldReader("978-0441013593;Dune;9.99");

while (tokens.TryRead(out ReadOnlySpan<char> field))
    Console.WriteLine($"[{field.ToString()}]");

public ref struct FieldReader
{
    private ReadOnlySpan<char> _rest;
    private bool _done;

    public FieldReader(ReadOnlySpan<char> text)
    {
        _rest = text;
        _done = false;
    }

    public bool TryRead(out ReadOnlySpan<char> field)
    {
        if (_done)
        {
            field = default;
            return false;
        }

        int sep = _rest.IndexOf(';');
        if (sep < 0)
        {
            field = _rest;
            _done = true;
            return true;
        }

        field = _rest[..sep];
        _rest = _rest[(sep + 1)..];
        return true;
    }
}
[978-0441013593]
[Dune]
[9.99]

FieldReader heap'e hiç uğramadan satırı field'larına ayırıyor ve her field'ı span olarak veriyor. Span'in kısıtlarının hepsi onun için de geçerli: bir class'ın field'ı olamaz, boxing yapılamaz, lambda'da yakalanamaz, C# 10'da async method'da local olamaz, interface implemente edemez ve generic tip argümanı olamaz. Son ikisi C# 13 ile gevşedi (allows ref struct); C# 11 ise ref struct'lara ref field'lar ve scoped keyword'ünü getirdi. Bunları C# Sürümleri serisinde göreceğiz.

Sık Düşülen Tuzaklar

List'teki struct'ı değiştirmeye çalışmak. Array'in indexer'ı hücreyi referansla verir, List<T>'in indexer'ı ise normal bir property ve değerin kopyasını döner. Kopyayı değiştirmek anlamsız olduğu için compiler izin vermez:

var stock = new List<BookStock> { new("978-0441013593", 5) };
stock[0].Stock--;

public struct BookStock
{
    public BookStock(string isbn, int stock)
    {
        Isbn = isbn;
        Stock = stock;
    }

    public string Isbn { get; }
    public int Stock { get; set; }
}
Program.cs(2,1): error CS1612: Cannot modify the return value of 'List<BookStock>.this[int]' because it is not a variable

Çözümler: element'i değişkene al, değiştir, geri yaz (var s = stock[0]; s.Stock--; stock[0] = s;); ya da sıcak bir yoldaysan CollectionsMarshal.AsSpan ile listenin iç array'ine span olarak eriş. Ama bu ikinci yolun kendi tuzağı var:

using System.Runtime.InteropServices;

var stock = new List<BookStock>(capacity: 2)
{
    new("978-0441013593", 5),
    new("978-0141439518", 3),
};

foreach (ref BookStock item in CollectionsMarshal.AsSpan(stock))
    item.Stock += 100;

Console.WriteLine($"{stock[0].Stock} {stock[1].Stock}");

ref BookStock first = ref CollectionsMarshal.AsSpan(stock)[0];
stock.Add(new BookStock("978-0316769488", 7));
Console.WriteLine(stock.Capacity);
first.Stock = 0;
Console.WriteLine($"{stock[0].Stock} {first.Stock}");

public struct BookStock
{
    public BookStock(string isbn, int stock)
    {
        Isbn = isbn;
        Stock = stock;
    }

    public string Isbn { get; }
    public int Stock { get; set; }
}
105 103
4
105 0

foreach (ref BookStock item in ...) ile her element'i yerinde güncelledik, buraya kadar sorun yok. Sonra ilk element'e bir ref aldık ve listeye element ekledik. Kapasite 2 doluydu, liste 4 kapasiteli yeni bir iç array'e taşındı. first hâlâ eski array'i gösteriyor; ona yazdığımız 0, listeye hiç ulaşmadı. Hata yok, exception yok, sadece kaybolan bir güncelleme. Kural: CollectionsMarshal.AsSpan ile aldığın span ya da ref, liste değişmediği sürece geçerlidir. Aynı şey, iç array'ini değiştirebilen her class'tan döndürdüğün ref için de geçerli.

Her yere ref koymak. Ref local ve ref return okunabilirliği düşürür: kodu okuyan biri her atamanın kopya mı takma ad mı olduğunu düşünmek zorunda kalır. Küçük struct'larda ve class'larda (zaten referans tipler) kazanç yoktur. Bu araçlar ölçülmüş bir performans sorununa ya da yerinde güncelleme ihtiyacına cevaptır, varsayılan stil değil.

Küçük Bir Deneme

★☆☆ Açılıştaki örnekte copy satırını ref BookStock copy = ref books[0]; olarak değiştir. Çıktı ne olur? Sonra sağ taraftaki ref'i sil ve compiler'ın verdiği hatayı oku.

★★☆ Counter'ı readonly struct yap. Next() için compiler ne diyor? Next()'i, yeni bir Counter döndüren public Counter Increment() olarak yeniden yaz ve Use'u buna göre düzelt.

★★★ Inventory'ye Add method'u ekle; array dolunca iki katı büyüklükte yeni bir array'e kopyalasın. Sonra Find ile bir ref al, Add çağır ve ref üzerinden stoğu değiştir. Güncelleme kayboluyor mu? Bu riski API tasarımında nasıl önlersin (örneğin ref yerine bir Update(string isbn, Func<BookStock, BookStock> change) method'u)?

Ref'lerle managed belleğin içinde kopyasız gezdik ve compiler her adımı denetledi. Bir sonraki yazıda bu güvenli alanın kenarına çıkıyoruz: native kütüphaneleri çağırmak için P/Invoke, pointer'lar ve unsafe kod.