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

C# Method'ları: Parametreler, Overloading, ref/out/in ve params

Mert Özen 7 Kas 2026 11 dk 2 görüntülenme
C# Method'ları: Parametreler, Overloading, ref/out/in ve params

C#'ta method yazmanın temel araçları: değerle geçen parametreler ve dönüş değeri, overloading ve signature, optional ve named argümanlar, ref, out ve in, params, local function'lar ve neden overload edilemedikleri, expression-bodied member'lar.

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

Bir kitabın fiyatına yüzde 10 indirim uygulayan küçük bir method yazdık ve 100 TL'lik fiyatı ona verdik. Method'dan sonra price değişkeni kaç olur?

decimal price = 100m;
ApplyDiscount(price);
Console.WriteLine(price);

static void ApplyDiscount(decimal amount)
{
    amount = amount * 0.9m;
}
100

Cevap 100. ApplyDiscount çağrıldığında price'ın değeri amount parametresine kopyalandı. Method kendi kopyasını 90 yaptı, ama o kopya method bitince yok oldu; çağıran taraftaki price'a hiç dokunulmadı. C#'ta parametreler varsayılan olarak değerle geçer (pass by value). Bir method'un sonucunu dışarı vermesinin doğal yolu parametresini değiştirmek değil, bir değer döndürmektir. Bu yazıda method yazmanın bütün temel araçlarını göreceğiz ve sonunda bu davranışı bilerek değiştirmenin yollarına, ref ve out'a da geleceğiz.

Bir Method'un Anatomisi

Gerçek bir projede method'lar bir class'ın içinde yaşar. Class'ları C# ile Nesne Yönelimli Programlama (OOP) serisinde ayrıntılı göreceğiz; şimdilik static class'ı, ilgili method'ları bir araya toplayan bir kutu gibi düşün:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

decimal price = 100m;
decimal discounted = Pricing.ApplyDiscount(price, 0.10m);
Console.WriteLine($"{price} -> {discounted}");

Pricing.PrintLabel("Dune", discounted);

static class Pricing
{
    public static decimal ApplyDiscount(decimal price, decimal rate)
    {
        if (rate < 0m || rate > 1m)
        {
            throw new ArgumentOutOfRangeException(nameof(rate));
        }

        return price * (1m - rate);
    }

    public static void PrintLabel(string title, decimal price)
    {
        Console.WriteLine($"{title}: {price:0.00}");
    }
}
100 -> 90.00
Dune: 90.00

ApplyDiscount'un tanımını soldan sağa okuyalım. public access modifier: bu method'u class dışından çağırabileceğimizi söylüyor. static method'un bir instance'a değil class'ın kendisine ait olduğunu belirtiyor; bu yüzden Pricing.ApplyDiscount(...) diye çağırıyoruz. Sonra dönüş tipi (decimal), method'un adı ve parantez içinde tipleriyle birlikte parametreler geliyor. Body'deki return sonucu döndürür ve method'u orada bitirir.

PrintLabel bir şey döndürmüyor, sadece ekrana yazıyor; dönüş tipi bu yüzden void. Method adları C#'ta PascalCase yazılır ve genellikle bir fiille başlar: ApplyDiscount, PrintLabel, CalculateTotal. Parametreler ise camelCase.

Önceki yazılarda top-level kodun altına yazdığımız static decimal DiscountRate(...) gibi method'lar aslında local function'lardı; birazdan aralarındaki farkı göreceğiz.

Overloading

Aynı adı taşıyan ama parametreleri farklı olan birden fazla method yazabilirsin. Buna overloading denir:

Console.WriteLine(Pricing.Total(10m, 3));
Console.WriteLine(Pricing.Total(new[] { 10m, 20m }));

static class Pricing
{
    public static decimal Total(decimal unitPrice, int quantity) => unitPrice * quantity;

    public static decimal Total(decimal[] prices)
    {
        decimal sum = 0m;
        foreach (decimal price in prices)
        {
            sum += price;
        }
        return sum;
    }
}
30
30

Compiler hangi method'un çağrılacağına argümanların sayısına ve tiplerine bakarak karar verir. Method'un adı ve parametre tipleri birlikte onun signature'ını (imza) oluşturur. Dönüş tipi signature'ın parçası değildir; yalnızca dönüş tipi farklı iki method yazamazsın:

Console.WriteLine(Pricing.Total(10m, 3));

static class Pricing
{
    public static decimal Total(decimal unitPrice, int quantity) => unitPrice * quantity;

    public static int Total(decimal unitPrice, int quantity) => (int)(unitPrice * quantity);
}
Program.cs(7,23): error CS0111: Type 'Pricing' already defines a member called 'Total' with the same parameter types

Overload'ları çok benzer tiplerle kurmak da başını ağrıtabilir. int, hem decimal'a hem double'a implicit olarak dönüşebildiği için compiler aşağıdaki çağrıda birini seçemiyor:

Console.WriteLine(Pricing.Round(10));

static class Pricing
{
    public static decimal Round(decimal value) => Math.Round(value, 2);
    public static decimal Round(double value) => (decimal)Math.Round(value, 2);
}
Program.cs(1,27): error CS0121: The call is ambiguous between the following methods or properties: 'Pricing.Round(decimal)' and 'Pricing.Round(double)'

Bu tür bir belirsizlikte çağıran taraf 10m ya da 10.0 yazarak tercihini açıkça belirtmek zorunda kalır. İyi bir overload seti, çağıranın hangi method'a gittiğini düşünmesine gerek bırakmaz.

Optional ve Named Argümanlar

Bir parametreye default değer verirsen o parametre optional olur ve çağıran onu yazmayabilir:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Console.WriteLine(Shipping.Fee(2.0m));
Console.WriteLine(Shipping.Fee(2.0m, express: true));
Console.WriteLine(Shipping.Fee(weightKg: 2.0m, country: "DE"));
Console.WriteLine(Shipping.Fee(2.0m, "DE", true));

static class Shipping
{
    public static decimal Fee(decimal weightKg, string country = "TR", bool express = false)
    {
        decimal fee = country == "TR" ? 30m : 120m;
        fee += weightKg * 5m;
        return express ? fee * 2m : fee;
    }
}
40.0
80.0
130.0
260.0

İkinci satırda express: true bir named argüman. Aradaki country parametresini atlayıp doğrudan express'e değer verebildik. Named argümanın asıl faydası okunaklılık: son satırdaki Fee(2.0m, "DE", true) çağrısında true'nun ne anlama geldiğini method'a bakmadan bilemezsin, express: true ise kendini anlatıyor. Bir method'a bool geçiyorsan çağıran tarafta named argüman kullanmak iyi bir alışkanlıktır.

İki kural var. Optional parametreler zorunlu olanlardan sonra gelir ve default değer compile time'da bilinen bir sabit olmalıdır:

Console.WriteLine(Shipping.Fee(2.0m, "DE"));

static class Shipping
{
    public static decimal Fee(decimal weightKg = 1.0m, string country) => 0m;

    public static DateTime Deadline(DateTime start = DateTime.Today) => start;
}
Program.cs(5,70): error CS1737: Optional parameters must appear after all required parameters
Program.cs(7,54): error CS1736: Default parameter value for 'start' must be a compile-time constant

DateTime.Today her gün değişen bir değer, bu yüzden default olamaz. Böyle durumlarda parametreyi DateTime? yapıp method'un içinde start ?? DateTime.Today yazmak yaygın bir çözüm.

ref, out ve in

Önce Tahmin Et bölümündeki davranışı bilerek değiştirmek istersen ref kullanırsın. ref parametresi değerin kopyasını değil, çağıranın değişkeninin kendisini alır:

int stock = 5;
Inventory.Restock(ref stock, 10);
Console.WriteLine(stock);

if (Inventory.TryReserve(stock, 3, out int remaining))
{
    Console.WriteLine($"Reserved, {remaining} left");
}

if (!Inventory.TryReserve(stock, 50, out _))
{
    Console.WriteLine("Not enough stock");
}

static class Inventory
{
    public static void Restock(ref int stock, int amount)
    {
        stock += amount;
    }

    public static bool TryReserve(int stock, int quantity, out int remaining)
    {
        if (quantity > stock)
        {
            remaining = stock;
            return false;
        }

        remaining = stock - quantity;
        return true;
    }
}
15
Reserved, 12 left
Not enough stock

Restock içinde stock += amount yazdığımızda çağıran taraftaki stock değişti. ref'in hem tanımda hem çağrıda yazılması zorunlu; çağıran taraf, değişkeninin değişebileceğini bilerek bu kapıyı açar:

int stock = 5;
Inventory.Restock(stock, 10);

static class Inventory
{
    public static void Restock(ref int stock, int amount) => stock += amount;
}
Program.cs(2,19): error CS1620: Argument 1 must be passed with the 'ref' keyword

out ise method'un çağırana ek bir sonuç vermesinin yolu. .NET'teki int.TryParse ile tanıdığın "Try" kalıbı tam olarak budur: method başarıyı bool olarak döndürür, asıl sonucu out parametresiyle verir. out int remaining çağrı sırasında değişkeni tanımlıyor; sonuçla ilgilenmiyorsan out _ yazıp atabilirsin. Compiler, out parametresine method'un her çıkış yolunda değer atandığından emin olmak ister:

Console.WriteLine(Inventory.TryReserve(5, 3, out int left));

static class Inventory
{
    public static bool TryReserve(int stock, int quantity, out int remaining)
    {
        if (quantity > stock)
        {
            return false;
        }

        remaining = stock - quantity;
        return true;
    }
}
Program.cs(9,13): error CS0177: The out parameter 'remaining' must be assigned to before control leaves the current method

in parametresi de referansla geçer ama read-only'dir; method onu değiştiremez:

Console.WriteLine(Pricing.WithTax(100m, 0.18m));

static class Pricing
{
    public static decimal WithTax(in decimal price, decimal rate)
    {
        price = price * (1m + rate);
        return price;
    }
}
Program.cs(7,9): error CS8331: Cannot assign to variable 'price' or use it as the right hand side of a ref assignment because it is a readonly variable

in'in amacı büyük struct'ları kopyalamadan geçirmek; decimal gibi küçük tiplerde bir kazancı yok ve günlük kodda nadiren görürsün. ref için de benzer bir uyarı yapayım: birden fazla sonuç döndürmek için ref ya da out yerine çoğu zaman bir tuple ya da küçük bir tip döndürmek daha okunaklıdır. out'u "Try" kalıbına, ref'i gerçekten gerektiği yerlere sakla.

params

Değişken sayıda argüman alan bir method için params kullanırsın:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Console.WriteLine(Pricing.Sum());
Console.WriteLine(Pricing.Sum(12.5m));
Console.WriteLine(Pricing.Sum(12.5m, 8.9m, 21m));

decimal[] cart = { 5m, 5m };
Console.WriteLine(Pricing.Sum(cart));

static class Pricing
{
    public static decimal Sum(params decimal[] prices)
    {
        decimal total = 0m;
        foreach (decimal price in prices)
        {
            total += price;
        }
        return total;
    }
}
0
12.5
42.4
10

params decimal[] prices, çağıranın argümanları virgülle sıralamasına izin verir; compiler onları senin yerine bir array'e koyar. Hiç argüman vermezsen boş bir array gelir, null değil. Elinde hazır bir array varsa onu doğrudan da verebilirsin. params parametresi listenin en sonunda olmalı ve bir method'da yalnızca bir tane olabilir. Console.WriteLine ve string.Format'ın değişken sayıda değer alabilmesi de bu sayede. C# 13 ile params yalnızca array'lerle değil List<T>, Span<T> gibi collection'larla da çalışır hâle geldi; bunu C# Sürümleri serisinde göreceğiz.

Local Function'lar

Bir method'un içinde, sadece o method'un kullanacağı küçük bir yardımcı method tanımlayabilirsin:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

decimal[] prices = { 12.50m, 8.90m, 21.00m };
Console.WriteLine(Report(prices, 0.18m));

static string Report(decimal[] prices, decimal taxRate)
{
    decimal total = 0m;
    foreach (decimal price in prices)
    {
        total += WithTax(price);
    }
    return $"{prices.Length} books, total {total:0.00}";

    decimal WithTax(decimal price) => price * (1m + taxRate);
}
3 books, total 50.03

WithTax, Report'un içinde yaşıyor ve dışarıdan görünmüyor. Dikkat et, taxRate'i parametre olarak almadı; içinde bulunduğu method'un değişkenine doğrudan erişti. Buna capture denir. Kullanışlı ama gizli bir bağımlılık da yaratır; static local function yazarak bunu yasaklayabilirsin:

Console.WriteLine(Total(new[] { 10m, 20m }, 0.18m));

static decimal Total(decimal[] prices, decimal taxRate)
{
    return Sum() * (1m + taxRate);

    static decimal Sum()
    {
        decimal total = 0m;
        foreach (decimal price in prices)
        {
            total += price;
        }
        return total;
    }
}
Program.cs(10,35): error CS8421: A static local function cannot contain a reference to 'prices'.

static yazdığında local function dış değişkenlere erişemez, ihtiyaç duyduğu her şeyi parametre olarak almak zorunda kalır. Bu da onu bağımsız ve test edilmesi kolay hâle getirir.

Top-level statements ile yazdığın dosyada tanımladığın her fonksiyon da aslında bir local function. Bunun önemli bir sonucu var: local function'lar overload edilemez.

Console.WriteLine(Describe(3));
Console.WriteLine(Describe("Dune"));

string Describe(int quantity) => $"{quantity} copies";
string Describe(string title) => $"Title: {title}";
Program.cs(2,28): error CS1503: Argument 1: cannot convert from 'string' to 'int'
Program.cs(5,8): error CS0128: A local variable or function named 'Describe' is already defined in this scope
Program.cs(5,8): warning CS8321: The local function 'Describe' is declared but never used

Aynı isimle iki fonksiyon istiyorsan onları bir class'a taşı; class içindeki method'lar overload edilebilir. Bu, örneklerimizde neden static class Pricing kullandığımızın da cevabı.

Expression-Bodied Member'lar

Body'si tek bir expression'dan oluşan method'ları => ile kısaca yazabilirsin. Aynı syntax property'lerde de çalışır:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

var book = new Book("Dune", 12.50m, 3);
Console.WriteLine(book.Label());
Console.WriteLine(book.IsAvailable);

class Book
{
    public Book(string title, decimal price, int stock)
    {
        Title = title;
        Price = price;
        Stock = stock;
    }

    public string Title { get; }
    public decimal Price { get; }
    public int Stock { get; }

    public bool IsAvailable => Stock > 0;

    public string Label() => $"{Title} ({Price:0.00})";
}
Dune (12.50)
True

Label() bir method, IsAvailable ise her okunduğunda yeniden hesaplanan bir property. İkisi de => ile yazıldı. Bu yazım, yazının başından beri kullandığımız Total ve DiscountRate method'larında da vardı. Kural basit: body tek satırlık ve okunaklı bir expression'sa => kullan; içine if, döngü ya da birden fazla adım girmeye başladıysa normal süslü parantezli body'ye dön. Constructor ve property'leri OOP serisinde ayrıntılı göreceğiz.

Sık Düşülen Tuzaklar

Parametreyi değiştirip çağıranın da değiştiğini sanmak. Yazının başındaki örnek bu; sonucu return ile döndür. Ama array gibi reference type'larda method'un element'leri değiştirmesi çağırana yansır, önceki yazıda ResetStock örneğinde görmüştük. Bu farkın tamamı bir sonraki yazının konusu.

Optional parametrenin default değerini sonradan değiştirmek. Compiler default değeri çağıran tarafın koduna gömer. Bir library'de country = "TR"'yi "DE" yaparsan, library'yi kullanan ama yeniden derlenmeyen kod eski değeri kullanmaya devam eder. Herkese açık API'lerde default değerleri sabit bir contract gibi düşün.

Çok sayıda parametre ve bool bayrağı. Fee(2.0m, "DE", true, false, true) gibi bir çağrı, method'un çok fazla iş yaptığının işaretidir. Ya method'u böl ya da parametreleri anlamlı bir tipte topla.

Her şeyi tek bir dev method'a yazmak. Bir method'un adını "ve" kullanmadan söyleyemiyorsan (ValidateAndSaveAndNotify) muhtemelen birden fazla iş yapıyordur. Küçük, adı ne yaptığını anlatan method'lar hem okunur hem test edilir.

Küçük Bir Deneme

★☆☆ İki overload'u olan bir FormatPrice method'u yaz: biri decimal alıp "12.50" biçiminde yazsın, diğeri decimal ve para birimi string'i alıp "12.50 EUR" döndürsün. Culture'ı InvariantCulture olarak ayarlamayı unutma.

★★☆ bool TryParseIsbn(string text, out string isbn) method'unu yaz: tireleri silsin, kalan metin 13 haneli bir sayıysa true döndürsün. out _ ile ve gerçek bir değişkenle ayrı ayrı çağır.

★★★ params alan bir Average method'u yaz. Hiç argüman verilmezse ne döndürmeli: 0 mı, exception mı? İki seçeneği de dene ve hangisinin çağıran için daha dürüst olduğunu tartış.

Bu yazıda int ve decimal method'a kopyalanarak geçerken array'in referansıyla geçtiğini gördük, ama sebebini hep sonraya bıraktık. Bir sonraki yazıda value type ve reference type farkını ele alıyoruz: atama ve kopyalama semantiği, method'lara geçirmek, string'in özel durumu ve stack ile heap'i doğru ve abartısız anlatmak.