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

C# Tip Dönüşümleri: Cast, Parse, TryParse ve Convert

Mert Özen 7 Kas 2026 8 dk 1 görüntülenme
C# Tip Dönüşümleri: Cast, Parse, TryParse ve Convert

C#'ta implicit ve explicit dönüşüm, cast'in kesmesi ve overflow'u, Parse ile TryParse'ın ne zaman kullanılacağı, Convert'ün yuvarlama ve null davranışı, fiyatları sessizce yüz katına çıkaran culture'a bağlı parse ve boxing/unboxing'e ilk bakış.

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 double fiyatı int'e çevirmenin iki yolu var: parantezli cast ve Convert.ToInt32. İkisi aynı şeyi mi yapar? Dört satırı tahmin et:

double price = 34.99;

Console.WriteLine((int)price);
Console.WriteLine(Convert.ToInt32(price));
Console.WriteLine(Convert.ToInt32(34.5));
Console.WriteLine(Convert.ToInt32(35.5));
34
35
34
36

Cast ondalık kısmı keser: 34.99 → 34. Convert.ToInt32 ise yuvarlar: 34.99 → 35. Son iki satır daha da ilginç: 34.5 → 34 ama 35.5 → 36. Convert, önceki yazıda Math.Round'da gördüğümüz banker's rounding kuralıyla tam ortadaki değerleri en yakın çift sayıya yuvarlıyor. Aynı işi yapıyor gibi görünen iki yol üç farklı davranış gösterdi. Bu yazıda C#'ta tipler arasında nasıl geçiş yapıldığını, hangi dönüşümün sessiz hangisinin açık olduğunu, string'den sayıya çevirmeyi ve culture'ın bu işe nasıl karıştığını görüyoruz.

Implicit Dönüşüm: Güvenli Genişleme

Bir değeri bilgi kaybı olmadan daha geniş bir tipe taşıyabiliyorsan C# bunu sana sormadan yapar. Buna implicit (örtük) dönüşüm denir. Küçük bir bardaktaki suyu büyük bir sürahiye dökmek gibi: hiçbir şey taşmaz.

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

int stock = 120;
long bigStock = stock;
double ratio = stock;
decimal money = stock;

long pagesPrinted = 123_456_789_012;
float lossy = pagesPrinted;
double better = pagesPrinted;

Console.WriteLine($"{bigStock} {ratio} {money}");
Console.WriteLine((long)lossy);
Console.WriteLine((long)better);
120 120 120
123456790528
123456789012

int'ten long'a, double'a ve decimal'e geçiş tamamen güvenli. Ama ikinci kısım bir istisna gösteriyor: long'dan float'a geçiş de implicit, yani compiler hiçbir şey demiyor, oysa değer bozuldu. float'un hassasiyeti yaklaşık 7 basamak; 12 basamaklı sayı ona sığmıyor ve en yakın temsil edilebilen değere kayıyor. double ise bu sayıyı kesin olarak tutuyor. "Implicit dönüşüm" her zaman "kayıpsız" demek değil; büyüklük açısından güvenli, hassasiyet açısından her zaman değil.

Explicit Dönüşüm: Cast

Bilgi kaybedebilecek bir dönüşümü compiler sessizce yapmaz. double rating = 4.97; int stars = rating; yazdığında:

Program.cs(1,35): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)

Mesaj ne yapacağını da söylüyor: dönüşüm mümkün, ama "kaybı kabul ediyorum" demen gerekiyor. Bunu hedef tipi paranteze yazarak yaparsın:

double rating = 4.97;
int stars = (int)rating;

long soldCount = 3_000_000_000;
int truncated = (int)soldCount;

int quantity = 300;
byte small = (byte)quantity;

Console.WriteLine(stars);
Console.WriteLine(truncated);
Console.WriteLine(small);
Console.WriteLine(checked((byte)quantity));
4
-1294967296
44
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.

4.97 yıldız 4 oldu; cast yuvarlamaz, keser. 3 milyar int'e sığmadığı için anlamsız bir negatif sayıya döndü. 300 de byte'a sığmıyor: byte 0–255 arası değer alır, 300'ün alt 8 biti 44 eder. Bu iki durumda cast sana hiçbir uyarı vermeden yanlış bir sayı teslim etti. Sayılar yazısındaki checked burada da çalışıyor: overflow varsa exception fırlatır. Değer bir sabitse compiler bunu compile time'da yakalar: byte b = (byte)300; için error CS0221: Constant value '300' cannot be converted to a 'byte' (use 'unchecked' syntax to override).

double ile decimal arasında ise iki yönde de implicit dönüşüm yok: decimal price = d; de, double d = price; da CS0266 verir. İkisinin temsil biçimleri farklı ve her iki yönde de kayıp olabilir; dönüşümü bilerek, cast ile yaparsın.

Parse ve TryParse

Kullanıcının yazdığı, bir dosyadan okunan ya da bir API'den gelen her şey önce string'dir. Sayıya çevirmek için hedef tipin Parse ya da TryParse method'unu kullanırsın:

int stock = int.Parse("42");
Console.WriteLine(stock + 1);

string input = "12 books";
if (int.TryParse(input, out int quantity))
{
    Console.WriteLine($"Quantity: {quantity}");
}
else
{
    Console.WriteLine($"'{input}' is not a number, quantity is {quantity}");
}

int pages = int.Parse(input);
Console.WriteLine(pages);
43
'12 books' is not a number, quantity is 0
Unhandled exception. System.FormatException: The input string '12 books' was not in a correct format.

Parse başarılı olursa sayıyı döndürür, olamazsa FormatException fırlatır. TryParse ise exception fırlatmaz: başarılı olup olmadığını bool olarak döndürür, sonucu da out parametresiyle verir. Başarısız olduğunda out değişkeni tipin varsayılan değerini, burada 0'ı alır. out int quantity yazımı değişkeni çağrının içinde tanımlar; out'u method'lar yazısında ayrıntılı göreceğiz.

Hangisini ne zaman kullanacağın basit bir soruya bağlı: geçersiz girdi beklenen bir durum mu? Kullanıcının forma yazdığı bir değer yanlış olabilir; bu bir hata değil, normal bir akış. Orada TryParse. Kendi uygulamanın yazdığı bir ayar dosyasındaki değer ise her zaman geçerli olmalı; bozuksa gerçekten bir şeyler ters gitmiştir ve Parse'ın exception'ı sana bunu söyler.

Convert Class'ı

System.Convert, temel tipler arasında dönüşüm yapan static method'ların toplandığı class. Parse'a benzer ama birkaç önemli farkı var:

string? missing = null;

Console.WriteLine(Convert.ToInt32(missing));
Console.WriteLine(Convert.ToInt32("FF", 16));
Console.WriteLine(Convert.ToBoolean("True"));
Console.WriteLine(Convert.ToInt32(true));
Console.WriteLine(int.Parse(missing));
Program.cs(7,29): warning CS8604: Possible null reference argument for parameter 's' in 'int int.Parse(string s)'.
0
255
True
1
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 's')

Convert.ToInt32(null) 0 döndürür, int.Parse(null) ise exception fırlatır. Bu fark bazen işine yarar, ama çoğu zaman "değer yok" ile "değer sıfır" arasındaki farkı sessizce siler. Bir kitabın stok bilgisi gelmediğinde 0 stok varsaymak yanlış bir karar olabilir. Compiler'ın verdiği CS8604 uyarısı da dikkat çekici: nullable açık olduğu için int.Parse'a null olabilecek bir değer verdiğini compile time'da fark etti. Convert'ün sayı tabanı alan overload'ları (16'lık, 2'lik) ve bool ile sayı arasındaki dönüşümleri de var. double'dan tamsayıya geçerken ise Önce Tahmin Et'te gördüğün gibi keser değil yuvarlar.

Culture'a Bağlı Parse

Bu yazının en sinsi tuzağı burada. "34,90" Türkiye'de otuz dört lira doksan kuruş demek; ABD'de ise virgül binlik ayırıcı. Aynı string'i farklı culture'larla parse edelim:

using System.Globalization;

CultureInfo turkish = new CultureInfo("tr-TR");
CultureInfo invariant = CultureInfo.InvariantCulture;

decimal a = decimal.Parse("34,90", turkish);
decimal b = decimal.Parse("34.90", invariant);
decimal c = decimal.Parse("34,90", invariant);
decimal d = decimal.Parse("34.90", turkish);

Console.WriteLine(a.ToString(invariant));
Console.WriteLine(b.ToString(invariant));
Console.WriteLine(c.ToString(invariant));
Console.WriteLine(d.ToString(invariant));
Console.WriteLine(b.ToString(turkish));
Console.WriteLine(decimal.TryParse("34,90", NumberStyles.AllowDecimalPoint, invariant, out decimal strict));
34.90
34.90
3490
3490
34,90
False

c ve d satırlarına bak: hata yok, exception yok, sadece fiyat yüz katına çıktı. Invariant culture'da virgül binlik ayırıcı sayıldığı için "34,90" 3490 oldu. Türkçe culture'da de nokta binlik ayırıcı, bu yüzden "34.90" yine 3490. Culture vermeden çağırdığın decimal.Parse("34.90"), uygulamanın çalıştığı makinenin culture'ını kullanır: geliştirme makinende doğru çalışan kod Türkçe ayarlı bir sunucuda fiyatları sessizce bozar.

Çözüm, kaynağın hangi biçimde olduğunu bilmek ve bunu açıkça söylemek. Bir JSON dosyası, CSV ya da API gibi makineden gelen veride CultureInfo.InvariantCulture; kullanıcının kendi dilinde yazdığı bir form alanında kullanıcının culture'ı. Son satır bir adım daha ileri gidiyor: NumberStyles.AllowDecimalPoint ile binlik ayırıcıya hiç izin vermediğimizde "34,90" kabul edilmiyor ve hatalı veri yüz katına çıkmak yerine reddediliyor. Çıktı tarafı da aynı kurala uyar: b.ToString(turkish) kullanıcıya "34,90" gösterir.

char, string ve Sayılar

Birkaç küçük dönüşüm de günlük kodda sık karşına çıkar:

Console.WriteLine((int)'A');
Console.WriteLine((char)66);
Console.WriteLine('7' - '0');
Console.WriteLine(42.ToString() + 1);
Console.WriteLine(42 + 1 + "");
65
B
7
421
43

char aslında bir sayı kodu taşır: 'A' 65, 66 da 'B'. Bir rakam karakterini sayıya çevirmenin klasik yolu, ondan '0'ın kodunu çıkarmak. Her tipin bir ToString() method'u var ve string ile sayının + ile buluşması toplama değil birleştirme yapar. Son iki satır bunu gösteriyor: işlemler soldan sağa yapılır, string'e ulaşana kadar sayılar toplanır, sonra birleştirilir. Bu sıralamayı bir sonraki yazıda operatörlerle birlikte göreceğiz.

Boxing ve Unboxing'e İlk Bakış

C#'ta her tip object'ten türer. Bir int'i object tipinde bir değişkene atadığında ne olur?

int stock = 42;
object boxed = stock;
stock = 50;

int unboxed = (int)boxed;
Console.WriteLine(boxed);
Console.WriteLine(unboxed);
Console.WriteLine(boxed is int);

if (boxed is int count)
{
    Console.WriteLine($"Pattern match: {count + 1}");
}

long wrong = (long)boxed;
Console.WriteLine(wrong);
42
42
True
Pattern match: 43
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'.

object boxed = stock; satırında runtime, int'in değerini kopyalayıp bir "kutuya" koyar ve boxed o kutuyu gösterir. Buna boxing denir. Kutudaki değer bir kopya olduğu için stock'u 50 yapmak kutuyu etkilemedi. Kutudan değeri geri almak (unboxing) bir cast ister ve bu cast katıdır: kutuda bir int varsa ancak int olarak çıkarabilirsin. int'ten long'a implicit dönüşüm olsa bile kutudan doğrudan long çıkaramazsın, InvalidCastException alırsın. Önce (int), sonra istersen long.

Güvenli yol is: boxed is int count hem tipi kontrol eder hem de değeri yeni bir değişkene koyar. Pattern matching'i koşullar yazısında genişleteceğiz. Boxing'in bir de maliyeti var: her kutu bellekte yeni bir object demek. Generics'in neden icat edildiğini anlatırken bu maliyete koleksiyonlar serisinde döneceğiz; value type ile reference type farkını da bu serinin ilerleyen bir yazısında ayrıntılı göreceğiz.

Sık Düşülen Tuzaklar

Cast'in yuvarladığını sanmak. (int)4.97 4'tür. Yuvarlama istiyorsan Math.Round kullan ve MidpointRounding'i açıkça ver; Convert.ToInt32'nin banker's rounding yaptığını unutma.

Kullanıcı girdisinde Parse. Geçersiz bir girdi uygulamanın exception ile düşmesine yol açar. Kullanıcıdan gelen veride TryParse kullan ve başarısız durumu ayrıca ele al.

Culture'sız parse. decimal.Parse("34.90") makineden makineye farklı sonuç verir ve hata da fırlatmaz. Makineden gelen veride CultureInfo.InvariantCulture'ı, kullanıcıdan gelen veride kullanıcının culture'ını açıkça ver.

Convert'ün null'ı sıfır yapması. Eksik veri ile sıfır değerini ayırt etmen gereken yerde Convert.ToInt32 bu farkı siler. Null'ı önce kendin kontrol et.

Küçük Bir Deneme

★☆☆ Kullanıcıdan Console.ReadLine() ile bir sipariş adedi al, TryParse ile çevir; geçersizse "Please enter a number" yazdır, geçerliyse adedi iki katına çıkarıp göster. ReadLine'ın dönüş tipi string?; TryParse null'ı da sorunsuzca reddeder.

★★☆ "1.234,56" string'ini tr-TR culture'ıyla decimal'e çevir ve sonucu invariant culture'la yazdır. Aynı string'i invariant culture'la parse etmeye çalıştığında ne oluyor?

★★★ object[] values = { 42, 42L, 4.2, "42" }; array'indeki her değeri is ile kontrol edip int'e çeviren bir döngü yaz: int doğrudan, long ve double cast ile, string TryParse ile. Hangisinde bilgi kaybı oluyor?

Artık değerleri bir tipten diğerine taşıyabiliyorsun. Bir sonraki yazıda bu değerlerle çalışan operatörleri ve expression'ları görüyoruz: aritmetik ve karşılaştırma operatörleri, mantıksal operatörler ve short-circuit, ?:, ??, ??=, ?. ve hepsinin öncelik sırası.