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

C# Sayılar ve Aritmetik: int, double, decimal ve Overflow

Mert Özen 7 Kas 2026 8 dk 1 görüntülenme
C# Sayılar ve Aritmetik: int, double, decimal ve Overflow

C#'ta tamsayı bölmesi ve kalan, int, long, double ve decimal arasında seçim, 0.1 + 0.2'nin neden 0.3 etmediği, para için decimal, overflow ve checked, sıfıra bölme, Math class'ı ve banker's rounding tuzağı.

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

Dört satır, dördü de ilkokul matematiği. Ekrana ne yazar?

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Console.WriteLine(7 / 2);
Console.WriteLine(7 / 2.0);
Console.WriteLine(0.1 + 0.2);
Console.WriteLine(0.1 + 0.2 == 0.3);
3
3.5
0.30000000000000004
False

İlk satır 3.5 değil 3, çünkü iki tamsayıyı böldün ve sonuç da tamsayı. İkinci satırda bölenlerden biri double olduğu için sonuç 3.5. Üçüncü ve dördüncü satırlar ise C#'a özgü değil: bilgisayarların ondalıklı sayıları nasıl sakladığıyla ilgili ve Java'da, JavaScript'te, Python'da da aynı sonucu alırsın. Bu yazıda sayısal tiplerin davranışlarını, para hesabında neden decimal kullandığımızı, overflow'u ve Math class'ını görüyoruz. (İlk iki satır, sayıları culture'dan bağımsız yazdırmak için; Türkçe ayarlı bir makinede 3,5 görürdün.)

Tamsayı Bölmesi ve Kalan

İki tamsayıyı böldüğünde C# sonucun ondalık kısmını atar. Yuvarlamaz, keser. Bu bir hata değil, çoğu zaman tam da istediğin şey. Kitapçının deposunda kitapları kutulara yerleştirdiğimizi düşün:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

int books = 7;
int boxCapacity = 2;

int fullBoxes = books / boxCapacity;
int leftOver = books % boxCapacity;

Console.WriteLine($"{fullBoxes} full boxes, {leftOver} left over");
Console.WriteLine(-7 / 2);
Console.WriteLine(-7 % 2);

double average = books / boxCapacity;
double realAverage = (double)books / boxCapacity;
Console.WriteLine($"{average} vs {realAverage}");
3 full boxes, 1 left over
-3
-1
3 vs 3.5

/ bölümü, % kalanı verir. Kesme işlemi sıfıra doğru yapılır: -7 / 2 sonucu -4 değil -3, kalan da -1. Negatif sayılarla kalan hesaplarken bunu aklında tut.

Son iki satır en sık yapılan hatayı gösteriyor. double average = books / boxCapacity; satırında sonucu bir double'a atıyorsun ama bölme işlemi atamadan önce, iki int arasında yapılıyor. Sonuç 3 olarak hesaplanıyor, sonra 3.0'a çevriliyor. Ondalıklı sonuç istiyorsan bölmeden önce taraflardan birini double'a çevirmelisin: (double)books. Bu parantezli yazıma cast denir; tip dönüşümleri yazısında ayrıntılı göreceğiz.

Doğru Tipi Seçmek

Dört sayısal tip günlük işlerin neredeyse hepsini karşılar. int (32 bit) tamsayılar için varsayılan seçim: adetler, sayaçlar, sayfa sayıları. Yaklaşık ±2,1 milyara kadar sayıları tutar. long (64 bit) bundan büyük tamsayılar için: toplam satış adedi, dosya boyutları, milisaniye cinsinden süreler. double (64 bit kayan nokta) ölçümler ve bilimsel hesaplar için: ortalama puan, mesafe, yüzdelik istatistikler. Yaklaşık 15–17 basamak hassasiyeti vardır. decimal (128 bit) ise para ve ondalık basamağın kesin olması gereken her şey için; 28–29 anlamlı basamak tutar.

Hangi tipte hesap yapıldığını belirleyen şey, sonucu atadığın değişken değil işlemin taraflarıdır. Bu kural tamsayı bölmesinde karşımıza çıktı, burada da çıkıyor:

int pagesPerBook = 50_000;
int books = 50_000;

long wrong = pagesPerBook * books;
long right = (long)pagesPerBook * books;

Console.WriteLine(wrong);
Console.WriteLine(right);
-1794967296
2500000000

İki int'in çarpımı int olarak hesaplanır. 2,5 milyar int'e sığmaz ve sonuç negatif bir sayıya döner; long'a atama ancak bundan sonra olur. Taraflardan birini önce long yaptığında hesap baştan 64 bitle yapılır. Bu overflow olayına ilerleyen bölümde tekrar döneceğiz.

Kayan Nokta: 0.1 + 0.2 Neden 0.3 Değil

double ve float sayıları ikilik tabanda saklar. Onluk tabanda 1/3'ü sonlu basamakla yazamadığın gibi (0.3333…), ikilik tabanda da 0.1'i tam olarak yazamazsın. Bellekteki değer 0.1'e çok yakın ama 0.1 değil. Tek bir sayıda bu fark görünmez; işlemler biriktikçe ortaya çıkar:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

double doubleTotal = 0;
decimal decimalTotal = 0;

for (int i = 0; i < 10; i++)
{
    doubleTotal += 0.1;
    decimalTotal += 0.1m;
}

Console.WriteLine(doubleTotal);
Console.WriteLine(decimalTotal);
Console.WriteLine(doubleTotal == 1.0);
Console.WriteLine(decimalTotal == 1.0m);
0.9999999999999999
1.0
False
True

On kere 0.1 eklemek double'da 1 etmiyor, decimal'de ediyor. for döngüsünü kendi yazısında göreceğiz; burada sadece "aynı işlemi on kez yap" diye oku. decimal sayıları onluk tabanda sakladığı için 0.1 onun için tam bir değer. Bunun bedeli var: decimal bellekte iki kat yer kaplar ve işlemcide doğrudan karşılığı olmadığı için double'dan daha yavaş hesaplanır. Bilimsel hesaplarda, grafiklerde, oyunlarda bu yüzden double kullanılır.

İki double'ı karşılaştırman gerektiğinde == yerine aralarındaki farkın çok küçük olup olmadığına bak:

double total = 0.1 + 0.2;
const double Tolerance = 1e-9;

Console.WriteLine(total == 0.3);
Console.WriteLine(Math.Abs(total - 0.3) < Tolerance);
False
True

Para İçin decimal

Bir sipariş tutarını hesaplayalım: 34.90'lık bir kitaptan üç tane, yüzde 15 indirim ve yüzde 10 vergi:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

decimal unitPrice = 34.90m;
int quantity = 3;
decimal discountRate = 0.15m;
decimal vatRate = 0.10m;

decimal subtotal = unitPrice * quantity;
decimal discount = subtotal * discountRate;
decimal net = subtotal - discount;
decimal vat = net * vatRate;
decimal total = net + vat;

Console.WriteLine($"Subtotal: {subtotal}");
Console.WriteLine($"Discount: {discount}");
Console.WriteLine($"VAT:      {vat}");
Console.WriteLine($"Total:    {total}");
Console.WriteLine($"Rounded:  {Math.Round(total, 2)}");
Subtotal: 104.70
Discount: 15.7050
VAT:      8.899500
Total:    97.894500
Rounded:  97.89

İlginç bir detay: decimal basamak sayısını da hatırlar. 34.90m iki basamaklı bir değer, üçle çarpınca da iki basamak kaldı: 104.70. 0.15m ile çarpınca basamak sayıları toplandı ve dört basamaklı 15.7050 çıktı. Değer aynı, sadece gösterim farklı. Gerçek bir faturada ara tutarları hangi adımda ve hangi kuralla yuvarlayacağın bir iş kararıdır; muhasebe ekibine sormadan karar verme. int ile decimal'in çarpımında int sessizce decimal'e çevrildi, çünkü bu dönüşümde bilgi kaybı yok.

Overflow ve checked

Bir tipin taşıyabileceğinden büyük bir sonuç ürettiğinde ne olur? Tamsayılarda varsayılan davranış sessizce başa sarmak:

int stock = int.MaxValue;
stock = stock + 1;
Console.WriteLine(stock);

int safeStock = int.MaxValue;
safeStock = checked(safeStock + 1);
Console.WriteLine(safeStock);
-2147483648
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.

En büyük int'e 1 ekleyince en küçük int'e döndük: arabanın kilometre sayacının 999999'dan 000000'a dönmesi gibi. Varsayılanın bu olmasının sebebi performans; her işlemde overflow kontrolü yapmak maliyetli. checked ile sarmaladığın işlemde ise overflow bir OverflowException fırlatır ve program durur. Yanlış bir sayıyla devam etmektense durmak çoğu zaman daha iyidir. Exception'ları ve nasıl yakalandıklarını kendi yazısında göreceğiz.

Overflow'u compiler compile time'da görebiliyorsa seni hiç çalıştırmaya bırakmaz. int x = int.MaxValue + 1; satırı error CS0220: The operation overflows at compile time in checked mode hatası verir. Bütün projede overflow kontrolünü açmak istersen .csproj'a <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> eklersin; o zaman yukarıdaki ilk toplama da exception fırlatır, bilerek başa sarmak istediğin yerde unchecked(...) yazarsın.

decimal ise bu konuda farklı: checked olsun olmasın overflow'da her zaman exception fırlatır. decimal.MaxValue'ya 1 eklediğinde System.OverflowException: Value was either too large or too small for a Decimal. alırsın.

Sıfıra Bölmek

Tamsayılar ve kayan nokta sayıları sıfıra bölmeye de farklı tepki verir:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

double rating = 10.0;
double noVotes = 0;
Console.WriteLine(rating / noVotes);
Console.WriteLine(noVotes / noVotes);
Console.WriteLine(double.IsNaN(noVotes / noVotes));

int books = 10;
int boxes = 0;
Console.WriteLine(books / boxes);
Infinity
NaN
True
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.

double'da sıfıra bölmek sonsuzluk, sıfırı sıfıra bölmek NaN (Not a Number) üretir ve program devam eder. Bu sessizlik tehlikeli: NaN'ın dokunduğu her hesap NaN olur ve hatanın nerede başladığını bulmak zorlaşır. Kontrolü double.IsNaN ve double.IsInfinity ile yaparsın. Tamsayılarda ise sıfıra bölmek DivideByZeroException fırlatır. Bölen bir sabitse compiler bunu da önceden yakalar: 7 / 0 için error CS0020: Division by constant zero.

Math Class'ı ve Yuvarlama

Sık kullanılan matematik fonksiyonları System.Math class'ında static method olarak durur:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Console.WriteLine(Math.Pow(2, 10));
Console.WriteLine(Math.Sqrt(144));
Console.WriteLine(Math.Abs(-42));
Console.WriteLine(Math.Max(3, 8));
Console.WriteLine(Math.Clamp(120, 0, 100));
Console.WriteLine(Math.PI);
Console.WriteLine(Math.Pow(2, 10).GetType().Name);
1024
12
42
8
100
3.141592653589793
Double

Math.Clamp bir değeri verdiğin aralığa sıkıştırır; indirim yüzdesini 0–100 arasında tutmak gibi işler için birebir. Math.Pow'un her zaman double döndürdüğüne dikkat et; sonucu bir int'e atamak için cast gerekir.

Yuvarlama ise bu yazının en sinsi konusu:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Console.WriteLine(Math.Round(2.5));
Console.WriteLine(Math.Round(3.5));
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));
Console.WriteLine(Math.Round(0.125m, 2));
Console.WriteLine(Math.Round(0.125m, 2, MidpointRounding.AwayFromZero));
Console.WriteLine($"{Math.Floor(-2.5)} {Math.Ceiling(-2.5)} {Math.Truncate(-2.5)}");
2
4
3
0.12
0.13
-3 -2 -2

Math.Round(2.5) sonucu 3 değil 2. Varsayılan kural "banker's rounding": tam ortadaki değerler en yakın çift sayıya yuvarlanır. 2.5 → 2, 3.5 → 4. Bunun amacı çok sayıda yuvarlamada hatanın hep aynı yöne birikmemesi. Okulda öğrendiğin "beş ve üstü yukarı" kuralını istiyorsan MidpointRounding.AwayFromZero vermelisin. Fatura gibi bir belgede hangisinin kullanılacağı yine bir iş kuralıdır, ama hangisini kullandığını kodda açıkça yaz. Floor aşağı, Ceiling yukarı yuvarlar, Truncate ise ondalık kısmı atar; negatif sayılarda üçünün farkı belirginleşir.

Sık Düşülen Tuzaklar

Tamsayı bölmesini unutmak. Ortalama, yüzde ya da oran hesaplarken iki int'i bölüp sonucu double'a atamak sonucu kurtarmaz. Bölmeden önce taraflardan birini çevir.

double'ları == ile karşılaştırmak. Hesaplanmış iki double neredeyse hiçbir zaman tam eşit değildir. Bir tolerans kullan ya da değer kesin olmalıysa baştan decimal seç.

Sessiz overflow. Sayaçlar, toplamlar ve çarpımlar zamanla büyür. Büyüyebilecek değerler için long kullan, kritik hesaplarda checked ile overflow'u görünür yap.

Math.Round'un varsayılanını bilmemek. 2.5'in 3 olacağını varsayan bir indirim hesabı, kuruşları her seferinde farklı yöne yuvarlar. MidpointRounding değerini açıkça ver.

Küçük Bir Deneme

★☆☆ 250 sayfalık bir kitabı günde 30 sayfa okursan kaç günde bitirirsin? Tamsayı bölmesi ve kalanla hesapla; kalan sayfa varsa bir gün daha ekle.

★★☆ Sipariş örneğini double ile yeniden yaz ve sonuçları karşılaştır. Fark ilk hangi satırda ortaya çıkıyor?

★★★ int bir sayacı int.MaxValue - 2'den başlatıp beş kez 1 artır ve her adımda değeri yazdır. Sonra aynı işi checked içinde yap ve programın hangi adımda durduğunu gözle.

Sayılarla hesap yapmayı öğrendik; ama bir kitapçının asıl verisi metin: başlıklar, yazar adları, açıklamalar. Bir sonraki yazıda string'leri görüyoruz: interpolation, verbatim string'ler, sık kullanılan method'lar, string'in neden değiştirilemez olduğu, StringBuilder ve Türkçe "I/ı/İ" tuzağı.