Rust'ın veri tipleri iki gruba ayrılır: tek bir değer tutan skaler tipler (tamsayılar, kayan noktalı sayılar, bool ve char) ve birden çok değeri bir arada tutan bileşik tipler (tuple ve dizi). Tamsayı tipleri i8 ile i128, u8 ile u128 arasında ve mimariye bağlı isize/usize olarak gelir; varsayılan tamsayı i32, varsayılan kayan nokta f64'tür. Rust statik tiplidir: her değişkenin tipi derleme zamanında bilinmek zorundadır.

Bu yazıda her tipin aralığını, değişmez (literal) yazım biçimlerini, tamsayı taşmasının debug ve release derlemelerdeki farklı davranışını, char tipinin Türkçe harflerle ilişkisini, tuple ve dizilerin kurallarını Rust 1.99.1 ile çalıştırılmış örneklerle anlatıyoruz. Değişken tanımlamayı henüz görmediyseniz önce değişkenler yazısını okuyun; serinin tamamı için Rust öğrenme rehberi sayfasına bakın.

Statik tipler ve tip çıkarımı

Derleyici çoğu zaman tipi, atanan değerden ve değerin nasıl kullanıldığından kendisi çıkarır; bu yüzden her değişkene tip yazmanız gerekmez. Ancak birden çok tipin mümkün olduğu durumlarda bir tip açıklaması (type annotation) eklemeniz gerekir. Klasik örnek bir metni sayıya çeviren parse metodudur: metin u32'ye de f64'e de dönüştürülebilir, derleyici hangisini istediğinizi bilemez:

rust
fn main() {
    let tahmin = "42".parse().expect("Sayı değil!");
    println!("{tahmin}");
}
// error[E0284]: type annotations needed
// help: consider giving `tahmin` an explicit type

Çözüm, let tahmin: u32 = "42".parse().expect("Sayı değil!"); biçiminde tipi yazmaktır. Bu tür tip hatalarının diğer biçimleri için E0277 ve E0308 tip hataları yazısına bakabilirsiniz.

Tamsayı tipleri

Tamsayı tipleri: 8 bit, 16 bit, 32 bit, 64 bit

Tamsayılar kesirli kısmı olmayan sayılardır. Her tamsayı tipi işaretli (i ile başlar, negatif olabilir) ya da işaretsiz (u ile başlar, yalnızca sıfır ve pozitif) olur ve bit cinsinden açık bir boyuta sahiptir. İşaretli sayılar ikiye tümleyen (two's complement) gösterimle saklanır. n bitlik işaretli bir tip -(2 üzeri n-1) ile (2 üzeri n-1) - 1 arasını, işaretsiz bir tip 0 ile (2 üzeri n) - 1 arasını tutar.

Boyutİşaretliİşaretsizİşaretli aralıkİşaretsiz aralık
8 biti8u8-128 .. 1270 .. 255
16 biti16u16-32768 .. 327670 .. 65535
32 biti32u32-2147483648 .. 21474836470 .. 4294967295
64 biti64u64-(2^63) .. 2^63 - 10 .. 2^64 - 1
128 biti128u128-(2^127) .. 2^127 - 10 .. 2^128 - 1
Mimariye bağlıisizeusizeİşaretçi boyutundaİşaretçi boyutunda

isize ve usize, programın çalıştığı makinenin işaretçi boyutundadır: 64 bit mimaride 64, 32 bit mimaride 32 bittir. Reference'a göre bu tipler en az 16 bit genişliğindedir ve usize süreçteki her bellek adresini temsil edebilir. Bu yüzden koleksiyon indeksleri ve uzunluklar usize tipindedir. Hangi tipi seçeceğinizden emin değilseniz Rust'ın varsayılanı olan i32 iyi bir başlangıçtır; usize'ı esas olarak bir koleksiyonu indekslerken kullanırsınız.

Tamsayı değişmezleri onluk, onaltılık, sekizlik, ikilik ve (yalnızca u8 için) bayt biçiminde yazılabilir. Okunabilirlik için alt çizgi kullanılabilir (1_000 ile 1000 aynıdır) ve birden çok tipe uyabilen değişmezlerin sonuna 57u8 gibi bir tip soneki eklenebilir. Her tipin MIN, MAX ve BITS sabitleri vardır:

rust
fn main() {
    let ondalik = 98_222;
    let onaltilik = 0xff;
    let sekizlik = 0o77;
    let ikilik = 0b1111_0000;
    let bayt = b'A';
    let sonekli = 57u8;
    println!("{ondalik} {onaltilik} {sekizlik} {ikilik} {bayt} {sonekli}");
    println!("i8: {} .. {}", i8::MIN, i8::MAX);
    println!("u8: {} .. {}", u8::MIN, u8::MAX);
    println!("i32: {} .. {}", i32::MIN, i32::MAX);
    println!("usize bit sayısı: {}", usize::BITS);
}
// çıktı: 98222 255 63 240 65 57
// çıktı: i8: -128 .. 127
// çıktı: u8: 0 .. 255
// çıktı: i32: -2147483648 .. 2147483647
// çıktı: usize bit sayısı: 64

Son satırdaki 64 değeri, örneği 64 bit bir Windows makinede çalıştırmamızdan kaynaklanır; 32 bit bir hedefte 32 yazardı. b'A' değişmezi bir karakter değil, A harfinin ASCII kodu olan 65 değerine sahip bir u8'dir.

Tamsayı taşması: debug ve release farkı

Tamsayı taşması: debug ve release farkı: checked_add, wrapping_add, saturating_add, overflowing_add

Bir u8 değişkeni 0 ile 255 arasını tutar. Değeri bu aralığın dışına, örneğin 256'ya çıkarmaya çalışırsanız tamsayı taşması (integer overflow) olur ve The Book'a göre sonuç derleme moduna bağlıdır. Debug modda Rust taşma kontrolleri ekler ve taşma olursa program panic ile durur. --release ile derlendiğinde bu kontroller eklenmez; bunun yerine ikiye tümleyen sarma (wrapping) yapılır: 256 değeri 0'a, 257 değeri 1'e döner. Aşağıdaki programı iki şekilde derleyip çalıştırdık. Değeri metinden okuduk ki derleyici taşmayı derleme zamanında göremesin:

rust
fn main() {
    let girdi = "255";
    let x: u8 = girdi.parse().unwrap();
    let y = x + 1;
    println!("{y}");
}
// Debug derleme (rustc varsayılanı, cargo build):
// thread 'main' (3064) panicked at v2.rs:4:13:
// attempt to add with overflow
// Release derleme (rustc -O, cargo build --release):
// çıktı: 0

Panic satırındaki parantez içindeki sayı iş parçacığı kimliğidir ve her çalıştırmada değişir. Release derlemede program çökmedi ama beklediğiniz değer 256 değil 0 oldu. The Book bu sarma davranışına güvenmenin bir hata sayıldığını açıkça belirtir. Cargo profil ayarlarında bu davranış overflow-checks anahtarıyla değiştirilebilir; varsayılan değerler kurulum yazısında ve Cargo.toml yazısında anlatılıyor.

Taşma derleyicinin derleme zamanında görebileceği bir yerdeyse, örneğin aralık dışı bir değişmez yazdıysanız, kod hiç derlenmez. Bu, varsayılan olarak deny seviyesindeki overflowing_literals lint'inden gelir:

rust
fn main() {
    let x: u8 = 256;
    println!("{x}");
}
// error: literal out of range for `u8`
// note: the literal `256` does not fit into the type `u8` whose range is `0..=255`

Taşma ihtimali olan hesaplarda davranışı açıkça seçmek için standart kütüphane, ilkel sayı tiplerine dört metot ailesi sağlar. checked_* taşmada None döndürür, wrapping_* her modda sarar, saturating_* tipin en küçük ya da en büyük değerinde durur, overflowing_* ise sonucu ve taşma olup olmadığını bir tuple olarak verir. std belgelerinde ayrıca taşmada derleme modundan bağımsız olarak her zaman panic eden strict_* metotları da yer alır:

rust
fn main() {
    let x: u8 = 250;
    println!("{:?}", x.checked_add(10));
    println!("{:?}", x.checked_add(5));
    println!("{}", x.wrapping_add(10));
    println!("{}", x.saturating_add(10));
    println!("{:?}", x.overflowing_add(10));
    let y: i8 = -128;
    println!("{:?}", y.checked_neg());
    println!("{}", y.saturating_sub(1));
}
// çıktı: None
// çıktı: Some(255)
// çıktı: 4
// çıktı: 255
// çıktı: (4, true)
// çıktı: None
// çıktı: -128
Metot ailesiTaşmada ne olur250u8 + 10 sonucu
checked_addNone dönerNone
wrapping_addHer modda sarar4
saturating_addSınırda durur255
overflowing_addSonuç ve taşma bilgisi(4, true)
strict_addHer modda panicpanic
+ operatörüDebug: panic, release: sarmaModa bağlı

-128i8 değerinin tersi olan 128, i8 aralığına sığmadığı için checked_neg da None döndürür; bu, işaretli tiplerde sık gözden kaçan bir uç durumdur. Option tipini ve None ile nasıl çalışacağınızı enum ve Option yazısında bulabilirsiniz.

Kayan noktalı sayılar ve sayısal işlemler

Rust'ın iki kayan noktalı tipi vardır: 32 bitlik f32 ve 64 bitlik f64. Ondalıklı bir değişmez yazdığınızda varsayılan tip f64'tür; The Book bunun gerekçesini modern işlemcilerde f64'ün yaklaşık f32 kadar hızlı olup daha yüksek hassasiyet sunması olarak açıklar. Kayan noktalı sayıların hepsi işaretlidir ve IEEE-754 standardına göre temsil edilir. Bu standardın doğal sonuçları Rust'ta da görülür:

rust
fn main() {
    let x = 2.0;
    let y: f32 = 3.0;
    println!("{}", x / 3.0);
    println!("{}", y / 3.0);
    println!("{}", 0.1 + 0.2);
    println!("{}", -5 / 3);
    println!("{}", -5 % 3);
    println!("{}", 7 / 2);
    println!("{}", 7.0 / 2.0);
    println!("{}", f64::NAN == f64::NAN);
    println!("{}", 1.0 / 0.0);
}
// çıktı: 0.6666666666666666
// çıktı: 1
// çıktı: 0.30000000000000004
// çıktı: -1
// çıktı: -2
// çıktı: 3
// çıktı: 3.5
// çıktı: false
// çıktı: inf

Çıktıların her biri bir kural gösterir. 0.1 + 0.2 tam olarak 0.3 değildir, çünkü bu değerler ikilik tabanda tam temsil edilemez; para gibi kesin hesap gereken yerlerde kayan noktalı sayıları dikkatle kullanın. Tamsayı bölmesi sıfıra doğru keser: -5 / 3 sonucu -1, 7 / 2 sonucu 3'tür. Reference'a göre kalan operatörü kesen bölmeye dayanır ve kalanın işareti bölünenin işaretiyle aynıdır; bu yüzden -5 % 3 sonucu -2'dir. NaN kendisine bile eşit değildir; Rust 1.99.1 bu karşılaştırma için invalid_nan_comparisons uyarısı verir ve is_nan() kullanmanızı önerir. Sıfıra bölme kayan noktada inf üretir.

Rust sayı tipleri arasında örtük dönüşüm yapmaz. Bir tamsayıyla bir kayan noktalı sayıyı doğrudan toplayamazsınız:

rust
fn main() {
    let tam = 5;
    let ondalikli = 2.5;
    let toplam = tam + ondalikli;
    println!("{toplam}");
}
// error[E0277]: cannot add a float to an integer

Bu durumda tiplerden birini açıkça dönüştürmeniz gerekir, örneğin tam as f64 + ondalikli. as dönüşümünün kuralları (büyük tamsayıdan küçüğe geçerken kesme, kayan noktadan tamsayıya geçerken sınırda durma, NaN değerinin 0 olması) ve daha güvenli TryFrom seçeneği From, Into ve TryFrom dönüşümleri yazısında ayrıntılı işleniyor.

bool ve char

Boolean tipi bool, true ve false değerlerini alır ve bir bayt yer kaplar. Rust'ta if koşulları mutlaka bool olmalıdır; bir sayıyı koşul olarak yazmak derleme hatasıdır (kontrol akışı yazısında gösteriliyor). char ise dilin en temel alfabetik tipidir: tek tırnakla yazılır (çift tırnak metin içindir), 4 bayt yer kaplar ve bir Unicode skaler değerini temsil eder. Geçerli aralık U+0000 ile U+D7FF ve U+E000 ile U+10FFFF arasıdır; aksanlı harfler, Çince, Japonca ve Korece karakterler, emojiler ve sıfır genişlikli boşluklar geçerli char değerleridir.

rust
fn main() {
    let harf = 'ş';
    let euro = '€';
    println!("{}", std::mem::size_of::<char>());
    println!("{}", std::mem::size_of::<bool>());
    println!("{} {} {}", 'a'.len_utf8(), harf.len_utf8(), euro.len_utf8());
    println!("{}", 'i'.to_uppercase());
    println!("{}", 'ı'.to_uppercase());
    println!("{:?}", 'İ'.to_lowercase().to_string());
    println!("{:?}", '7'.to_digit(10));
    println!("{}", 'ğ'.is_alphabetic());
}
// çıktı: 4
// çıktı: 1
// çıktı: 1 2 3
// çıktı: I
// çıktı: I
// çıktı: "i\u{307}"
// çıktı: Some(7)
// çıktı: true

Bu çıktılar Türkçe metinle çalışanlar için önemlidir. Bir char bellekte hep 4 bayt tutsa da UTF-8 olarak kodlandığında a 1, ş 2, € 3 bayt yer kaplar; bu fark metin dilimlerinde (slice yazısı) ve String uzunluklarında karşınıza çıkar. Büyük küçük harf dönüşümü ise dile duyarlı değildir: std belgelerinin to_uppercase notunda Türkçe ve Azericedeki noktalı ve noktasız i örneği özellikle verilir. 'i'.to_uppercase() her dilde I üretir, Türkçedeki İ'yi değil. 'İ'.to_lowercase() ise iki karakterlik bir sonuç verir: i ve U+0307 birleşik nokta işareti. Dile duyarlı dönüşüm gerekiyorsa belge harici bir crate kullanmayı önerir. Metinlerle ilgili ayrıntılar String ve str farkı yazısında.

Tuple tipi

Tuple, farklı tiplerdeki birkaç değeri tek bir bileşik tipte toplamanın genel yoludur. Uzunluğu sabittir: tanımlandıktan sonra büyüyüp küçülemez. Değerlere desenle parçalayarak (destructuring) ya da nokta ve indeksle (tup.0) erişilir; ilk indeks 0'dır:

rust
fn main() {
    let tup: (i32, f64, char) = (500, 6.4, 'k');
    let (x, y, z) = tup;
    println!("{x} {y} {z}");
    println!("{}", tup.0);
    println!("{tup:?}");
    let tek = (5,);
    let sadece_sayi = (5);
    println!("{tek:?} {sadece_sayi:?}");
    let bos = ();
    println!("{bos:?}");
}
// çıktı: 500 6.4 k
// çıktı: 500
// çıktı: (500, 6.4, 'k')
// çıktı: (5,) 5
// çıktı: ()

Tek elemanlı tuple yazarken sondaki virgül zorunludur: Reference'a göre (5,) bir tuple, (5) ise parantez içinde bir sayıdır. Derleyici ikinci yazım için gereksiz parantez uyarısı (unused_parens) verir. Hiç elemanı olmayan () tuple'ına unit denir; hem tipi hem tek değeri () ile yazılır ve başka bir değer döndürmeyen ifadeler örtük olarak unit döndürür. std belgelerine göre Debug, PartialEq, Default gibi bazı trait'ler şimdilik yalnızca 12 veya daha az elemanlı tuple'lar için uygulanmıştır; daha uzun bir tuple'ı {:?} ile yazdıramazsınız. Bu kadar çok alan gerektiğinde isimli alanlara sahip bir struct daha okunaklıdır.

Dizi (array) tipi

Dizi (array) tipi: Eleman tipleri, Uzunluk, Erişim, Tip yazımı

Dizide bütün elemanlar aynı tipte olmalıdır ve uzunluk sabittir. Tipi [T; N] biçiminde yazılır: eleman tipi, noktalı virgül ve eleman sayısı. [3; 5] gibi bir tekrar ifadesi beş elemanın hepsini 3 yapar; std belgelerine göre tekrarlanan ifade Copy uygulayan bir tipte ya da bir const değer olmalıdır. Diziler genellikle yığın (stack) üzerinde, bilinen sabit boyutta tek bir bellek parçasıdır:

rust
fn main() {
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    let b = [3; 5];
    println!("{a:?} {b:?}");
    println!("uzunluk: {}", a.len());
    println!("{}", a[0] + a[4]);
    println!("{:?} {:?}", a.get(1), a.get(10));
    let aylar = ["Ocak", "Şubat", "Mart"];
    println!("{}", aylar[1]);
    let mut c = a;
    c[0] = 100;
    println!("{:?} {:?}", a, c);
}
// çıktı: [1, 2, 3, 4, 5] [3, 3, 3, 3, 3]
// çıktı: uzunluk: 5
// çıktı: 6
// çıktı: Some(2) None
// çıktı: Şubat
// çıktı: [1, 2, 3, 4, 5] [100, 2, 3, 4, 5]

Son satır önemli bir ayrıntıyı gösterir: eleman tipi Copy olduğunda dizinin kendisi de Copy olur, bu yüzden let mut c = a; bir kopya oluşturur ve c'yi değiştirmek a'yı etkilemez. Dizi, slice'a dönüşebildiği için slice metotlarının hepsini kullanabilir; len ve get bunlardan ikisidir. get indeks geçersizse None döndürür. Köşeli parantezle geçersiz bir indekse erişirseniz ise Rust çalışma zamanında kontrol yapar ve programı durdurur:

rust
fn main() {
    let a = [1, 2, 3, 4, 5];
    let indeks: usize = "10".parse().unwrap();
    let eleman = a[indeks];
    println!("{eleman}");
}
// thread 'main' (19968) panicked at v9.rs:4:18:
// index out of bounds: the len is 5 but the index is 10

The Book bunu Rust'ın bellek güvenliği ilkesinin somut bir örneği olarak sunar: pek çok düşük seviyeli dilde bu kontrol yapılmaz ve geçersiz bir bellek alanı okunabilir. Rust ise geçersiz erişime izin vermek yerine programı hemen durdurur. Eleman sayısı değişecek bir koleksiyona ihtiyacınız varsa diziler yerine heap üzerinde büyüyüp küçülebilen Vec vektör tipini kullanın; The Book da emin olmadığınızda vektörü seçmenizi önerir. Yılın on iki ayı gibi sayısı hiç değişmeyecek değerler ise dizi için iyi bir örnektir.

ÖzellikTupleDiziVec (karşılaştırma için)
Eleman tipleriFarklı olabilirHepsi aynıHepsi aynı
UzunlukSabitSabit, tipin parçasıÇalışma zamanında değişir
Erişimtup.0, desena[i], a.get(i)v[i], v.get(i)
Tip yazımı(i32, f64)[i32; 5]Vec<i32>
BellekDeğerin kendisiDeğerin kendisi, genelde yığınİçerik heap üzerinde

Sık yapılan hatalar

  • Debug'da test edip release'te sarmaya güvenmek: Taşma debug'da panic, release'te sessiz sarma üretir; taşma olasıysa checked_* veya saturating_* kullanın.
  • Tamsayı ile kayan noktalıyı karıştırmak: Rust örtük dönüşüm yapmaz; E0277 alırsınız.
  • Tamsayı bölmesinin kesirli sonuç vermesini beklemek: 7 / 2 sonucu 3'tür; kesirli sonuç için işlenenlerin f64 olması gerekir.
  • Tek elemanlı tuple'da virgülü unutmak: (5) tuple değil, sayıdır.
  • Kullanıcı girdisini doğrudan indeks yapmak: Sınır dışı indeks panic üretir; güvenli erişim için get kullanın.
  • Türkçe büyük harf dönüşümünü doğru sanmak: to_uppercase dile duyarlı değildir, i harfi I olur.

Sık Sorulan Sorular

Hangi tamsayı tipini seçmeliyim?

Emin değilseniz i32 ile başlayın. Negatif olamayacak değerler için işaretsiz tipler, koleksiyon indeksleri ve uzunluklar için usize kullanılır. Değer aralığınızın tipin sınırlarına yaklaşabileceği yerlerde daha büyük bir tip seçin ya da taşmayı açıkça ele alın.

f32 mi f64 mü?

Varsayılan f64'tür ve The Book'a göre modern işlemcilerde f32 ile yaklaşık aynı hızda çalışıp daha fazla hassasiyet sunar. Bellek kısıtı olan özel durumlar dışında f64 kullanmanız yeterlidir.

char ile String arasındaki fark nedir?

char tek bir Unicode skaler değerdir ve tek tırnakla yazılır. Metin ise UTF-8 baytlarından oluşan String veya &str tipindedir ve çift tırnakla yazılır. İnsanın karakter olarak algıladığı şey her zaman tek bir char'a karşılık gelmeyebilir; İ harfinin küçük harf karşılığı örneği bunu gösterir.

Dizinin uzunluğunu çalışma zamanında belirleyebilir miyim?

Hayır. Dizi uzunluğu tipin bir parçasıdır ve derleme zamanında bilinmelidir; bu yüzden uzunluk olarak bir const kullanabilirsiniz ama let değişkeni kullanamazsınız. Uzunluk çalışma zamanında belli oluyorsa Vec kullanın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Rust değişkenler · String ve str farkı · Vec kullanımı · From, Into ve TryFrom · Display ve Debug formatlama · C++ nedir