C# Programlama


C# 14 ve .NET 10 ekseninde CLR, tür sistemi, nesne yönelimli tasarım, genel türler, LINQ, async/await, iş parçacıkları, atomiklik, kanallar, asenkron akışlar, bellek yönetimi, Pipelines, G/Ç, ağ, yansıma ve üretim performansını örneklerle ele alan kapsamlı ders notu.
C#; statik tür güvenliği, nesne yönelimli model, fonksiyonel ifade araçları, asenkron programlama ve çalışma zamanı hizmetlerini aynı dilde birleştirir. Dili doğru öğrenmek için üç katmanı ayırmak gerekir: C# kaynak dilidir, derleyici kaynak kodu ara dile ve üst veriye dönüştürür, .NET çalışma zamanı ise assembly'leri yükler ve kodu yürütür.
C# sözdizimi, dil semantiği ve çalışma zamanı maliyetleri birlikte değerlendirilmelidir. Temel dil yapıları ilerleyen bölümlerde genel türler, LINQ, eşzamanlılık, bellek, ağ ve reflection gibi konularla aynı model içinde birleşir.
Ünite 1: .NET Çalışma Modeli, Türler ve Kontrol Akışı
1.1. C#, .NET ve CLR
Modern .NET, yalnız Windows'a bağlı eski .NET Framework modeli değildir. .NET 10 uzun süreli destek sürümüdür ve C# 14 ile birlikte kullanılabilir. CLR; assembly yükleme, JIT derleme, çöp toplama, istisna aktarımı, iş parçacığı yönetimi ve tür güvenliği gibi çalışma zamanı hizmetlerini sağlar.
.cs kaynak
|
v
C# derleyici
|
v
IL + metadata
|
v
CLR yükleme / doğrulama
|
+--> JIT --------> yerel kod
|
+--> AOT yolu ---> yerel imageC# kaynak kodu doğrudan işlemci komutlarından oluşmaz. Derleme sonucunda assembly içinde Common Intermediate Language ve üst veri bulunur. Çalışma zamanı ihtiyaç duyulan metotları yerel makine koduna dönüştürebilir. Native AOT ise uygun uygulamalarda yürütülebilir kodu önceden üretme seçeneği sunar.
using System;
public static class Program
{
public static void Main()
{
Console.WriteLine($".NET: {Environment.Version}");
Console.WriteLine($"64 bit süreç: {Environment.Is64BitProcess}");
}
}Yönetilen kod bellek güvenliği ve çalışma zamanı hizmetlerinden yararlanır. Bununla birlikte dosya tanıtıcısı, socket, veritabanı bağlantısı veya yerel tanıtıcı gibi dış kaynakların ömrünü çöp toplayıcıya bırakmak doğru değildir.
1.2. Derleme, SDK ve Proje Modeli
Güncel komut satırı akışı dotnet CLI üzerine kuruludur:
dotnet new console -n Ornek
dotnet build Ornek
dotnet run --project OrnekSDK hedef framework, dil sürümü, nullable analiz ve optimizasyon gibi seçenekleri proje dosyasından yönetir. Üretimde kullanılan dil özelliği, yalnız geliştiricinin IDE sürümüne değil teslim hattındaki SDK ve derleyici sürümüne de bağlıdır.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>1.3. Tür Sistemi: Değer ve Referans Semantiği
C# tür sistemi, yalnız değişkenin hangi değeri tutacağını değil kopyalama, null olabilme, boxing ve bellek davranışını da belirler. int, double, bool, char, decimal ve kullanıcı tanımlı struct türleri değer türleridir. Sınıflar, diziler, delegate'ler ve string referans türüdür.
decimal, ondalık finansal hesaplarda ikili kayan noktanın temsil hatalarını azaltmak için tasarlanmıştır. double ise bilimsel ve genel sayısal hesaplamalarda daha yaygın ve hızlıdır.
decimal net = 1250.75m;
decimal taxRate = 0.20m;
decimal total = net * (1m + taxRate);
int count = 42;
long widened = count;
checked
{
int narrowed = checked((int)3_000_000_000L);
Console.WriteLine(narrowed);
}Son örnek OverflowException üretir. checked taşmanın sessizce sarmalanmasının kabul edilmediği hesaplarda önemlidir.
1.4. Nullable Türler ve Null Güvenliği
Nullable reference types çalışma zamanı özelliği değil, derleyici tarafından yapılan statik analizdir. string null olmaması beklenen, string? ise null olabilen referansı ifade eder. Bu ayrım API sözleşmesini görünür kılar.
static int LengthOrZero(string? value)
{
return value?.Length ?? 0;
}
string? input = Console.ReadLine();
Console.WriteLine(LengthOrZero(input));Null-forgiving ! operatörü null kontrolü yapmaz; yalnız derleyici uyarısını bastırır. Bu nedenle gerçek bir invariant tarafından desteklenmediği sürece kullanılmamalıdır.
1.5. Operatörler, Kısa Devre ve Bit İşlemleri
&& ve || kısa devrelidir; sağ taraf yalnız gerektiğinde değerlendirilir. &, |, ^, ~, << ve >> bit düzeyinde protokol alanı, maske ve düşük seviye veri işleme gibi durumlarda kullanılır.
const byte Valid = 0b0000_0001;
const byte Encrypted = 0b0000_0010;
byte flags = Valid | Encrypted;
bool valid = (flags & Valid) != 0;
bool encrypted = (flags & Encrypted) != 0;
Console.WriteLine($"{valid} {encrypted}");Bit alanları için anlamlı sabitler veya [Flags] enum kullanmak çıplak sayılardan daha güvenlidir.
1.6. Karar Yapıları ve Örüntü Eşleme
Klasik if ve switch hâlâ temel yapılardır; modern C# bunları örüntü eşleme ile genişletir. Tür, sabit, ilişki ve property pattern'leri karar mantığını daha bildirisel yazmayı sağlar.
static string Classify(int latencyMs) => latencyMs switch
{
< 0 => throw new ArgumentOutOfRangeException(nameof(latencyMs)),
< 20 => "çok düşük",
< 100 => "düşük",
< 500 => "orta",
_ => "yüksek"
};
Console.WriteLine(Classify(87));Örüntü eşleme, uzun if-else zincirini kısaltabilir; ancak iş kuralı büyüdükçe karar tablosu veya ayrı politika nesnesi daha sürdürülebilir olabilir.
1.7. Döngüler ve Enumerator Modeli
for, sayaç veya indeks gerektiğinde; foreach, koleksiyonun elemanları üzerinde ilerlerken; while koşul başta, do-while koşul sonda değerlendirildiğinde uygundur.
int[] samples = [12, 18, 21, 17, 25];
long sum = 0;
foreach (int sample in samples)
{
sum += sample;
}
double average = (double)sum / samples.Length;
Console.WriteLine(average);foreach sözdizimi IEnumerable<T> ile sınırlı değildir; uygun GetEnumerator pattern'ini sağlayan türler de kullanılabilir.
1.8. Metotlar, Parametre Aktarımı ve ref Ailesi
C# varsayılan olarak argümanı değer olarak aktarır. Referans türünde bu değer nesnenin kendisi değil referansın kopyasıdır. ref, out ve in parametre aktarım semantiğini değiştirir.
static bool TryNormalize(int value, out double normalized)
{
if (value is < 0 or > 100)
{
normalized = 0;
return false;
}
normalized = value / 100.0;
return true;
}
if (TryNormalize(73, out double result))
{
Console.WriteLine(result);
}Çok sayıda değer döndürmek için out yerine tuple veya küçük bir record/struct çoğu zaman daha okunabilirdir. ref özellikle kopyalama maliyeti yüksek değer türlerinde ve düşük seviyeli API'lerde anlamlıdır.
Ünite 2: Nesne Modeli, Koleksiyonlar ve Dil Soyutlamaları
Nesne modeli kurulurken önce korunacak durum ve bu durumu değiştirmeye yetkili işlemler belirlenir. Değişmez veri taşıyan türler ile kimliği ve yaşam döngüsü bulunan varlıklar farklı tasarım gerekçelerine sahiptir. Koleksiyon seçimi, anahtarla erişim, sıralama ve eşzamanlı kullanım örüntülerine göre yapılmalıdır. Eşitlik ve hash davranışı tanımlandıktan sonra nesnenin koleksiyonda tutulduğu sürede hangi alanlarının değişebileceği ayrıca kontrol edilir.
2.1. Sınıflar, Kapsülleme ve Nesne Ömrü
Alanları doğrudan herkese açmak yerine invariant'ı koruyan property ve metotlar tercih edilir. Kurucu, nesnenin geçerli başlangıç durumunu kurmalıdır.
public sealed class RateLimiter
{
private int _limit;
public RateLimiter(int limit)
{
Limit = limit;
}
public int Limit
{
get => _limit;
set => _limit = value > 0
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}C# 14 field anahtar sözcüğüyle compiler-generated backing field üzerinde doğrulama yapılmasını kolaylaştırır:
public sealed class Endpoint
{
public string Name
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Ad boş olamaz.", nameof(value))
: value.Trim();
} = "default";
}Bu ikinci örnek C# 14 gerektirir.
2.2. Record, Eşitlik ve Değişmezlik
Referans eşitliği ile değer eşitliği farklıdır. Normal sınıflarda == varsayılan olarak çoğunlukla referans kimliğiyle ilgilidir; record türleri ise veri taşıyan modeller için değer-temelli equality üretir.
public sealed record SensorReading(string Source, long Sequence, double Value);
SensorReading a = new("A1", 7, 42.5);
SensorReading b = new("A1", 7, 42.5);
Console.WriteLine(a == b);
SensorReading c = a with { Sequence = 8 };
Console.WriteLine(c);Kendi sınıfınızda equality tanımlıyorsanız Equals ve GetHashCode sözleşmeleri birlikte düşünülmelidir. Hash tabanlı koleksiyonlarda değişebilir alanlardan hash üretmek ciddi hatalara yol açabilir.
2.3. Kalıtım, Bileşim ve Çok Biçimlilik
Kalıtım bir "is-a" ilişkisini temsil etmelidir. Yalnız kod tekrarını azaltmak için hiyerarşi kurmak kırılgan tasarıma neden olabilir. Davranış değişimini bileşim ve interface ile modellemek çoğu durumda daha esnektir.
public interface IFormatter
{
string Format(double value);
}
public sealed class FixedFormatter : IFormatter
{
public string Format(double value) => value.ToString("F2");
}
public sealed class SampleWriter
{
private readonly IFormatter _formatter;
public SampleWriter(IFormatter formatter)
{
_formatter = formatter;
}
public string Write(double value) => _formatter.Format(value);
}virtual/override dinamik dispatch sağlar. sealed hem tipin hem override zincirinin sınırlandırılmasında kullanılabilir.
2.4. Arayüz, Struct, Enum ve Kutulama
struct küçük, değer semantiği taşıyan ve mümkünse değişmez veri modelleri için uygundur. Büyük mutable struct'lar kopyalama maliyeti ve şaşırtıcı semantik üretir. readonly record struct bu tür modeller için güçlü bir seçenektir.
public enum PacketState : byte
{
Unknown,
Ready,
Sent,
Failed
}
public readonly record struct Coordinate(double X, double Y);
Coordinate p = new(12.5, 8.25);
PacketState state = PacketState.Ready;
Console.WriteLine($"{p} {state}");Bir değer türü object veya uyguladığı interface türüne dönüştürüldüğünde boxing oluşabilir. Sık çalışan sıcak yollarda gereksiz kutulama kaynaklı ayırma ve GC baskısı yaratabilir.
2.5. Diziler, Span<T> Ve Bellek Görünümü
Diziler sabit uzunluklu referans türleridir. Çok boyutlu gerçek diziler T[,], jagged diziler ise dizi dizisi T[][] biçimindedir. Performans duyarlı kodlarda Span<T> ve ReadOnlySpan<T> mevcut belleğin yeni bellek ayırma yapmadan dilimlenmesini sağlar.
arka bellek
+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+
^-----------^
Span<T>
Span<T> veriyi sahiplenmez; mevcut belleğe sınırlandırılmış görünüm sağlar.static int SumPositive(ReadOnlySpan<int> values)
{
int sum = 0;
foreach (int value in values)
{
if (value > 0)
{
sum += value;
}
}
return sum;
}
int[] values = [4, -2, 7, 3];
Console.WriteLine(SumPositive(values.AsSpan(1, 3)));Span<T> bir ref struct olduğundan heap'e taşınamaz ve yaşam süresi kuralları daha katıdır. Bu kısıtlar güvenli düşük-bellek ayırma kod üretmek içindir.
2.6. Dize, Biçimlendirme ve Kültür
string değişmezdir. Çok sayıda parçayı döngü içinde birleştirmek için StringBuilder, formatlama için interpolation veya uygun format API'leri kullanılabilir.
using System.Globalization;
double value = 12345.6789;
Console.WriteLine(value.ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine($"{value:N2}");Makine-makine protokollerinde kültürden bağımsız biçim (InvariantCulture) tercih edilmelidir. Kullanıcı arayüzünde ise kullanıcının kültürü korunmalıdır.
2.7. Genel Türler ve Koleksiyonlar
Eski non-generic ArrayList ve Hashtable yerine List<T>, Dictionary<TKey,TValue>, HashSet<T>, Queue<T> ve Stack<T> gibi generic koleksiyonlar tercih edilir. Böylece cast ihtiyacı ve kutulama (boxing) riski azalır.
var latest = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["A1"] = 12.4,
["B7"] = 19.8
};
latest["A1"] = 13.1;
if (latest.TryGetValue("a1", out double current))
{
Console.WriteLine(current);
}Koleksiyon seçimi erişim biçimine göre yapılmalıdır. List<T> indeks ve ardışık tarama, Dictionary<TKey,TValue> anahtar erişimi, HashSet<T> üyelik testi için uygundur.
2.8. LINQ: Sorgu Semantiği ve Ertelenmiş Yürütme
LINQ, koleksiyon işleme için ortak bir sorgu modeli sunar. IEnumerable<T> üzerindeki birçok operatör ertelenmiş yürütülür; sonuç enumerasyon başladığında üretilir.
int[] samples = [4, 11, 7, 21, 9, 16];
var query = samples
.Where(static x => x >= 10)
.OrderBy(static x => x)
.Select(static x => x * 2);
foreach (int item in query)
{
Console.WriteLine(item);
}ToArray, ToList, Count gibi terminal işlemler yürütmeyi tetikleyebilir. IQueryable<T> ise expression tree üzerinden sorguyu başka bir sisteme çevirebilir; bu nedenle bellek içi LINQ ile veritabanı LINQ'u aynı maliyet modeline sahip değildir.
2.9. Delegate, Lambda ve Olaylar
Delegate, imzalı çağrılabilir referanstır. Lambda, delegate veya expression tree üretmek için kısa sözdizimi sağlar. Event ise yayıncının abonelik kontrolünü koruduğu bildirim modelidir.
public sealed class ThresholdMonitor
{
public event EventHandler<double>? Exceeded;
public void Observe(double value)
{
if (value > 80)
{
Exceeded?.Invoke(this, value);
}
}
}
var monitor = new ThresholdMonitor();
monitor.Exceeded += static (_, value) => Console.WriteLine($"Eşik aşıldı: {value}");
monitor.Observe(91.5);Event aboneliklerinde yayıncı uzun ömürlü, abone kısa ömürlü ise abonelik kaldırılmadığında bellek tutulması oluşabilir.
2.10. İstisna Yönetimi
İstisna, beklenmeyen veya normal dönüş değeriyle temsil edilmesi uygun olmayan hata durumları içindir. Akış kontrolü amacıyla sürekli istisna üretmek pahalıdır.
static int ParsePort(string text)
{
if (!int.TryParse(text, out int port) || port is < 1 or > 65535)
{
throw new ArgumentOutOfRangeException(nameof(text), "Geçerli bir TCP/UDP portu girilmelidir.");
}
return port;
}
try
{
Console.WriteLine(ParsePort("70000"));
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}Exception yeniden fırlatılırken throw; mevcut stack trace'i korur. throw ex; ise tanılama bilgisini bozabilir. finally kaynak temizleme için kullanılabilir; fakat using/await using çoğu sahiplik senaryosunu daha açık ifade eder.
Ünite 3: Kaynak Yönetimi, Asenkronluk ve Eşzamanlılık
Asenkron bir işlem hattında önce sahip olunan kaynaklar ve işlemin iptal edilmesi durumunda bunların nasıl bırakılacağı belirlenir. Beklenen gecikme ile paralel yapılabilecek iş ayrılır; aynı kaynağa sınırsız görev gönderilmesi işlem hacmini artırmak yerine kuyruk oluşturabilir. İptal, zaman aşımı ve hata yayılımının hangi katmanda gerçekleştiği açık olmalıdır. Başarım ölçülürken yalnız tamamlanan görev sayısı değil, bekleyen iş miktarı ve en uzun yanıt süreleri de izlenmelidir.
3.1. Kaynak Sahipliği: IDisposable Ve IAsyncDisposable
GC yönetilen belleği geri kazanır; dış kaynakların zamanında bırakılmasını garanti etmez. Deterministik temizlik için IDisposable kullanılır.
using FileStream stream = File.OpenRead("data.bin");
byte[] buffer = new byte[4096];
int read = stream.Read(buffer, 0, buffer.Length);
Console.WriteLine(read);Finalizer son çaredir; normal kaynak yönetiminin yerine geçmez. Native handle sahipliğinde SafeHandle tabanlı yaklaşım çoğu zaman doğrudan finalizer yazmaktan güvenlidir.
3.2. Dosya G/Ç ve Modern Asenkron API
FileStream, StreamReader, StreamWriter, BinaryReader, BinaryWriter hâlâ geçerlidir. Küçük ve basit işlemler için File sınıfı daha az kod gerektirir.
static async Task<long> CountLinesAsync(string path, CancellationToken cancellationToken)
{
long count = 0;
await using FileStream stream = new(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 64 * 1024,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
using var reader = new StreamReader(stream);
while (await reader.ReadLineAsync(cancellationToken) is not null)
{
count++;
}
return count;
}Büyük dosyada tüm içeriği ReadAllText ile belleğe almak yerine akış bazlı işleme daha öngörülebilir bellek kullanımı sağlar.
3.3. async/await, Task Ve İşin Sahipliği
async bir yöntemi arka plan iş parçacığına taşımaz. Yöntem, tamamlanmamış bir beklenebilir nesneye ulaşıncaya kadar çağıran bağlamda ilerler; await bekleme sırasında iş parçacığını bloke etmek yerine yöntemin devamını planlar. G/Ç ağırlıklı işlerde ölçeklenebilirlik kazancı buradan gelir.
çağıran
|
v
async metot ----> Task
|
await tamamlanmamış iş
|
+--> denetimi çağırana bırak
|
I/O tamamlanır
|
v
devam / state-machine -> kalan kodCPU ağırlıklı hesaplama ile G/Ç beklemesi ayrılmalıdır. Ağ veya dosya API'si zaten gerçek bir asenkron yöntem sunuyorsa onu Task.Run içine sarmak ek bir iş parçacığı tüketir fakat G/Ç'yi daha asenkron yapmaz. Task.Run, masaüstü arayüzünün yanıt verebilirliğini korumak veya CPU ağırlıklı işi iş parçacığı havuzuna taşımak gibi durumlarda anlamlıdır.
static async Task<string> DownloadWithDeadlineAsync(
HttpClient client,
Uri address,
CancellationToken cancellationToken)
{
Task<string> download = client.GetStringAsync(address, cancellationToken);
return await download.WaitAsync(
TimeSpan.FromSeconds(3),
cancellationToken);
}Bir Task, yalnız çalışma durumunu değil işin sahipliğini de temsil eder. Başlatılan görev ya await edilmeli, döndürülmeli, izlenen bir koleksiyonda tutulmalı veya sonuç/istisna davranışı açıkça sahiplenilmelidir. "Fire and forget" biçiminde kaybedilen görevler kapanış, hata ve kaynak ömrü bakımından belirsizlik yaratır.
async void normal uygulama mantığı için değil, dönüş türünü void olarak zorunlu kılan olay işleyicileri gibi sınırlı sözleşmeler için kullanılmalıdır.
3.4. Asenkronluk, Eşzamanlılık ve Paralellik
Bu üç kavram aynı değildir:
- Asenkronluk, beklerken çağıran iş parçacığını meşgul etmemeyi amaçlar.
- Eşzamanlılık, birden çok işin yaşam sürelerinin örtüşmesidir.
- Paralellik, birden çok işin gerçekten aynı anda farklı yürütme kaynaklarında ilerlemesidir.
Bir HTTP isteğini await etmek çoğu zaman paralel CPU çalışması üretmez. Buna karşılık Parallel.For çok çekirdekli CPU'da hesaplamayı paralelleştirebilir. Bir sunucu ise binlerce eşzamanlı isteği, her istek için ayrı bir iş parçacığı ayırmadan asenkron G/Ç ile taşıyabilir.
Mimari karar, "kaç thread var?" sorusundan önce işin doğasını belirlemelidir: CPU mu tüketiliyor, G/Ç mi bekleniyor, paylaşılan durum var mı, sıralama zorunlu mu ve eşzamanlılık sınırı nedir?
3.5. İş Parçacığı Modeli ve Doğrudan Thread Kullanımı
Thread, işletim sistemi tarafından zamanlanan bir yürütme yolunu temsil eder. Aynı süreçteki iş parçacıkları heap üzerindeki nesnelere ortak erişebilirken her iş parçacığının kendi çağrı yığını ve yürütme durumu vardır. Bu ortak adres alanı iletişimi ucuzlatır; yarış durumu ve görünürlük sorunlarını da mümkün kılar.
Modern .NET uygulamasında çoğu kısa veya orta ömürlü iş için doğrudan Thread oluşturmak yerine Task, iş parçacığı havuzu ve asenkron API'ler tercih edilir. Yine de uzun ömürlü, özel zamanlama/affinity gerektiren veya havuzun davranışından bilinçli biçimde ayrılması gereken sınırlı işlerde özel iş parçacığı gerekebilir.
var worker = new Thread(() =>
{
while (!stopping.IsCancellationRequested)
{
ProcessOneItem();
}
})
{
IsBackground = true,
Name = "dedicated-worker"
};
worker.Start();Bu örnek yalnız özel iş parçacığı gereksinimi varsa anlamlıdır. Genel amaçlı arka plan işi için Task.Run veya daha yüksek düzeyli bir kuyruk daha basit ve daha güvenlidir.
İş parçacığı önceliğini yükseltmek gerçek zaman garantisi sağlamaz. GC, işletim sistemi zamanlayıcısı, diğer süreçler ve donanım gecikmeleri hâlâ yürütme süresini etkiler.
3.6. İş Parçacığı Havuzu ve Görev Zamanlama
İş parçacığı oluşturmak yığın, çekirdek nesneleri ve zamanlayıcı maliyeti taşır. .NET iş parçacığı havuzu, çalışan iş parçacıklarını yeniden kullanır ve yük altında sayıyı dinamik biçimde ayarlar. Task.Run, varsayılan durumda CPU ağırlıklı kısa işi bu havuza kuyruğa almanın temel yollarından biridir.
Havuzun en önemli riski bloklama kaynaklı açlıktır. Çok sayıda havuz iş parçacığı .Result, .Wait(), uzun lock, eşzamanlı ağ çağrısı veya başka bloklayıcı API'lerde tutulursa yeni işler çalışacak iş parçacığı bekleyebilir. Bu nedenle doğal olarak asenkron olan yollar baştan sona asenkron tutulmalıdır.
static async Task<int[]> LoadAllAsync(
HttpClient client,
IReadOnlyList<Uri> addresses,
CancellationToken cancellationToken)
{
Task<int>[] jobs = addresses
.Select(async address =>
{
byte[] body = await client.GetByteArrayAsync(address, cancellationToken);
return body.Length;
})
.ToArray();
return await Task.WhenAll(jobs);
}Task.WhenAll işleri tek tek ardışık beklemek yerine tamamlanmalarını birlikte izler. Bununla birlikte giriş kümesi çok büyükse sınırsız eşzamanlılık üretmemek gerekir; semafor veya sınırlı kanal kullanılmalıdır.
3.7. Paylaşılan Durum, Yarış Durumu ve Atomiklik
Bir ifadenin kaynak kodda tek satır olması onun atomik olduğu anlamına gelmez. counter++, okuma, artırma ve yazma adımlarına ayrılabilir. İki yürütücü bu adımları iç içe geçirirse güncelleme kaybolabilir.
public sealed class UnsafeCounter
{
private int _value;
public void Increment() => _value++;
public int Value => _value;
}Yarış durumunun tehlikesi yalnız "yanlış sonuç" değildir; hata düşük olasılıklı ve zamana bağlı olduğundan testte görünmeyip üretimde ortaya çıkabilir.
İlk tercih paylaşılan değişebilir durumu azaltmaktır. Değişmez nesneler, mesajlaşma, sahipliği tek yürütücüde toplama ve sonuçları birleştirme, kilit ihtiyacını önemli ölçüde azaltabilir.
3.8. Interlocked, Volatile Ve Bellek Görünürlüğü
Basit sayaç ve durum geçişlerinde tam bir kilit yerine Interlocked kullanılabilir. Interlocked, tek bir bellek konumu üzerinde atomik read-modify-write işlemleri sağlar.
public sealed class AtomicCounter
{
private long _value;
public long Increment() => Interlocked.Increment(ref _value);
public long Value => Interlocked.Read(ref _value);
}Karşılaştır-ve-değiştir (CompareExchange) daha gelişmiş kilitsiz algoritmaların temel yapı taşlarından biridir:
static double Max(ref double location, double candidate)
{
while (true)
{
double snapshot = Volatile.Read(ref location);
if (snapshot >= candidate)
return snapshot;
double observed = Interlocked.CompareExchange(
ref location,
candidate,
snapshot);
if (observed == snapshot)
return candidate;
}
}volatile, belirli okuma/yazmaların görünürlüğü ve yeniden sıralanması üzerinde semantik sağlar; fakat x++ gibi bileşik işlemleri atomik yapmaz. Birden fazla alan arasında invariant korunacaksa çoğu zaman kilit veya daha yüksek düzeyli veri yapısı daha doğrudur.
Bellek sıralaması düşük seviyeli bir konudur. Önce lock, Interlocked, SemaphoreSlim, kanal veya concurrent koleksiyon gibi sözleşmesi açık yapılar kullanılmalı; açık bellek bariyerleri ancak ölçülmüş ve doğrulanmış gereksinimde düşünülmelidir.
3.9. lock, Monitor Ve System.Threading.Lock
Karşılıklı dışlama, bir kritik bölgeyi aynı anda tek yürütücünün değiştirmesini sağlar. .NET 9 ve C# 13'ten itibaren genel amaçlı kilitlemede ayrılmış bir System.Threading.Lock örneği kullanılabilir; C# 14/.NET 10 kodunda bu yaklaşım doğrudan uygundur.
public sealed class Balance
{
private readonly System.Threading.Lock _gate = new();
private decimal _value;
public void Add(decimal amount)
{
lock (_gate)
{
_value += amount;
}
}
public decimal Read()
{
lock (_gate)
{
return _value;
}
}
}Kilit nesnesi dışarı sızdırılmamalıdır. this, typeof(...) veya intern edilebilen dize sabitleri üzerinde kilitleme, dış kodun aynı kilide istemeden ortak olmasına neden olabilir.
lock gövdesi içinde await kullanılamaz. Bunun nedeni kritik bölümün iş parçacığına bağlı karşılıklı dışlama semantiğidir. Asenkron bekleme içeren bir karşılıklı dışlama gerekiyorsa SemaphoreSlim.WaitAsync gibi bir yapı kullanılmalıdır.
Monitor.Enter, Monitor.Exit, Monitor.Wait, Pulse ve PulseAll, klasik lock mekanizmasının daha düşük seviyeli biçimini sunar. Özel condition-variable benzeri protokoller kurulabilir; ancak yanlış kullanım kayıp sinyal ve karmaşık kilit etkileşimleri üretebildiği için daha yüksek düzeyli yapılar çoğu uygulamada daha güvenlidir.
3.10. Kilitlenme, Açlık, Canlı Kilit ve Çekişme
Bir program veri yarışı taşımadığı hâlde ilerleme hatası taşıyabilir.
Kilitlenme (deadlock) için tipik örnek, iki yürütücünün kilitleri ters sırayla almasıdır:
İş A: lock X -> lock Y
İş B: lock Y -> lock XBunu önlemenin en etkili yollarından biri sistem genelinde sabit bir kilit sırası tanımlamaktır. Ayrıca kilit altında G/Ç yapmak, kullanıcı kodu/callback çağırmak veya bilinmeyen süreli iş yürütmekten kaçınılmalıdır.
Açlık (starvation), bir yürütücünün sürekli başkalarına öncelik verildiği için ilerleyememesidir. Canlı kilit (livelock) ise yürütücülerin aktif biçimde birbirine yol verip yine de iş tamamlayamamasıdır.
Çekişme (contention) doğruluk hatası değildir fakat gecikmeyi büyütür. Kilit süresini kısaltmak, veriyi parçalara bölmek, immutable snapshot kullanmak veya tek sahipli mesajlaşmaya geçmek çekişmeyi azaltabilir.
3.11. SemaphoreSlim İle Eşzamanlılık Sınırı
Bir kaynak aynı anda birden çok işi kabul edebilir fakat sınırsız eşzamanlılık istemeyebilir. Veritabanı, uzak servis veya dosya sistemi gibi kaynakları korumak için SemaphoreSlim pratik bir sınırlandırıcıdır.
public sealed class LimitedFetcher
{
private readonly SemaphoreSlim _slots = new(initialCount: 8);
public async Task<byte[]> FetchAsync(
HttpClient client,
Uri address,
CancellationToken cancellationToken)
{
await _slots.WaitAsync(cancellationToken);
try
{
return await client.GetByteArrayAsync(address, cancellationToken);
}
finally
{
_slots.Release();
}
}
}Bu yapı hem await ile uyumludur hem de eşzamanlı çalışan iş sayısını sınırlar. Ancak sıra adaleti veya öncelik gibi daha güçlü gereksinimler varsa özel iş kuyruğu ya da kanal daha uygun olabilir.
3.12. Sinyalleme: Olay Tutamaçları, Geri Sayım ve Bariyer
Kilit bir kaynağın aynı anda kim tarafından kullanılacağını denetler; sinyalleme ise bir işin başka bir olay gerçekleşene kadar beklemesini koordine eder.
AutoResetEvent, her sinyalde bir bekleyeni serbest bırakır.ManualResetEvent/ManualResetEventSlim, açık kaldığı sürece bekleyenlerin geçmesine izin verir.CountdownEvent, belirli sayıda tamamlanma sinyali bekler.Barrier, çok aşamalı paralel algoritmada katılımcıları faz sınırlarında buluşturur.Mutex, gerektiğinde süreçler arası karşılıklı dışlama sağlayabilir.
Bu yapıların çoğu iş parçacığını bloke eder. Asenkron uygulamada bloke eden wait-handle yaklaşımını doğrudan taşımak yerine görev, kanal veya SemaphoreSlim.WaitAsync ile tasarım çoğu zaman daha ölçeklenebilirdir.
3.13. ReaderWriterLockSlim Ve Okuma Ağırlıklı Durum
Bir veri yapısı çok sık okunup seyrek değiştiriliyorsa aynı anda birden çok okuyucuya izin vermek teorik olarak çekişmeyi azaltabilir. ReaderWriterLockSlim, okuma, yazma ve yükseltilebilir okuma kipleri sunar.
public sealed class NameCache
{
private readonly ReaderWriterLockSlim _gate = new();
private readonly Dictionary<int, string> _items = [];
public bool TryGet(int id, out string? value)
{
_gate.EnterReadLock();
try
{
return _items.TryGetValue(id, out value);
}
finally
{
_gate.ExitReadLock();
}
}
public void Put(int id, string value)
{
_gate.EnterWriteLock();
try
{
_items[id] = value;
}
finally
{
_gate.ExitWriteLock();
}
}
}Okuyucu-yazar kilidi otomatik olarak basit locktan hızlı değildir. Kritik bölüm çok kısa, yazma sık veya çekişme düşükse ek yönetim maliyeti avantajı ortadan kaldırabilir. Ölçüm yapılmadan kullanılmamalıdır.
3.14. Üretici-tüketici Akışı, Channel<T> Ve Geri Basınç
Klasik producer/consumer problemi, bir tarafın iş üretirken diğer tarafın tüketmesidir. Modern .NET'te System.Threading.Channels, bu modeli asenkron okuma/yazma ve birden çok üretici/tüketici desteğiyle doğrudan sağlar.
üreticiler
| | |
v v v
+-----------+
| bounded |
| Channel<T>|
+-----------+
| | |
v v v
tüketiciler
üretim hızı > tüketim hızı
-> beklet / reddet / düşür / kapasiteyi sınırlaSınırlı kanal, üretim tüketimden hızlı olduğunda belleğin sınırsız büyümesini engeller ve geri basınç oluşturur:
using System.Threading.Channels;
Channel<Job> queue = Channel.CreateBounded<Job>(
new BoundedChannelOptions(256)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = false,
SingleWriter = false
});
static async Task ProduceAsync(
ChannelWriter<Job> writer,
IEnumerable<Job> jobs,
CancellationToken cancellationToken)
{
try
{
foreach (Job job in jobs)
await writer.WriteAsync(job, cancellationToken);
}
finally
{
writer.TryComplete();
}
}
static async Task ConsumeAsync(
ChannelReader<Job> reader,
CancellationToken cancellationToken)
{
await foreach (Job job in reader.ReadAllAsync(cancellationToken))
await ProcessAsync(job, cancellationToken);
}BlockingCollection<T> bloklayan producer/consumer akışı için hâlâ kullanılabilir. Baştan sona asenkron bir sistemde kanal çoğu zaman daha doğal bir bileşim sağlar.
3.15. Concurrent Koleksiyonlar ve Değişmez Snapshot'lar
ConcurrentDictionary<TKey,TValue>, ConcurrentQueue<T>, ConcurrentStack<T> ve benzeri türler temel koleksiyon işlemlerini eşzamanlı erişime uygun hâle getirir. Bu, koleksiyon etrafındaki bütün iş kuralının atomik olduğu anlamına gelmez.
Örneğin "varsa oku, koşulu kontrol et, başka sistemi çağır, sonra güncelle" zinciri birkaç thread-safe koleksiyon çağrısından oluşsa bile bütün olarak yarış durumuna açık olabilir. Atomiklik sınırı domain invariant'ına göre belirlenmelidir.
Okuma çok sık ve güncelleme seyrekse immutable koleksiyon veya değişmez snapshot değişimi güçlü bir alternatiftir:
private ImmutableDictionary<string, Endpoint> _snapshot =
ImmutableDictionary<string, Endpoint>.Empty;
public Endpoint? Find(string key)
{
ImmutableDictionary<string, Endpoint> snapshot =
Volatile.Read(ref _snapshot);
return snapshot.GetValueOrDefault(key);
}Yeni snapshot hazırlanıp tek referans değişimiyle yayımlanabilir. Bu yaklaşım okuyucularda kilit ihtiyacını azaltır.
3.16. İptal, Zaman Aşımı, Son Tarih ve Düzenli Kapanış
CancellationToken zorla sonlandırma değil, işbirlikçi iptal sinyalidir. Kod güvenli noktada token'ı denetler ve işlemi sonlandırır. İptal ile zaman aşımı da aynı kavram değildir: iptal çağıranın artık sonucu istemediğini, zaman aşımı ise belirlenen süre sınırının aşıldığını ifade edebilir.
Birden çok iptal kaynağı birleştirilebilir:
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
applicationStopping,
requestAborted,
deadline.Token);
await ProcessAsync(linked.Token);Düzenli kapanış sırası genellikle şöyledir:
- yeni iş kabulünü durdur,
- çalışan işlere iptal sinyali ver,
- sahip olunan görevlerin tamamlanmasını sınırlı süre bekle,
- kuyruk ve akışları tamamla,
- kalıcı kaynakları kapat,
- bitmeyen işleri tanılanabilir biçimde kaydet.
Thread.Abort bu modelin alternatifi değildir; modern .NET'te desteklenmez.
3.17. SynchronizationContext, ExecutionContext Ve Devamların Yeri
Masaüstü kullanıcı arayüzleri çoğu zaman denetimlerin yalnız UI iş parçacığından değiştirilmesini ister. SynchronizationContext, bir devamın belirli bağlama gönderilmesini sağlayabilen soyutlamadır. await, bulunduğu ortama göre bağlamı yakalayabilir ve devamı tekrar oraya döndürebilir.
Genel amaçlı kitaplık kodunda bağlam yakalamanın gerekli olmadığı yerlerde ConfigureAwait(false) kullanılabilir. Uygulama katmanında ise devamın UI bağlamına dönmesi bilinçli bir gereksinim olabilir. ASP.NET Core gibi ortamlarda klasik UI tarzı SynchronizationContext varsayımı yapılmamalıdır.
ExecutionContext farklı bir kavramdır; AsyncLocal<T> gibi ortam durumlarının mantıksal yürütme akışı boyunca taşınmasıyla ilgilidir. ThreadLocal<T> fiziksel iş parçacığına, AsyncLocal<T> ise mantıksal asenkron akışa bağlıdır.
Bu ayrım request kimliği, trace bilgisi, kültür veya güvenlik bağlamı taşınırken önemlidir.
3.18. TaskCompletionSource<T> İle callback/event Dünyasını Task Modeline Bağlamak
Bazı API'ler tamamlanmayı callback, event veya özel protokolle bildirir. TaskCompletionSource<T>, bu dış tamamlanmayı bir Task<T> olarak sunmaya yarar.
static Task<string> WaitForMessageAsync(
LegacyReceiver receiver,
CancellationToken cancellationToken)
{
var source = new TaskCompletionSource<string>(
TaskCreationOptions.RunContinuationsAsynchronously);
void Handler(object? sender, MessageEventArgs e)
{
receiver.MessageReceived -= Handler;
source.TrySetResult(e.Message);
}
receiver.MessageReceived += Handler;
cancellationToken.Register(() =>
{
receiver.MessageReceived -= Handler;
source.TrySetCanceled(cancellationToken);
});
return source.Task;
}TrySet... yöntemleri yarışan tamamlanma yollarında ikinci tamamlamayı exception'a dönüştürmez. RunContinuationsAsynchronously, tamamlayan thread üzerinde beklenmedik biçimde kullanıcı devamlarının iç içe çalışmasını önlemeye yardımcı olur.
Bu araç, zaten Task döndüren bir API'yi gereksiz yere sarmak için kullanılmamalıdır.
3.19. CPU Ağırlıklı Paralellik: Parallel, PLINQ ve Görevler
CPU ağırlıklı bağımsız işler çok çekirdeğe bölünebiliyorsa paralellik hız kazandırabilir. İş çok küçükse partitioning, scheduling ve birleştirme maliyeti kazancı aşabilir.
double[] input = CreateInput();
Parallel.For(
fromInclusive: 0,
toExclusive: input.Length,
i =>
{
input[i] = ExpensiveTransform(input[i]);
});PLINQ, in-memory sorguların bazılarını paralelleştirebilir:
var result = values
.AsParallel()
.Where(IsCandidate)
.Select(Score)
.Where(static score => score > 0.80)
.ToArray();Sıra korunması gerektiğinde AsOrdered ek maliyet getirir. Yan etkili sorgular, paylaşılan mutable durum veya G/Ç ağırlıklı işlemler PLINQ için doğal aday değildir.
Paralellik derecesi makinenin çekirdek sayısı kadar basit bir sabit olmayabilir; aynı süreçteki başka işler, NUMA/önbellek davranışı ve dış kaynak sınırları da dikkate alınmalıdır.
3.20. Zamanlayıcılar ve Periyodik İş
Timer türleri callback tabanlıdır ve iş önceki tur bitmeden tekrar tetiklenebiliyorsa örtüşen yürütmeler oluşabilir. Düzenli bir asenkron döngü için PeriodicTimer daha okunabilir bir model sunar:
static async Task RunPeriodicAsync(CancellationToken cancellationToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await RefreshSnapshotAsync(cancellationToken);
}
}Periyodik görevde "her beş saniyede bir başlat" ile "bir iş bittikten beş saniye sonra yeniden başlat" farklı zaman semantiklerine sahiptir. Uzun süren iş, saat kayması, çakışma ve kapanış davranışı açıkça belirlenmelidir.
Ünite 4: Ağ, Yansıtma, Düşük Seviye Özellikler ve Uygulama Katmanları
4.1. Tarihsel API'leri Güncel .NET'e Taşımak
Eski C# threading kaynaklarının kavramsal çekirdeği değerlidir; API tercihleri ise dönemine göre okunmalıdır.
Thread.Abortmodern .NET'te desteklenmez vePlatformNotSupportedExceptionüretir. İşbirlikçi iptal içinCancellationTokenkullanılır.Thread.SuspendveThread.Resumekullanımdan kaldırılmıştır; kilit ve koordinasyon yapılarıyla değiştirilmelidir.BackgroundWorker, olay tabanlı eski masaüstü asenkron modelinin parçasıdır; yeni koddaTask,asyncveawaitgenellikle daha doğrudur.- Delegate üzerindeki
BeginInvoke/EndInvoke, modern .NET'te desteklenmeyen .NET Framework-era uzaktan iletişim altyapısına bağlıdır. WebClient/WebRequestmerkezli ağ örnekleri yeni koddaHttpClientile değiştirilmelidir.AppDomainile izolasyon modern .NET'te genel uygulama izolasyon modeli değildir; gerektiğinde süreç, container veya uygun yükleme bağlamı kullanılır.- Bloklayan producer/consumer yapılarının yanında
Channel<T>ile asenkron geri basınç tasarlanabilir. - Genel amaçlı
objectkilitleri çalışmaya devam etse de C# 13+ ve .NET 9+ için ayrılmışSystem.Threading.Lockyeni genel kilitleme seçeneğidir.
Tarihsel örneklerin değeri, yarış durumu, paylaşılan durum, sinyalleme, atomiklik ve paralellik maliyetini göstermeleridir. Güncel uygulama ise bu kavramları daha yüksek düzeyli, iptal edilebilir ve asenkron bileşebilir API'lerle kurar.
4.2. HTTP ve Ağ İstemcisi
Eski WebRequest/WebClient merkezli kod yerine güncel istemci API'si HttpClient'tır. İstemci yaşam döngüsü önemlidir; her istek için yeni istemci üretmek bağlantı havuzunun avantajlarını yok edebilir.
using var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(5)
};
using HttpResponseMessage response = await client.GetAsync(
"https://example.com/",
HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
await using Stream body = await response.Content.ReadAsStreamAsync();
Console.WriteLine(response.StatusCode);Sunucu uygulamalarında IHttpClientFactory gibi yönetilen yaşam döngüsü mekanizmaları tercih edilebilir.
4.3. Yansıma, Öznitelik ve Üst Veri
Yansıma, tip ve üyeleri çalışma zamanında incelemeyi sağlar. Güçlüdür ancak doğrudan çağrıdan daha maliyetlidir ve trimming/Native AOT senaryolarında dinamik erişimler ayrıca düşünülmelidir.
[AttributeUsage(AttributeTargets.Class)]
public sealed class HandlerAttribute(string name) : Attribute
{
public string Name { get; } = name;
}
[Handler("telemetry")]
public sealed class TelemetryHandler;
Type type = typeof(TelemetryHandler);
HandlerAttribute? attribute = type
.GetCustomAttributes(typeof(HandlerAttribute), inherit: false)
.Cast<HandlerAttribute>()
.SingleOrDefault();
Console.WriteLine(attribute?.Name);Kaynak üreteçleri veya compile-time registration, yüksek performans ve AOT uyumluluğu gereken durumlarda yansımaya alternatif olabilir.
4.4. Güvensiz Kod, İşaretçi ve stackalloc
C# gerektiğinde unsafe kod ve işaretçi kullanımına izin verir. Bu alan güvenli managed kodun garanti setini daraltır; yalnız ölçülmüş gereksinim olduğunda kullanılmalıdır.
static int SumStackAllocated()
{
Span<int> values = stackalloc int[4] { 3, 5, 7, 11 };
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return sum;
}
Console.WriteLine(SumStackAllocated());Bu örnek işaretçi kullanmadan stack bellek ayırma gösterir. unsafe işaretçi erişimi native birlikte çalışabilirlik veya çok özel performans yollarında gerekebilir.
4.5. Operatör Aşırı Yükleme, Dizinleyici ve Dönüşümler
Operatör aşırı yükleme, yalnız işlemin anlamı domain içinde doğal olduğunda kullanılmalıdır. Sürpriz semantik okunabilirliği azaltır.
public readonly record struct Vector2(double X, double Y)
{
public static Vector2 operator +(Vector2 left, Vector2 right)
=> new(left.X + right.X, left.Y + right.Y);
public double this[int index] => index switch
{
0 => X,
1 => Y,
_ => throw new IndexOutOfRangeException()
};
}
Vector2 v = new(1, 2) + new Vector2(3, 4);
Console.WriteLine($"{v[0]}, {v[1]}");Implicit dönüşümler veri kaybı veya şaşırtıcı maliyet üretmemelidir. Riskli dönüşümler explicit olmalıdır.
4.6. XML Belge Açıklamaları ve API Sözleşmesi
Public API'lerde XML documentation IDE desteği ve dokümantasyon üretimi için kullanılabilir. Ancak yorum, kodun zaten söylediğini tekrar etmek yerine sözleşme, sınır ve hata davranışını açıklamalıdır.
/// <summary>0 ile 1 arasında normalize edilmiş oran döndürür.</summary>
/// <exception cref="ArgumentOutOfRangeException">Değer 0..100 aralığında değilse.</exception>
public static double NormalizePercent(int value)
{
if (value is < 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
return value / 100.0;
}4.7. Özyinelemeli İnişle Basit bir Ayrıştırıcı
Dil işleme, yalnız derleyici teorisine ait değildir. Küçük kural dilleri, filtre ifadeleri ve yapılandırma sözdizimleri recursive-descent parser ile okunabilir.
Aşağıdaki örnek toplama ve çarpmanın önceliğini koruyan küçük bir aritmetik ayrıştırıcıdır:
public sealed class ExpressionParser
{
private readonly string _text;
private int _index;
public ExpressionParser(string text)
{
_text = text;
}
public double Parse()
{
double value = ParseExpression();
SkipSpaces();
if (_index != _text.Length)
{
throw new FormatException($"Beklenmeyen karakter: {_text[_index]}");
}
return value;
}
private double ParseExpression()
{
double value = ParseTerm();
while (true)
{
SkipSpaces();
if (Match('+'))
{
value += ParseTerm();
}
else if (Match('-'))
{
value -= ParseTerm();
}
else
{
return value;
}
}
}
private double ParseTerm()
{
double value = ParseNumber();
while (true)
{
SkipSpaces();
if (Match('*'))
{
value *= ParseNumber();
}
else if (Match('/'))
{
value /= ParseNumber();
}
else
{
return value;
}
}
}
private double ParseNumber()
{
SkipSpaces();
int start = _index;
while (_index < _text.Length &&
(char.IsDigit(_text[_index]) || _text[_index] == '.'))
{
_index++;
}
if (start == _index ||
!double.TryParse(
_text.AsSpan(start, _index - start),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double value))
{
throw new FormatException("Sayı bekleniyordu.");
}
return value;
}
private bool Match(char expected)
{
if (_index >= _text.Length || _text[_index] != expected)
{
return false;
}
_index++;
return true;
}
private void SkipSpaces()
{
while (_index < _text.Length && char.IsWhiteSpace(_text[_index]))
{
_index++;
}
}
}
Console.WriteLine(new ExpressionParser("2 + 3 * 4").Parse());Gerçek bir parser için parantez, unary operatör, hata konumu, tokenization ve limitler ayrıca ele alınmalıdır.
4.8. Masaüstü, Web ve Uygulama Katmanları
Windows Forms ve WPF, Windows masaüstünde yaşamaya devam eder. ASP.NET Core web/API uygulamalarının ana .NET çatısıdır. .NET MAUI mobil ve masaüstü istemciler için ortak bir uygulama modeli sunar. Dil bilgisi bu UI veya web çatılarına bağlı değildir; aynı C# tür sistemi ve çalışma zamanı ilkeleri farklı uygulama modellerinde kullanılır.
UI kodunda iş kurallarını event handler içine yığmak yerine domain ve servis katmanlarını ayrıştırmak test edilebilirliği artırır.
4.9. Veri Erişiminde Sorgu Şekli ve Nesne Yaşam Süresi
Bir ORM sorgusunun maliyeti yalnız SQL'in yürütme süresi değildir. Sunucudan dönen satır sayısı, ağ üzerinden aktarılan veri, istemci tarafındaki nesne üretimi ve değişiklik izleme maliyeti birlikte değerlendirilir. Yalnız gerekli sütunların izdüşümünü almak, örneğin bir liste ekranında tam entity yerine küçük bir sonuç türü üretmek, hem aktarımı hem de bellekte tutulan durum miktarını azaltır. Bunun karşılığında izdüşüm, güncellenebilir bir entity'nin sahip olduğu davranışları otomatik olarak taşımaz.
Nesne grafı sorgulanırken koleksiyon ilişkilerinin gelişigüzel yüklenmesi, tek bir sonuç için çok sayıda ek sorgu (N+1) veya join çoğalması doğurabilir. İki ayrı koleksiyonun aynı sonuçta birleştirilmesi satır sayısını beklenmedik biçimde çarpabilir. Bu durumda seçici izdüşüm, ayrı sorgular veya açıkça belirlenmiş yükleme stratejileri arasındaki seçim, gerçek satır hacmiyle sınanır. IQueryable<T> üzerinde işlem zincirinin kurulması, bunların veritabanında yürütüldüğü anlamına yalnızca sorgu sağlayıcısı ilgili ifadeleri çevirebildiği ölçüde gelir. IEnumerable<T> sınırına erken geçilmesi filtrelemeyi istemciye taşıyabilir.
Bağlantı ile sorgu sonucunun yaşam süresi ayrıdır. Açık okuyucu, ertelenmiş enumerasyon ve transaction kapsamı, metodun görünürde bitmesinden sonra da kaynağı tutabilir. Asenkron API çağrısının kullanılması bağlantı kapasitesini artırmaz; beklemenin iş parçacığını bloklamamasını sağlar. Ölçümde sorgu sayısı, toplam satır, veri aktarım hacmi, tahsis ve bağlantı tutma süresi birlikte raporlanır. Bu ayrım LINQ ifadesinin okunabilirliği ile üretilen veri erişim davranışını birbirinden ayırır.
4.10. Akış Tabanlı Serileştirmede Bellek ve Kaynak Yaşam Döngüsü
Veritabanı satırlarını JSON veya XML çıktısına aktarırken bütün sonuç kümesini bellekte nesne listesine dönüştürmek, veri hacmiyle doğrusal heap büyümesi oluşturabilir. DbDataReader üzerinden ileri yönlü okuma ve kademeli yazma, ara nesne tahsisini azaltır; ancak veri tabanı bağlantısının ve okunan sonucun yaşam süresi çıktı akışının tamamlanmasına bağlı hale gelir.
Tasarımdaki temel ayrımlar sonuç kümesi sırası, NULL/veri tipi eşlemesi, XML karakter kaçışı veya JSON escaping, istemcinin aktarımı kesmesi ve cancellation sonrasında kaynakların bırakılmasıdır. Aynı anda çok sayıda uzun akış açık tutulduğunda düşük heap kullanımı, bağlantı havuzunun tükenmediğini garanti etmez. Ölçümlerde tahsis miktarı yanında bağlantı elde tutma süresi, karşı tarafa uygulanan backpressure ve hata anında çıktı geçerliliği değerlendirilir.
Ünite 5: Modern C#/.NET: Yaşam Süresi, Bellek ve Yüksek Başarım
5.1. required, init Ve Birincil Kurucularla Nesne Sözleşmesi
Modern C# nesne oluşturmayı yalnız kurucu aşırı yüklemeleriyle sınırlamaz. init yalnız başlatma aşamasında atamaya izin verir; required, nesne oluşturulurken ilgili üyenin başlatılmasını derleyici sözleşmesine dönüştürür. Birincil kurucular ise sınıf veya struct bildiriminin üzerinde kurucu parametreleri tanımlar.
public sealed class ServiceOptions(string endpoint)
{
public string Endpoint { get; } =
string.IsNullOrWhiteSpace(endpoint)
? throw new ArgumentException("Endpoint boş olamaz.", nameof(endpoint))
: endpoint;
public required TimeSpan Timeout { get; init; }
public int RetryCount { get; init; } = 2;
}
var options = new ServiceOptions("https://api.example.com")
{
Timeout = TimeSpan.FromSeconds(3)
};required, çalışma zamanında otomatik doğrulama yapmaz; başlatma sözleşmesini derleyici düzeyinde güçlendirir. Birincil kurucu parametresi sınıf gövdesinde kullanılıyorsa derleyici gerektiğinde değeri nesne durumunda tutabilir. Bu nedenle kurucu parametresi ile aynı değeri ayrıca alanda saklamak, fark edilmeden iki kopya oluşturabilir.
5.2. ref struct, scoped Ve Yaşam Süresi Güvenliği
Span<T> ve ReadOnlySpan<T> birer ref struct örneğidir. Bu türler yönetilen heap üzerinde keyfî biçimde yaşayamaz; derleyici, tuttukları referansların hedeflerinden daha uzun yaşamamasını denetler. Bu sınırlama düşük seviyeli bellek erişimini güvenli sınırlar içinde tutar.
static int ParseHeader(scoped ReadOnlySpan<byte> data)
{
if (data.Length < 4)
throw new ArgumentException("Başlık en az 4 bayt olmalıdır.", nameof(data));
return data[0]
| (data[1] << 8)
| (data[2] << 16)
| (data[3] << 24);
}scoped, bir referansın veya ref struct değerinin çağrı kapsamı dışına kaçamayacağını ifade etmeye yardımcı olur. ref readonly ise büyük değer türlerini kopyalamadan salt okunur başvuruyla döndürmek veya geçirmek için kullanılabilir.
Bu araçlar mikro-optimizasyon olarak değil, yaşam süresi ve kopyalama maliyeti gerçekten önemli olduğunda kullanılmalıdır. Daha basit değer aktarımı aynı işi karşılıyorsa okunabilirlik tercih edilir.
5.3. ValueTask Kullanım Koşulları
Asenkron API için varsayılan dönüş türü Task/Task<T> olmalıdır. ValueTask<T>, çağrıların önemli bir bölümünün eşzamanlı tamamlandığı ve Task ayırmalarının ölçülmüş bir darboğaz oluşturduğu sıcak yollarda yararlı olabilir.
public sealed class LookupCache
{
private readonly Dictionary<int, string> _cache = [];
public ValueTask<string?> FindAsync(
int id,
CancellationToken cancellationToken = default)
{
if (_cache.TryGetValue(id, out string? value))
return ValueTask.FromResult<string?>(value);
return LoadSlowAsync(id, cancellationToken);
}
private static async ValueTask<string?> LoadSlowAsync(
int id,
CancellationToken cancellationToken)
{
await Task.Delay(20, cancellationToken);
return id.ToString();
}
}ValueTask daha karmaşık bir tüketim sözleşmesine sahiptir. Genel kural, dönen değeri doğrudan bir kez await etmektir. Birden çok kez bekleme, sonucu erken okuma veya aynı değeri farklı tüketim yollarıyla kullanma güvenli varsayım değildir. Performans ölçümü yoksa Task daha basit ve daha doğru varsayılandır.
5.4. Asenkron Akışlar: IAsyncEnumerable<T>
Tek bir sonuç yerine zaman içinde veri üreten G/Ç kaynakları için IAsyncEnumerable<T> doğal bir modeldir. Tüm sonucu bellekte biriktirmek yerine öğeler geldikçe işlenebilir.
using System.Runtime.CompilerServices;
static async IAsyncEnumerable<int> ReadSamplesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(50, cancellationToken);
yield return i * 10;
}
}
await foreach (int sample in ReadSamplesAsync(cancellationToken))
{
Console.WriteLine(sample);
}await foreach üretici ile tüketiciyi asenkron biçimde bağlar. Bununla birlikte akışın doğal olarak geri basınçlı olduğu varsayılmamalıdır; üretici ayrı bir kuyrukta sınırsız veri biriktiriyorsa bellek yine büyüyebilir. Yüksek hacimli sistemlerde bounded Channel<T> veya açık kapasite politikasıyla birleştirmek gerekebilir.
5.5. Memory<T>, ArrayPool<T> Ve Tampon Sahipliği
Span<T> kısa ömürlü ve yığınla sınırlı yaşam süresi için uygundur; asenkron sınırların ötesinde taşınacak tamponlar için Memory<T>/ReadOnlyMemory<T> kullanılabilir. Böylece aynı bellek bölgesi kopyalanmadan farklı katmanlar arasında temsil edilebilir.
Sık büyük dizi ayırmak GC baskısı yaratıyorsa ArrayPool<T> yeniden kullanılabilir tampon sağlar:
using System.Buffers;
static int ReadPrefix(Stream stream)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
int read = stream.Read(buffer, 0, 4096);
return read;
}
finally
{
ArrayPool<byte>.Shared.Return(
buffer,
clearArray: true);
}
}Havuzdan dönen dizinin uzunluğu istenenden büyük olabilir; mantıksal veri uzunluğu ayrıca tutulmalıdır. Hassas veri taşıyan tamponlar geri verilirken temizleme gerekebilir. Havuzlama, nesneleri ücretsiz kılmaz; belleği daha uzun süre elde tutabilir. Bu nedenle yalnız ölçülmüş ayırma baskısında kullanılmalıdır.
5.6. Çöp Toplama, Büyük Nesneler ve Sabitleme
.NET çöp toplayıcısı nesil temellidir. Kısa ömürlü nesnelerin çoğu 0. nesilde toplanır; daha uzun yaşayan nesneler üst nesillere taşınır. Büyük nesneler ayrı büyük nesne yığınında tutulur. Varsayılan büyük nesne eşiği 85.000 bayttır ve çalışma zamanı yapılandırmasıyla değiştirilebilir.
Üretim açısından önemli sonuçlar:
- çok sayıda kısa ömürlü küçük nesne, sık genç nesil GC'lerine;
- tekrarlanan büyük tamponlar, büyük nesne yığını ve 2. nesil toplamalarına;
- uzun süre sabitlenen nesneler, heap parçalanmasına;
- gereksiz sonlandırıcılar, nesnelerin yaşam süresinin uzamasına
neden olabilir.
GC'yi iyileştirmenin ilk yolu GC.Collect() çağrılarını artırmak değildir. Önce gereksiz ayırmaları, yaşam sürelerini ve büyük geçici nesneleri azaltmak gerekir.
static void Fill(Span<byte> destination)
{
Random.Shared.NextBytes(destination);
}
byte[] buffer = new byte[16 * 1024];
Fill(buffer);Düşük ayırmalı kod için Span<T>, havuzlar ve yeniden kullanılabilir tamponlar yararlı olabilir; ancak bunlar sahiplik ve yaşam süresi karmaşıklığını artırdığından yalnız ölçümle doğrulanmış sıcak yollarda uygulanmalıdır.
5.7. Zamanı Bağımlılık Olarak Modellemek: TimeProvider
Doğrudan DateTime.UtcNow veya gerçek zamanlı Task.Delay kullanımı, zaman bağımlı iş kurallarını test etmeyi zorlaştırabilir. .NET'teki TimeProvider, zamanı bir bağımlılık olarak taşımaya olanak verir.
public sealed class ExpiringValue(TimeProvider timeProvider)
{
private readonly TimeProvider _timeProvider = timeProvider;
private DateTimeOffset _expiresAt;
public void Reset(TimeSpan lifetime)
{
_expiresAt = _timeProvider.GetUtcNow() + lifetime;
}
public bool IsExpired()
{
return _timeProvider.GetUtcNow() >= _expiresAt;
}
}Bu yaklaşım saat, son kullanma süresi, retry gecikmesi ve benzeri davranışların deterministik testini kolaylaştırır. Sistem saatini doğrudan her katmandan okumak yerine zamanın nerede iş kuralı olduğunu açık hale getirir.
5.8. Yüksek Başarımlı Akış G/Ç: System.IO.Pipelines
Ağ protokolü, sıkıştırma, codec veya büyük akış ayrıştırma gibi senaryolarda System.IO.Pipelines, tampon yönetimi ve geri basınçla birlikte yüksek başarımlı G/Ç için tasarlanmıştır. PipeReader ve PipeWriter, parçalı gelen veriyi birleştirirken gereksiz kopyaları azaltmaya yardımcı olur.
using System.IO.Pipelines;
static async Task<int> CountBytesAsync(
PipeReader reader,
CancellationToken cancellationToken)
{
int total = 0;
while (true)
{
ReadResult result = await reader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> buffer = result.Buffer;
total += checked((int)buffer.Length);
reader.AdvanceTo(buffer.End);
if (result.IsCompleted)
break;
}
await reader.CompleteAsync();
return total;
}AdvanceTo çağrısının yanlış sınırlarla yapılması tüketilmiş veya incelenmiş verinin yönetimini bozabilir. Pipelines düşük seviyeli bir performans aracıdır; sıradan dosya veya HTTP işlemlerinde Stream ve yüksek düzeyli API'ler genellikle daha okunabilir seçimdir.
5.9. ThreadPool Açlığını ve Beklemeyi Tanılamak
Eşzamanlılık hataları yalnız doğruluk hatası olarak görülmez; yüksek kuyruk uzunluğu, artan iş parçacığı sayısı ve düşük CPU kullanımı altında ciddi gecikme olarak da ortaya çıkabilir. Sync-over-async kalıbı (.Result, .Wait(), .GetAwaiter().GetResult()), uzun kilit beklemeleri ve bloklayan I/O, ThreadPool çalışanlarını tüketebilir.
.NET tanılama araçları bu ayrımı görünür kılar:
dotnet-counters, çalışma zamanı sayaçlarını canlı gözlemlemek;dotnet-trace, bekleme ve çalışma zamanı olaylarını zaman içinde toplamak;dotnet-stackveya dump incelemesi, hangi çağrı yığınlarının çalışanları tuttuğunu görmek
için kullanılabilir.
Tanılama metriği tek başına neden değildir. Yüksek thread sayısını "daha fazla thread verelim" yaklaşımıyla düzeltmek yerine, hangi kodun havuz iş parçacığını bloke ettiğini bulmak gerekir. Asenkron G/Ç yolunu baştan sona await ile sürdürmek çoğu sync-over-async sorununda temel çözümdür.
5.10. C# 14 ve .NET 10 ile Güncel Dil Çizgisi
C# 14; extension members, null-conditional assignment, field backed property, daha güçlü Span<T> dönüşümleri, genişletilmiş partial üyeler ve kullanıcı tanımlı compound assignment gibi yenilikler getirir. Bu özellikler temel C# bilgisinin yerine geçmez; var olan tür, bellek ayırma, dispatch ve lifetime kuralları üzerinde çalışır.
public sealed class Job
{
public string? Result { get; set; }
}
Job? current = new();
current?.Result = "done";
Console.WriteLine(current?.Result);Null-conditional assignment C# 14 özelliğidir.
.NET 10 çalışma zamanı JIT inlining, devirtualization, stack bellek ayırma, Native AOT ve kod üretimi tarafında geliştirmeler içerir. Performans davranışı yine ölçülmelidir; yeni çalışma zamanı sürümü her iş yükünde otomatik ve aynı oranda hızlanma anlamına gelmez.
5.11. Eski C#/.NET Kodunu Güncellerken
Eski kaynakların büyük bölümü dilin temel semantiği açısından hâlâ öğreticidir; fakat API seçimi ve çalışma zamanı varsayımları güncellenmelidir.
ArrayListyerineList<T>,HashtableyerineDictionary<TKey,TValue>kullanılır.WebRequest/WebClientyerineHttpClienttercih edilir.Thread.Abort,SuspendveResumeyerine işbirlikçi iptal ve görev tabanlı model kullanılır.- Sonlandırıcı ile doğrudan kaynak yönetimi yerine
IDisposable,IAsyncDisposableve gerekirseSafeHandlekullanılır. - Dize birleştirme, I/O ve koleksiyon kodunda bellek ayırma davranışı ayrıca değerlendirilir.
- Null olabilir referans türleri ve çözümleyiciler yeni kodda sözleşme hatalarını erken yakalamaya yardımcı olur.
- Yansıma yoğun mimariler kırpma ve Native AOT ile birlikte yeniden değerlendirilir.
- Güvensiz kod, "daha hızlıdır" varsayımıyla değil ölçülmüş gereksinim ve sınırlandırılmış kapsamla kullanılır.
Ünite 6: Üretim Performansı, İlerleme Garantileri ve Doğrulama
6.1. Performans ve Üretim Davranışı
C# uygulamasının maliyeti yalnız algoritmanın Big-O sınıfından oluşmaz. Allocation oranı, GC nesilleri, boxing, arayüz üzerinden dinamik çağrı, closure/kapama üretimi, asenkron durum makinesi, çekişme, sistem çağrıları, G/Ç toplulaştırması ve veri yerleşimi gerçek gecikmeyi etkiler.
Yüksek trafikli sistemlerde şu sorular yararlıdır:
- Sıcak yol her çağrıda bellek ayırma yapıyor mu?
- Koleksiyon kapasitesi öngörülebiliyor mu?
- LINQ zinciri okunabilirlik kazandırırken gereksiz enumerator veya ara koleksiyon oluşturuyor mu?
Tasküretimi gerçekten asenkron bekleme için mi?- Paylaşılan kilit altında I/O yapılıyor mu?
- İptal ve zaman aşımı katmanlar arasında taşınıyor mu?
- Serileştirme sırasında gereksiz ara string veya byte[] üretiliyor mu?
- Ölçüm BenchmarkDotNet, profiler veya üretime benzer yük ile doğrulanıyor mu?
Dil özelliği seçimi, okunabilirlik ve bakım maliyeti kadar bellek ayırma, gecikme ve eşzamanlılık davranışıyla da değerlendirilmelidir.
6.2. Lock-free Algoritmada İlerleme Garantisini Doğru Adlandırmak
Interlocked kullanmak bir algoritmayı otomatik olarak wait-free yapmaz. Lock-free, sistem genelinde en az bir yürütücünün ilerlemeye devam edeceğini; wait-free ise her yürütücünün sonlu sayıda kendi adımından sonra tamamlanacağını ifade eden daha güçlü bir garantidir. Obstruction-free ise bir yürütücü tek başına yeterince uzun çalışırsa ilerleyebilmesini garanti eder.
CAS döngüsünde yoğun çekişme altında aynı thread tekrar tekrar başarısız olabilir; sistem ilerlerken bireysel starvation mümkündür. Bu nedenle "kilit yok = herkes hızlı ilerler" sonucu çıkarılmamalıdır.
Kilitli ve kilitsiz tasarım arasında seçim yaparken yalnız uncontended mikrobenchmark değil; çekişme, adalet, kod doğrulanabilirliği ve tail latency birlikte ölçülmelidir. Basit bir lock çoğu iş kuralında karmaşık bir lock-free yapıdan daha güvenilir olabilir.
6.3. .NET Davranışını Sürüm ve Ölçümle Doğrulamak
C# dil özelliği ile .NET çalışma zamanı davranışı ayrılmalıdır. Dil belirtimi, BCL API sözleşmesi ve CoreCLR/JIT optimizasyonu farklı katmanlardır; bir JIT çıktısı dil garantisi gibi yorumlanmamalıdır.
Asenkron ve eşzamanlı kodda zamanlama nondeterministic olabilir. Testin birkaç kez geçmesi race condition olmadığını kanıtlamaz. CancellationToken, sahiplik, kilit sırası ve paylaşılan durum sözleşmesi açık tutulmalıdır.
Performans incelemesinde BenchmarkDotNet benzeri harness, dotnet-counters, dotnet-trace ve profiler çıktıları farklı soruları yanıtlar. Ölçüm, kullanılan .NET sürümü, GC modu ve donanımla birlikte anlam taşır.
6.4. C# Eşzamanlılığında Paylaşılan Durum ve İlerleme
Çok çekirdekli programlamada doğruluk yalnız veri yarışının bulunmaması değildir. C# ve .NET eşzamanlılığında bir veri yapısının gözlenebilir işlemlerinin tutarlılığı, lock/Interlocked kullanımının ilerleme davranışı, contention altında throughput kaybı ve bellek bariyerlerinin görünürlük etkisi ayrı değerlendirilmelidir; yalnız thread-safe etiketi bu özelliklerin hiçbirini tek başına açıklamaz. Linearizability doğruluk için güçlü bir model sunarken lock-free ve wait-free gibi ilerleme sınıfları, sistemin gecikme davranışının yalnız mutex sayısıyla açıklanamayacağını gösterir.
- Task tabanlı asenkronluk ile gerçek paralel CPU yürütmesini ayırmak.
- Cancellation ve timeout davranışını paylaşılan durum invariantlarıyla birlikte tasarlamak.
- Concurrent collection kullanımında doğruluk kadar contention profilini de ölçmek.
6.5. Çalışma Zamanı Kavramlarını Birlikte Okumak
C# özelliklerini yalnız dil sözdizimi olarak okumak, üretimde görülen maliyetlerin bir bölümünü gizler. async/await, Task, thread havuzu, allocation ve reflection aynı çalışma zamanı üzerinde kaynak tüketir. Bu nedenle Just-In-Time Compilation, Garbage Collection, Thread Pool, Lock-Free, Reflection ve Backpressure kavramları C#/.NET davranışını karşılaştırmalı okumak için yararlı bir çerçeve sağlar.
Özellikle yüksek concurrency altında daha fazla paralellik otomatik olarak daha yüksek throughput anlamına gelmez. ThreadPool kuyruğu, GC baskısı, kilit rekabeti ve downstream kapasitesi birlikte ölçülmelidir. Dil özelliği seçimi ancak çalışma zamanı davranışıyla birlikte değerlendirildiğinde üretim maliyeti görünür olur.
Ünite 7: CLR Tür Sözleşmesi, Modern Interface ve LINQ Çalıştırma Modeli
.NET mülakat kaynaklarında CLR/CTS, assembly, boxing, interface ve LINQ kavramları sıkça birlikte sorulur. Bunlar aynı katmanın kavramları değildir.
7.1. CTS ile CLS Ayrımı
Common Type System (CTS), CLR altında türlerin nasıl tanımlandığını ve işlendiğini belirleyen ortak tür modelidir. Value type ve reference type ayrımı bunun temel parçalarındandır.
Common Language Specification (CLS) ise .NET dilleri arasında geniş birlikte çalışabilirlik sağlamak için CTS'nin daha dar bir ortak alt kümesini tanımlar.
CTS:
runtime'ın desteklediği tür sistemi
CLS:
diller arası ortak kullanım için kısıtlı sözleşmeBir public kütüphane yalnız tek C# uygulamasında kullanılacaksa bütün CTS özelliklerinden yararlanabilir; çok dilli tüketim hedefleniyorsa CLS uyumluluğu önem kazanabilir.
7.2. Namespace, Assembly, Module ve Metadata
Namespace adlandırma/organizasyon kavramıdır; fiziksel deployment birimi değildir.
Assembly:
- IL,
- metadata,
- manifest,
- type/member tanımları,
- referanslar,
- resource'lar
içerebilir.
Bir assembly bir veya daha fazla module'den oluşabilir; modern C# projelerinde tipik çıktı tek ana module'dür.
Reflection bu metadata üzerinden tür ve member bilgisine ulaşabilir.
7.3. GAC'ı Tarihsel Bağlamda Okumak
Global Assembly Cache (GAC), .NET Framework döneminde strong-named assembly'lerin makine çapında paylaşılmasıyla ilişkili önemli bir deployment kavramıdır.
Modern .NET'in normal application deployment modeli GAC üzerine kurulmaz. deps.json, runtime resolution, NuGet paketleri, framework-dependent/self-contained deployment ve uygulama dizini gibi modern mekanizmalar farklı bir dağıtım modelidir.
Bu nedenle GAC hâlâ tarihsel/legacy .NET Framework sorularında geçerlidir; modern .NET uygulamasının varsayılan bağımlılık çözümleme modeli olarak öğretilmemelidir.
7.4. Boxing ve Unboxing
Value type object veya uyguladığı interface türüne dönüştürüldüğünde boxing oluşabilir.
int n = 42;
object boxed = n;Unboxing, boxed değerin gerçek value type'ına geri alınmasıdır:
int x = (int)boxed;Boxing heap üzerinde nesne oluşturma ve kopyalama maliyeti doğurabilir. Generic koleksiyonların önemli avantajlarından biri value type'larda gereksiz boxing ihtiyacını azaltmasıdır.
Unboxing yalnız sayısal dönüştürme değildir; boxed runtime type ile hedef type uyumlu olmalıdır.
7.5. Generic Variance
C# generic interface/delegate'lerinde bazı type parameter'lar variance tanımlayabilir.
out T -> covariance
in T -> contravarianceCovariance, daha türetilmiş türün üretici konumunda kullanılmasına; contravariance daha genel türün tüketici konumunda kullanılmasına olanak verir.
Bu özellik class generic type parameter'larına rastgele uygulanmaz; language kuralları yalnız güvenli yönlerde izin verir.
7.6. Modern İnterface Üyeleri
Eski C# anlatımındaki:
"Interface method body içeremez."
kuralı modern C# için artık genel doğru değildir.
Interface'ler:
- default instance implementation,
- static abstract member,
- static virtual member
gibi daha zengin sözleşmeler tanımlayabilir.
static abstract üyeler generic math gibi tasarımlarda type'ın statik operasyonlarını generic constraint üzerinden ifade etmeye yarar.
Bu özellikler interface'i "çoklu implementation inheritance" aracına çevirmek için değil, sözleşme evrimi ve generic abstraction için kontrollü kullanılmalıdır.
7.7. Partial Type ve Extension Method
partial, bir type deklarasyonunun kaynak kodda birden fazla dosyaya bölünmesini sağlar. Runtime'da iki ayrı type oluşmaz; compiler parçaları tek type olarak birleştirir.
Extension method ise var olan type'ı değiştirmeden method-call sözdizimi sağlar:
public static class TextExtensions
{
public static bool HasContent(this string? value)
=> !string.IsNullOrWhiteSpace(value);
}Bu gerçek instance member eklemek veya inheritance değiştirmek değildir; compiler'ın static method çağrısını uygun sözdizimine bağlamasıdır.
7.8. dynamic
dynamic, belirli operasyonların compile-time type checking'ini runtime binder'a erteler.
Bu esneklik:
- COM/interop,
- dinamik nesne modelleri,
- reflection benzeri bazı sınırlar
için yararlı olabilir.
Karşılığında bazı hatalar compile-time yerine runtime'da ortaya çıkar. Normal statik domain modelinde gereksiz dynamic type safety ve refactoring desteğini azaltır.
7.9. IEnumerable<T> İle IQueryable<T> Sınırı
IEnumerable<T> üzerinde LINQ operatörleri .NET kodu olarak sequence üzerinde çalışır.
IQueryable<T> ise expression tree ve query provider üzerinden sorguyu başka bir hedef dile çevirebilir:
IEnumerable<T>
-> delegate / local execution
IQueryable<T>
-> expression tree
-> provider translation
-> remote/native queryÖrneğin veritabanı provider'ı bazı C# methodlarını SQL'e çevirebilir, bazılarını çeviremez.
Bu nedenle:
query.AsEnumerable()veya ToList() gibi materialization sınırından sonra yazılan filtre ile provider'a çevrilen filtre aynı maliyet modeline sahip değildir.
7.10. Query Provider bir Compiler Sınırıdır
IQueryable<T> kullanan kodda yalnız C# semantiğini bilmek yetmez. Provider'ın expression tree'den hedef sorgu diline hangi operatörleri çevirebildiği de sözleşmenin parçasıdır.
Bu yüzden:
C# ifadesi compile oluyorile:
uzak veri kaynağında verimli/uygulanabilir sorguya çevriliyoraynı iddia değildir.
Bu ayrım LINQ, ORM ve veri erişim performansının temelidir.
İlgili Dersler
C#'ın diğer dillerle tür sistemi ve çalışma zamanı açısından karşılaştırılması için Programlama Dilleri, düşük seviye bellek modeli için C Programlama ve C++ Programlama, JVM karşılaştırması için Java Programlama, yazılım tasarımı için Yazılım Mühendisliği notları birlikte okunabilir.
Kaynakça
- Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
- ECMA International. ECMA-334: C# Language Specification, 7th Edition. https://ecma-international.org/publications-and-standards/standards/ecma-334/
- ECMA International. ECMA-335: Common Language Infrastructure. https://ecma-international.org/publications-and-standards/standards/ecma-335/
- Jeffrey Richter. CLR via C#, 4th Edition. Microsoft Press, 2012.
- Joseph Albahari. Threading in C#. Updated 2011-04-27. https://www.albahari.com/threading/
- Maurice Herlihy; Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
- Microsoft. .NET API Browser. https://learn.microsoft.com/dotnet/api/
- Microsoft. .NET garbage collection. https://learn.microsoft.com/dotnet/standard/garbage-collection/
- Microsoft. Asynchronous programming scenarios. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-scenarios
- Microsoft. Asynchronous programming with async and await. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/
- Microsoft. C# Guide. https://learn.microsoft.com/dotnet/csharp/
- Microsoft. C# Language Reference. https://learn.microsoft.com/dotnet/csharp/language-reference/
- Microsoft. Common Type System. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/standard/base-types/common-type-system
- Microsoft. Debug ThreadPool starvation. https://learn.microsoft.com/dotnet/core/diagnostics/debug-threadpool-starvation
- Microsoft. Generate and consume async streams. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream
- Microsoft. Interfaces — C# Language Specification. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/interfaces
- Microsoft. Large object heap. https://learn.microsoft.com/dotnet/standard/garbage-collection/large-object-heap
- Microsoft. Managed Threading Best Practices. https://learn.microsoft.com/dotnet/standard/threading/managed-threading-best-practices
- Microsoft. Memory<T> and Span<T> usage guidelines. https://learn.microsoft.com/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
- Microsoft. Primary constructors. https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/primary-constructors
- Microsoft. ref struct types. https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/ref-struct
- Microsoft. Standard Query Operators Overview. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/csharp/linq/standard-query-operators/
- Microsoft. System.IO.Pipelines. https://learn.microsoft.com/dotnet/standard/io/pipelines
- Microsoft. System.Threading.Channels library. https://learn.microsoft.com/dotnet/core/extensions/channels
- Microsoft. The lock statement. https://learn.microsoft.com/dotnet/csharp/language-reference/statements/lock
- Microsoft. Threading Objects and Features. https://learn.microsoft.com/dotnet/standard/threading/threading-objects-and-features
- Microsoft. TimeProvider overview. https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview
- Microsoft. What's new in .NET 10. https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/
- Microsoft. What's new in C# 14. https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14