Rust'ta struct, birbiriyle ilişkili değerleri isimli alanlar altında tek bir tipte toplamanızı sağlar: struct Kullanici { aktif: bool, eposta: String } gibi tanımlanır, Kullanici { aktif: true, eposta: ... } ile örneği oluşturulur ve alanlara nokta ile erişilir. Davranışlar ise impl bloğunda tanımlanır: ilk parametresi self olan fonksiyonlar metottur (d.alan()), self almayanlar ilişkili fonksiyondur (Dikdortgen::new(3, 4)).

Bu yazıda isimli alanlı struct'ları, tuple struct'ları ve alanı olmayan birim struct'ları, alan kısaltmasını ve struct güncelleme söz dizimini, #[derive(Debug)] ile yazdırmayı, &self, &mut self ve self alıcılarını, ilişkili fonksiyonları ve birden çok impl bloğunu Rust 1.99.1 ile çalıştırılmış örneklerle anlatıyoruz. Fonksiyon kavramını bilmiyorsanız önce fonksiyonlar yazısını okuyun; serinin tamamı için Rust öğrenme rehberi sayfasına bakın.

Struct tanımlama ve örnek oluşturma

Struct tanımlama ve örnek oluşturma

Struct'lar tuple'lara benzer: ikisi de birbiriyle ilişkili birden çok değeri tutar ve bu değerler farklı tiplerde olabilir. Fark, struct'ta her parçaya bir isim vermenizdir. Bu sayede değerlerin anlamı açık olur ve bir örneğin değerlerine erişirken sıralarına güvenmek zorunda kalmazsınız. Tanım struct anahtar kelimesi, tipin adı ve süslü parantez içinde alan adları ile tipleriyle yazılır. Örnek oluştururken alanları tanımdaki sırayla yazmanız gerekmez; tanım bir şablon, örnek ise o şablonu somut verilerle dolduran bir değerdir:

rust
struct Kullanici {
    aktif: bool,
    kullanici_adi: String,
    eposta: String,
    giris_sayisi: u64,
}

fn kullanici_olustur(eposta: String, kullanici_adi: String) -> Kullanici {
    Kullanici {
        aktif: true,
        kullanici_adi,
        eposta,
        giris_sayisi: 1,
    }
}

fn main() {
    let mut k1 = kullanici_olustur(
        String::from("ayse@example.com"),
        String::from("ayse"),
    );
    k1.giris_sayisi += 1;
    k1.eposta = String::from("ayse.yilmaz@example.com");
    println!("{} {} {} {}", k1.kullanici_adi, k1.eposta, k1.aktif, k1.giris_sayisi);

    let k2 = Kullanici {
        eposta: String::from("baska@example.com"),
        ..k1
    };
    println!("{} {}", k2.kullanici_adi, k2.eposta);
    println!("{}", k1.eposta);
}
// çıktı: ayse ayse.yilmaz@example.com true 2
// çıktı: ayse baska@example.com
// çıktı: ayse.yilmaz@example.com

Bu örnekte birkaç kural birlikte görünüyor. Bir alanı değiştirmek için bütün örneğin mut olması gerekir; Rust yalnızca belirli alanları değiştirilebilir olarak işaretlemenize izin vermez. Bir fonksiyonun son ifadesi olarak yeni bir struct örneği yazarak onu örtük olarak döndürebilirsiniz. kullanici_olustur içindeki kullanici_adi, ve eposta, satırları alan kısaltmasıdır (field init shorthand): parametre adı alan adıyla aynı olduğunda eposta: eposta yerine yalnızca eposta yazabilirsiniz.

Struct güncelleme söz dizimi ve taşıma

Örneğin ikinci yarısındaki ..k1, struct güncelleme söz dizimidir: açıkça belirtilmeyen alanlar k1'deki karşılıklarından alınır. ..k1 her zaman en sonda yazılmalıdır, ama ondan önce istediğiniz kadar alanı istediğiniz sırayla belirtebilirsiniz. The Book'un vurguladığı kritik ayrıntı, bu söz diziminin bir atama gibi veriyi taşımasıdır. kullanici_adi alanı bir String olduğu için k2'ye taşınır ve artık k1 üzerinden kullanılamaz:

rust
// ..k1 ile k2 oluşturulduktan sonra:
println!("{} {}", k2.aktif, k1.kullanici_adi);
// error[E0382]: borrow of moved value: `k1.kullanici_adi`
// note: move occurs because `k1.kullanici_adi` has type `String`, which does not implement the `Copy` trait

Buna karşılık ilk örnekte k1.eposta'yı sorunsuz yazdırabildik, çünkü k2'ye yeni bir e-posta verdik ve k1'in e-postası taşınmadı. aktif ve giris_sayisi gibi Copy trait'ini uygulayan alanlar ise taşınmaz, kopyalanır. Yani .. sonrasında eski örneğin hangi alanlarını kullanmaya devam edebileceğiniz, hangi alanların taşındığına bağlıdır. Taşıma ve kopyalama kuralları ownership yazısında, bu hatanın çözüm yolları E0382 hatası yazısında anlatılıyor.

Tuple struct ve birim struct

Tuple struct'lar, struct adının kattığı anlamı taşıyan ama alanlarına isim verilmeyen struct'lardır. Bütün tuple'a bir isim vermek ve onu başka tuple'lardan ayrı bir tip yapmak istediğinizde, her alana isim vermenin gereksiz olacağı durumlarda kullanışlıdırlar. Alanı hiç olmayan birim benzeri struct'lar (unit-like struct) ise () gibi davranır; bir tipe veri tutmadan trait uygulamak istediğinizde işe yarar:

rust
struct Renk(i32, i32, i32);
struct Nokta(i32, i32, i32);
struct HerZamanEsit;

fn kirmizi_mi(r: Renk) -> bool {
    r.0 == 255 && r.1 == 0 && r.2 == 0
}

fn main() {
    let siyah = Renk(0, 0, 0);
    let baslangic = Nokta(0, 0, 0);
    let Nokta(x, y, z) = baslangic;
    println!("{} {} {}", siyah.0, siyah.1, siyah.2);
    println!("{x} {y} {z}");
    println!("{}", kirmizi_mi(Renk(255, 0, 0)));
    let _bos = HerZamanEsit;
    println!("{}", std::mem::size_of::<HerZamanEsit>());
}
// çıktı: 0 0 0
// çıktı: 0 0 0
// çıktı: true
// çıktı: 0

Renk ve Nokta aynı üç i32 alandan oluşsa da tanımladığınız her struct kendi başına ayrı bir tiptir. Renk bekleyen bir fonksiyona Nokta verirseniz derleyici reddeder: error[E0308]: mismatched types, ayrıntısında expected `Renk`, found `Nokta`. Tuple struct'ı parçalarken tipin adını yazmanız gerekir (let Nokta(x, y, z) = baslangic;); düz tuple'larda bu gerekmez. Son satır birim struct'ın boyutunu gösteriyor: hiç veri tutmadığı için 0 bayttır.

Struct verisinin sahipliği

Kullanici tanımında &str yerine sahipli String tipini kullandık. Bu bilinçli bir tercihtir: her örneğin kendi verisinin sahibi olmasını ve bu verinin struct geçerli olduğu sürece geçerli kalmasını istiyoruz. Struct'larda başka birinin sahip olduğu veriye referans tutmak da mümkündür, ama bunun için lifetime belirtmeniz gerekir; aksi hâlde derleyici reddeder:

rust
struct Kullanici {
    kullanici_adi: &str,
    aktif: bool,
}
// error[E0106]: missing lifetime specifier
// help: consider introducing a named lifetime parameter

Derleyicinin önerdiği struct Kullanici<'a> yazımı ve lifetime'ların neyi garanti ettiği lifetime yazısında anlatılıyor. Başlangıç için kural basit: struct alanlarında referans yerine sahipli tipleri (String, Vec<T>) tercih edin.

Debug ile yazdırma ve dbg!

Bir struct örneğini println!("{d}") ile yazdırmayı denerseniz derleme hatası alırsınız. Süslü parantezler Display biçimini kullanır ve ilkel tiplerin aksine struct'lar için tek bir doğru gösterim olmadığından (virgüller olsun mu, süslü parantezler yazılsın mı, bütün alanlar gösterilsin mi) Rust tahmin yürütmez:

rust
// error[E0277]: `Dikdortgen` doesn't implement `std::fmt::Display`
// note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead

Geliştiriciye yönelik çıktı için Debug biçimi vardır, ama struct'ınızda bunu açıkça istemeniz gerekir: tanımın hemen önüne #[derive(Debug)] özniteliğini eklersiniz. Sonra {:?} tek satırlık, {:#?} çok satırlık çıktı verir. dbg! makrosu ise ifadenin sahipliğini alır, dosya adı ve satır numarasıyla birlikte değeri yazdırır ve sahipliği geri verir; bu yüzden bir alan değerinin ortasına bile yerleştirilebilir:

rust
#[derive(Debug)]
struct Dikdortgen {
    genislik: u32,
    yukseklik: u32,
}

fn main() {
    let olcek = 2;
    let d = Dikdortgen {
        genislik: dbg!(30 * olcek),
        yukseklik: 50,
    };
    println!("{d:?}");
    println!("{d:#?}");
    dbg!(&d);
}
// stderr: [s6.rs:10:19] 30 * olcek = 60
// çıktı: Dikdortgen { genislik: 60, yukseklik: 50 }
// çıktı: Dikdortgen {
// çıktı:     genislik: 60,
// çıktı:     yukseklik: 50,
// çıktı: }
// stderr: [s6.rs:15:5] &d = Dikdortgen {
// stderr:     genislik: 60,
// stderr:     yukseklik: 50,
// stderr: }

dbg! çıktısını standart hataya (stderr), println! ise standart çıktıya (stdout) yazar; yorumlarda bu ayrımı gösterdik. dbg!(&d) satırında referans verdik, çünkü dbg!'ın d'nin sahipliğini almasını istemiyoruz. İlginç bir uç durum: bu program derlenirken Rust 1.99.1 alanların hiç okunmadığını söyleyen bir dead_code uyarısı verir ve notunda türetilmiş Debug uygulamasının ölü kod analizinde bilerek yok sayıldığını belirtir. derive ile kazanılabilen diğer trait'ler için derive yazısına, kendi Display uygulamanızı yazmak için Display ve Debug formatlama yazısına bakın.

impl blokları ve metotlar

impl blokları ve metotlar: &self, &mut self, self, self yok

Metotlar fonksiyonlara benzer, ama bir struct (ya da enum veya trait object) bağlamında tanımlanır ve ilk parametreleri her zaman metodun çağrıldığı örneği temsil eden self'tir. Bir tipe metot eklemek için impl (implementation) bloğu açarsınız; bloğun içindeki her şey o tiple ilişkilendirilir. Aşağıdaki örnek tek bir tipte farklı alıcı türlerini bir arada gösteriyor:

rust
#[derive(Debug)]
struct Dikdortgen {
    genislik: u32,
    yukseklik: u32,
}

impl Dikdortgen {
    fn new(genislik: u32, yukseklik: u32) -> Self {
        Self { genislik, yukseklik }
    }

    fn kare(boyut: u32) -> Self {
        Self { genislik: boyut, yukseklik: boyut }
    }

    fn alan(&self) -> u32 {
        self.genislik * self.yukseklik
    }

    fn sigdirabilir_mi(&self, diger: &Dikdortgen) -> bool {
        self.genislik > diger.genislik && self.yukseklik > diger.yukseklik
    }

    fn olcekle(&mut self, kat: u32) {
        self.genislik *= kat;
        self.yukseklik *= kat;
    }

    fn genislik(&self) -> bool {
        self.genislik > 0
    }

    fn boyutlar(self) -> (u32, u32) {
        (self.genislik, self.yukseklik)
    }
}

impl Dikdortgen {
    fn cevre(&self) -> u32 {
        2 * (self.genislik + self.yukseklik)
    }
}

fn main() {
    let d1 = Dikdortgen::new(30, 50);
    let d2 = Dikdortgen::new(10, 40);
    let d3 = Dikdortgen::new(60, 45);
    println!("alan: {}", d1.alan());
    println!("d1 d2'yi alır mı? {}", d1.sigdirabilir_mi(&d2));
    println!("d1 d3'ü alır mı? {}", d1.sigdirabilir_mi(&d3));

    let mut k = Dikdortgen::kare(3);
    k.olcekle(2);
    println!("{k:?} çevre: {}", k.cevre());

    if d1.genislik() {
        println!("genişlik sıfırdan büyük: {}", d1.genislik);
    }

    println!("{}", (&d1).alan() == d1.alan());
    let (g, y) = d1.boyutlar();
    println!("{g} x {y}");
}
// çıktı: alan: 1500
// çıktı: d1 d2'yi alır mı? true
// çıktı: d1 d3'ü alır mı? false
// çıktı: Dikdortgen { genislik: 6, yukseklik: 6 } çevre: 24
// çıktı: genişlik sıfırdan büyük: 30
// çıktı: true
// çıktı: 30 x 50

&self, aslında self: &Self yazımının kısaltmasıdır; impl bloğunun içinde Self, bloğun ait olduğu tipin (burada Dikdortgen) takma adıdır. Metotlar diğer parametreler gibi self'i değişmez ödünç alabilir (&self), değiştirilebilir ödünç alabilir (&mut self) ya da sahipliğini alabilir (self). alan yalnızca okuduğu için &self, olcekle alanları değiştirdiği için &mut self alır. self'in sahipliğini alan metotlar nadirdir; The Book'a göre genellikle örneği başka bir şeye dönüştüren ve çağıranın eski örneği kullanmasını engellemek istediğiniz durumlarda kullanılır. boyutlar çağrıldıktan sonra d1 artık kullanılamaz:

rust
let b = d.boyutlar();
println!("{:?} {}", b, d.genislik);
// error[E0382]: borrow of moved value: `d`
// note: `Dikdortgen::boyutlar` takes ownership of the receiver `self`, which moves `d`

Bir metodu alan adıyla aynı isimde tanımlamak da mümkündür. Örnekteki genislik() metodu genişliğin sıfırdan büyük olup olmadığını döndürür; parantezli d1.genislik() metodu, parantezsiz d1.genislik alanı ifade eder. Yalnızca alanın değerini döndüren bu tür metotlara getter denir ve Rust bunları otomatik üretmez. Getter'lar, alanı gizli (private) tutup metodu açık (public) yaparak salt okunur erişim sağlamak için kullanışlıdır; görünürlük kuralları modül sistemi yazısında anlatılıyor.

AlıcıAnlamıÇağrıdan sonra örnekÖrnek metot
&selfDeğişmez ödünç alırKullanılabiliralan, sigdirabilir_mi
&mut selfDeğiştirilebilir ödünç alırKullanılabilir, örnek mut olmalıolcekle
selfSahipliği alırTaşınmıştır, kullanılamazboyutlar
self yokİlişkili fonksiyon, metot değilTip::isim() ile çağrılırnew, kare

Otomatik referanslama: -&gt; operatörü nerede?

C ve C++'ta bir nesnenin metodunu doğrudan çağırırken nokta, bir işaretçi üzerinden çağırırken -> kullanılır. Rust'ta -> operatörünün karşılığı yoktur; bunun yerine otomatik referanslama ve referans çözme vardır ve metot çağrıları Rust'ta bu davranışın görüldüğü az sayıdaki yerden biridir. nesne.metot() yazdığınızda Rust, nesneyi metodun imzasına uydurmak için gerekirse otomatik olarak &, &mut veya * ekler. Örnekteki (&d1).alan() == d1.alan() satırının true yazdırması bundandır. Bu, metotların açık bir alıcısı olduğu için mümkündür: metodun adı ve alıcı tipi bilindiğinde Rust metodun okuyup okumadığını, değiştirip değiştirmediğini ya da tükettiğini kesin olarak bilir.

Otomatik referanslama değişebilirlik kurallarını gevşetmez. &mut self alan bir metodu mut olmadan tanımlanmış bir örnek üzerinde çağırırsanız derleyici reddeder ve mut eklemenizi önerir:

rust
let s = Sayac { deger: 0 };
s.artir(); // artir(&mut self)
// error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
// help: consider changing this to be mutable

İlişkili fonksiyonlar ve birden çok impl

Bir impl bloğunda tanımlanan bütün fonksiyonlara ilişkili fonksiyon (associated function) denir, çünkü impl'in ait olduğu tiple ilişkilidirler. İlk parametresi self olmayanlar metot değildir, çünkü çalışmak için bir örneğe ihtiyaç duymazlar; String::from bunun tanıdık bir örneğidir. Bu tür fonksiyonlar çoğunlukla yeni bir örnek döndüren yapıcılar (constructor) olarak kullanılır ve sıklıkla new adını alır. The Book özellikle belirtir: new özel bir isim değildir ve dilde yerleşik bir anlamı yoktur. Çağırırken tip adı ve :: kullanılır (Dikdortgen::kare(3)); aynı söz dizimi modüllerin oluşturduğu isim alanları için de kullanılır.

Her struct'ın birden çok impl bloğu olabilir; örnekte cevre metodu ayrı bir blokta tanımlandı ve diğerleriyle aynı şekilde çağrıldı. Burada metotları ayırmak için bir neden yok, ama bu geçerli bir söz dizimidir ve generic tipler ile trait'ler konusunda gerçek bir işe yarar; trait yazısında bunun örneklerini görebilirsiniz. Metotlar yalnızca struct'lara değil enum'lara de tanımlanabilir. Reference'a göre bir tipin kendi (inherent) impl bloğu, tipin tanımlandığı crate içinde yazılmalıdır.

Sık yapılan hatalar

Sık yapılan hatalar: Yalnızca bir alanı mut yapmaya çalışmak, ..k1 sonrasında taşınmış alanı kullanmak, {} ile struct yazdırmak, Struct alanında lifetime'sız referans
  • Yalnızca bir alanı mut yapmaya çalışmak: Değişebilirlik örneğin tamamına aittir; let mut k1 yazın.
  • ..k1 sonrasında taşınmış alanı kullanmak: String gibi alanlar yeni örneğe taşınır; E0382 alırsınız.
  • {} ile struct yazdırmak: Display uygulanmamışsa E0277 alırsınız; hata ayıklama için #[derive(Debug)] ve {:?} kullanın.
  • Struct alanında lifetime'sız referans: &str alanı E0106 verir; sahipli String kullanın ya da lifetime ekleyin.
  • Gereksiz yere self almak: Sahipliği alan bir metot, çağrıdan sonra örneği kullanılamaz yapar; yalnızca okuyacaksanız &self alın.
  • Değiştiren metodu mut olmayan örnekte çağırmak: E0596 alırsınız; örneği mut tanımlayın.
  • Aynı alanlara sahip tuple struct'ları birbirinin yerine kullanmak: Her struct ayrı bir tiptir; E0308 alırsınız.

Sık Sorulan Sorular

Struct mı tuple mı kullanmalıyım?

Değerlerin anlamı önemliyse struct kullanın. The Book'un dikdörtgen örneğinde tuple ile yazılan sürümde genişliğin 0, yüksekliğin 1 numaralı indeks olduğunu akılda tutmak gerekir; struct ise alanlara isim vererek bu karışıklığı ortadan kaldırır.

Rust'ta sınıf (class) var mı?

Rust'ta class anahtar kelimesi yoktur. Veri struct ya da enum ile, davranış impl blokları ve trait'lerle tanımlanır. Kalıtım yerine ortak davranış trait'lerle paylaşılır.

new fonksiyonu zorunlu mu?

Hayır. new yalnızca yaygın bir isimlendirme alışkanlığıdır; yapıcı fonksiyonu istediğiniz isimle yazabilir ya da struct'ı doğrudan alanlarıyla oluşturabilirsiniz.

Bir metodun içinden aynı tipin başka metodunu nasıl çağırırım?

self.metot() biçiminde çağırırsınız. Otomatik referanslama sayesinde self'in referans olup olmadığını düşünmeniz gerekmez; yalnızca çağrılan metodun alıcı türü, mevcut metodun alıcısıyla uyumlu olmalıdır.

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 · Ownership · Referanslar ve borrowing · Enum ve Option · derive · Trait tanımlama · E0382 hatası