İçeriğe geç
academia.sh

Ders 07 / 12

Yapılar

Aynı Nokta değeri üç ayrı yazımla kuruluyor — tam yazım, güncelleme sözdizimi, ilişkili fonksiyon — ve üçü de bir ifade olarak değer üretiyor; struct bildiriminin kendisi hiçbir değer üretmiyor, yalnızca sonraki satırların kullanacağı bir ad tanıtıyor.

Yayın Güncelleme

Bu derste

  • Yapı değeri kurmanın üç yazımının da birer ifade olduğunu gösterir
  • struct bildiriminin bir öğe olduğunu ve tek başına değer üretmediğini açıklar
  • Yöntem ile ilişkili fonksiyonu alıcının varlığına göre ayırt eder
  • Adlandırılmış yapı ile demet yapısında alan sırasının kimliğini karşılaştırır
İçindekiler

Denetim Akışı dersi beş akış biçiminin değer üretip üretmediğini tek tek ölçtü: if ve match birer ifadeydi, loop yalnızca break ile değer taşıdı, while ile for hiçbir zaman değer vermedi. Bu ders aynı soruyu farklı bir malzemeye taşıyor. Artık akış biçimleri değil veri tipleri var ve soru değişmiyor: bir tipten değer kurmak bir ifade mi, yoksa tipin kendisini kaynağa yazmak mı?

Rust’ta birden çok alanı tek bir tip altında toplamanın yolu yapıdır (struct). Bu ders, bir yapının bildirilmesi ile o yapıdan bir değer kurulması arasındaki farkı üç ayrı yazımla ölçüyor — tam yazım, güncelleme sözdizimi ve ilişkili fonksiyon — ve üçünün de aynı sınıfa düştüğünü gösteriyor. Bildirimin kendisi hiçbir zaman o sınıfa girmiyor. Dersin ikinci yarısı ise bildirimin içindekilere, yani alanlara döner: aynı alan kümesi iki farklı yapı biçiminde — adı olan ve adı olmayan — ne kadar farklı davranıyor, bu da ayrıca ölçülüyor.

Bildirim Bir Öğedir, Değer Kurmak Bir İfadedir

struct Nokta { x: i32, y: i32 } satırı çalıştırıldığında hiçbir şey üretmez. Bu satır bir hesaplama değil, derleyiciye bir ad tanıtan bir bildirimdir: sonraki satırların Nokta adını kullanabilmesinin önkoşuludur, ama kendisi bir değer değildir. Bir önceki dersin terimiyle bu bir öğedir — kaynağın herhangi bir noktasında “çalışmaz”, yalnızca orada durur ve derleyicinin bileceği bir tip ekler.

Buna karşılık Nokta { x: 3, y: 4 } yazımı çalıştırıldığında bir değer üretir: iki tam sayıyı alıp bunları Nokta tipinde tek bir değere paketler. Bu bir ifadedir — sonucu bir değişkene bağlanabilir, bir fonksiyona argüman olarak geçirilebilir, bir listeye eklenebilir. Aradaki fark tam olarak önceki derste kurulan ayrımın kendisidir: biri değerlendirilmeden orada durur, öbürü değerlendirilip bir değer verir.

RV1. — Bir struct bildiriminin tek başına hiçbir çıktı üretmediği iddiası burada ayrı bir koşumla değil, aşağıdaki programın kendisiyle doğrulanıyor: struct Nokta { ... } satırı main içinde değil, mainden önce duruyor ve hiçbir yazdırma çağrısı o satırı tetiklemiyor. Program çalıştığında görünen tek çıktı, Nokta adının kullanıldığı satırlardan geliyor.

Bu ayrımın altında yatan neden, iki satırın derleyici için farklı zamanlarda işlenmesidir. Bir bildirim derleme zamanında (compile time) okunur: derleyici Nokta adını, alanlarını ve tiplerini tabloya kaydeder, ama bu kayıt işlemi programın çalışma zamanında (runtime) hiçbir iz bırakmaz. Bir değer kurma yazımı ise çalışma zamanında işler: Nokta { x: 3, y: 4 } satırı çalıştığında bellekte gerçekten iki tam sayı bir araya getirilir. let ile kurulan bir bağlamanın (Değişkenler ve Değişmezlik dersinin konusu) çalışma zamanında gerçekleşmesiyle aynı düzendedir bu — farkı, bağlamanın soluna bir öğe değil bir ifade gelmek zorunda olmasıdır.

Bu, üç sınıfın en berrak biçimde ayrıştığı yerlerden biridir. Bir deyim hâlâ değerlendirilir — yalnızca sonucu ()e düşer ya da atılır — ama bir öğe hiç değerlendirilmez; kaynakta durduğu yerde “çalışan” bir şey yoktur. struct bildirimi bu üçüncü kategoriye girer: program çalışırken üzerinden geçilen bir adım değil, derleyicinin kaynağı okurken bir kez işlediği bir kayıttır.

Aşağıdaki koşum, aynı Nokta tipinden değer kurmanın üç ayrı yolunu yan yana koyuyor.

// yapi_kurma.rs — struct bildirimi bir oge, yapi degeri kurmak bir ifadedir
struct Nokta {
    x: i32,
    y: i32,
}

impl Nokta {
    fn koken() -> Nokta {
        Nokta { x: 0, y: 0 }
    }

    fn boyut(&self) -> i32 {
        self.x.abs() + self.y.abs()
    }
}

fn main() {
    let a = Nokta { x: 3, y: 4 };
    println!("{:<21}: x={} y={}", "tam yazim", a.x, a.y);

    let b = Nokta { y: 9, ..a };
    println!("{:<21}: x={} y={}", "guncelleme sozdizimi", b.x, b.y);

    let c = Nokta::koken();
    println!("{:<21}: x={} y={}", "iliskili fonksiyon", c.x, c.y);

    println!("{:<21}: {}", "yontem sonucu", b.boyut());
}
tam yazim            : x=3 y=4
guncelleme sozdizimi : x=3 y=9
iliskili fonksiyon   : x=0 y=0
yontem sonucu        : 12

Tam yazım bütün alanları tek tek verir: x ve y açıkça yazılır, sonuç x=3 y=4 olan bir değerdir. Güncelleme sözdizimi (..a) bunun bir kısaltmasıdır: yalnızca y alanı yeniden verilir, geri kalan alanlar adan kopyalanır. Sonuç yine tek bir çağrıda üretilen bir Nokta değeridir — sözdizimi farklı, sınıf aynı. İlişkili fonksiyon (Nokta::koken()) ise bambaşka bir yoldan aynı sınıfa varır: x ve y hiç yazılmadan, işlev çağrısının kendisi bir Nokta üretir. Üç yazımın ortak noktası, üçünün de let ile bir ada bağlanabilmesidir — bu, bir ifadenin tanımı gereği zaten mümkündür, bir öğe için ise anlamsızdır.

Yöntem ile İlişkili Fonksiyon: Alıcının Varlığı

Nokta::koken() ile b.boyut() çağrıları görünüşte benzer: ikisi de impl Nokta bloğu içinde tanımlı, ikisi de bir değer döndürüyor. Aralarındaki fark çağrı sözdiziminde görünür ve tek bir noktaya iniyor: alıcının varlığı.

koken fonksiyonunun imzasında self yoktur; bu yüzden tip adıyla çağrılır (Nokta::koken()) ve bir örnek gerektirmez. Rust’ın kendi terimiyle bu bir ilişkili fonksiyondur — Nokta tipiyle ilişkilidir ama belirli bir Nokta değeriyle değil. boyut fonksiyonunun imzasında ise ilk parametre &selfdir; bu parametre çağrının hangi değer üzerinde yapıldığını belirtir ve çağrı noktasında görünmez, b.boyut() yazımındaki b onun yerini alır. Bu, Rust’ın yöntem dediği biçimdir.

Ayrım, işin ne yaptığıyla değil imzanın neyi zorunlu kıldığıyla ölçülür: self parametresi varsa çağrı bir değer üzerinden yapılır (yöntem), yoksa tip adı üzerinden yapılır (ilişkili fonksiyon). Yukarıdaki koşumda koken() hiçbir mevcut Noktaya ihtiyaç duymadan sıfırdan bir değer üretti; boyut() ise yalnızca zaten var olan b değeri üzerinden çağrılabildi. İkisi de birer ifadedir — ikisi de bir değer döndürür — ve bu, ifade/deyim/öğe eksenine yeni bir dal eklemiyor: Rust’ta () dönmeyen her fonksiyon çağrısı zaten bir ifadedir, yöntem olsun ilişkili fonksiyon olsun. Buradaki ayrım eksenin dışındadır — çağrı sözdizimine, alıcının olup olmamasına aittir.

boyut fonksiyonu, noktanın orijine olan uzaklığını iki mutlak değerin toplamıyla hesaplıyor: self.x.abs() + self.y.abs(). Ölçümde b, x=3 y=9 değerini taşıyordu; bu yüzden boyut() çağrısı 3 + 9 = 12 verdi. koken ise adından da anlaşılacağı gibi bir kurucu görevi görüyor: bir Nokta değeri üretmenin yalnızca üç yolundan biri değil, sık kullanılan bir adlandırma alışkanlığıdır — bir tipin en yaygın başlangıç değerini Tip::koken(), Tip::bos() ya da Tip::varsayilan() gibi bir ilişkili fonksiyonla sunmak. Bildirim aşamasında bu fonksiyonların kaç tane olacağına dair bir sınır yoktur; impl Nokta bloğu her ikisini de aynı yerde toplar.

Adlandırılmış yapı sözdizimi yalnızca değer kurarken alan adı ister; yöntem çağrısı sırasında alan adı hiç görünmez. b.boyut() yazımında b, çağrının hangi değer üzerinde yapıldığını belirtir, ama boyut fonksiyonunun içinde self.x ve self.y alanlarına adıyla erişilir. Alıcı, çağrı sözdiziminde tek bir noktaya (.) indirgenmiş görünse de, fonksiyonun gövdesinde alanlarına adıyla ulaşılan tam bir Nokta değeridir.

self parametresinin önünde tek bir & işareti bulunur. Bu işaretin tam anlamı ve alıcının değeri neden kopyalamadan kullanabildiği, Sahiplik ve Ödünç Alma dersinin konusudur; burada ölçülen yalnızca alıcının varlığının çağrı sözdizimini nasıl belirlediğidir, alıcının nasıl aktarıldığı değil.

Bir tipin impl bloğunda kaç ilişkili fonksiyon ya da yöntem bulunabileceğine dair bir üst sınır yoktur. Nokta gibi küçük bir tip bile birden çok kurucu ilişkili fonksiyona sahip olabilir — koken() yanında birim() -> Nokta ya da ikisi(x: i32, y: i32) -> Nokta gibi başka isimlerle başka varsayılan değerler sunulabilir. Hangi isimlerin kullanılacağı bir dil kuralı değil, bir adlandırma alışkanlığıdır; derleyici için hepsi aynı sınıfa, ilişkili fonksiyona, girer.

Alan Sırası Adlandırılmış Yapıda Kimlik Taşımaz

Bir yapı değeri kurarken alanların kaynakta yazılma sırası, bildirimdeki sırayla aynı olmak zorunda değildir. Nokta { x: 3, y: 4 } ile Nokta { y: 4, x: 3 } aynı değeri üretir; derleyici her alanı adına göre eşler, konumuna göre değil.

// alan_sirasi.rs — adlandirilmis yapida alan sirasi kimlik tasimaz
struct Nokta {
    x: i32,
    y: i32,
}

fn main() {
    let d = Nokta { x: 3, y: 4 };
    let e = Nokta { y: 4, x: 3 };
    println!("{:<18}: x={} y={}", "d (x once)", d.x, d.y);
    println!("{:<18}: x={} y={}", "e (y once)", e.x, e.y);
    println!("{:<18}: {}", "d ve e esit mi", d.x == e.x && d.y == e.y);
}
d (x once)        : x=3 y=4
e (y once)        : x=3 y=4
d ve e esit mi    : true

d ve e kaynakta farklı sırada yazıldı, ama ikisi de aynı x ve y değerlerine sahip. Alan adları burada bir konum belirteci değil, bir eşleştirme anahtarıdır: yazım sırası okurluk tercihidir, anlamı değiştirmez.

Bu, Rust’ın genel çağrı kuralının dışında kalan bir istisnadır. Bir fonksiyon çağrısında argümanlar konumla eşlenir — iki_kat(3) yazımında 3ün hangi parametreye gideceği, parametre listesindeki sırasıyla belirlenir, adıyla değil. Yapı kurma sözdizimi bunun tek istisnasıdır: { } içindeki her alan: değer çifti, struct bildirimindeki karşılığını adından bulur. Bu ayrıcalık yalnızca kurma sözdizimine aittir; bir yapı değerinin alanına erişirken (a.x gibi) zaten adla erişilir, orada bir “sıra” sorusu hiç doğmaz.

RV2. — “Sıra anlamı değiştirmiyor” iddiası burada ayrı bir alan karşılaştırma yöntemiyle değil, d.x == e.x && d.y == e.y satırının doğrudan alan alan karşılaştırmasıyla ölçülüyor; bu satır her iki değerin alanlarının tek tek birbirine eşit olduğunu okur, d ile enin kendisini karşılaştırmaz.

Demet Yapısında Sıra Tek Kimliktir

Adlandırılmış yapının tam karşıtı demet yapısıdır (tuple struct): alanların adı yoktur, yalnızca konumu ve tipi vardır. Renk(u8, u8, u8) bildirimi üç bayt taşıyan bir tip kurar, ama bu üç baytın hangisinin “kırmızı” hangisinin “mavi” olduğunu ad değil konum belirler.

// demet_kimligi.rs — demet yapisinda sira tek kimliktir
struct Renk(u8, u8, u8);

fn main() {
    let kirmizi = Renk(220, 20, 20);
    let Renk(r1, g1, b1) = kirmizi;
    println!("{:<18}: r={} g={} b={}", "Renk(220,20,20)", r1, g1, b1);

    let mavi = Renk(20, 20, 220);
    let Renk(r2, g2, b2) = mavi;
    println!("{:<18}: r={} g={} b={}", "Renk(20,20,220)", r2, g2, b2);
}
Renk(220,20,20)   : r=220 g=20 b=20
Renk(20,20,220)   : r=20 g=20 b=220

Bu sınırlayıcı ölçümdür: adlandırılmış yapıda alan sırası serbestken (bir önceki koşumda d ile e bunu gösterdi), demet yapısında konum tek kimliktir. Renk(220, 20, 20) ile Renk(20, 20, 220) aynı üç sayıyı taşımaz — ilkinde ilk konum büyük, ikincisinde üçüncü konum büyük — ve sonuç iki ayrı renk değeridir. Adlandırılmış yapıda “hangi alan” sorusunu ad yanıtlar; demet yapısında aynı soruyu yalnızca konum yanıtlar. Renk(20, 20, 220) yazımında ilk iki 20nin hangi alana ait olduğu, o değerin nerede durduğuna bakılarak çözülür.

RV3. — Demet yapısında “konum tek kimliktir” iddiası burada aynı üç sayının farklı konumlara yerleştirilmesiyle, yani Renk(220, 20, 20) ile Renk(20, 20, 220) çiftinin üretilmesiyle ölçülüyor: iki çağrı da aynı üç değeri (20, 20, 220nin bir permütasyonunu) taşıyor, ama konumları farklı olduğu için sonuçlar da farklı.

Yapısöküm (destructuring) burada da devreye giriyor: let Renk(r1, g1, b1) = kirmizi; satırı, kirmizi değerinin üç alanını sırayla r1, g1, b1 adlarına bağlıyor. Bu yazım aslında bir desendir — sağdaki değerin biçimini sol taraftaki adlarla eşler — ve desenle ayrıştırmanın tam kapsamı sıradaki dersin konusu. Burada yalnızca demet yapısının alanlarına erişmenin bu yoldan geçtiği görülüyor: adlandırılmış yapıda a.x yazılabilirken, demet yapısında alana adla değil konumla (renk.0, renk.1, renk.2) ya da yapısökümle erişilir.

Tek alanlı bir demet yapısı, aynı temel tipi taşıyan iki büyüklüğü birbirinden ayırmak için de kullanılır. struct Piksel(i32); ile struct Milimetre(i32); ikisi de içeride bir i32 taşır, ama derleyici için iki ayrı tiptir; bir Piksel değeri, alanı aynı görünse de, bir Milimetre değerinin yerine doğrudan geçmez. Bu, demet yapısının en sık kullanıldığı biçimdir: mevcut bir tipe yeni bir isim değil, yeni bir kimlik kazandırmak. Adlandırılmış yapıda aynı amaca alan adıyla ulaşılır (Konum { piksel: i32 } gibi); demet yapısı bunu tek bir sarmalayıcı alanla, alan adı hiç yazılmadan yapar.

RV4. — Adlandırılmış yapı ile demet yapısı arasındaki karşıtlık, iki ayrı koşumun yan yana konmasıyla ölçülüyor: alan_sirasi.rs sırayı değiştirip sonucun aynı kaldığını, demet_kimligi.rs konumu değiştirip sonucun farklı olduğunu gösteriyor. İki koşum da aynı üç girdiyi (üç sayı) kullanıyor; değişen yalnızca tipin adlı mı adsız mı olduğu.

Yapı, Rust’ın veriyi bir arada tutmanın en dolaysız yoludur: sabit sayıda alan, her biri tek bir tipe bağlı, hepsi aynı anda var. Bu dersin ölçtüğü üç yazım — tam yazım, güncelleme sözdizimi, ilişkili fonksiyon — hepsi aynı sonuca varır çünkü hepsi aynı sabit alan kümesini doldurur. Ama her yapı değeri, sabit bir alan kümesine mahkûm değildir; bir programın modellemesi gereken veri çoğu zaman “ya şu ya bu” biçiminde birden fazla olası şekle bürünür. Yapı bunu tek başına karşılayamaz — bir Nokta her zaman hem x hem y taşır, üçüncü bir “hiç taşımıyor” durumu yoktur. Sıradaki ders bu boşluğu dolduran tipi kuruyor.

Özet

  • struct bildirimi bir öğedir: çalıştırıldığında hiçbir değer üretmez, yalnızca bir ad tanıtır.
  • Bir yapı değeri kurmanın üç yazımı — tam yazım, güncelleme sözdizimi, ilişkili fonksiyon — üçü de birer ifadedir ve bir değer üretir.
  • Yöntem ile ilişkili fonksiyon arasındaki fark alıcının (self) varlığıdır: yöntem bir değer üzerinden, ilişkili fonksiyon tip adı üzerinden çağrılır.
  • Adlandırılmış yapıda alan sırası anlam taşımaz; alanlar ada göre eşlenir.
  • Demet yapısında (tuple struct) alanların adı yoktur, yalnızca konumu vardır; konum tek kimliktir ve değiştirildiğinde sonuç değişir.

Sonraki Adım

Bu ders, bir yapının kaç alan taşıdığını ve bu alanların nasıl kurulduğunu ölçtü — ama bir yapı değeri her zaman aynı alan kümesini taşır. Sıradaki ders aynı soruyu farklı bir biçime taşıyor: bir tipin, hangi alanları taşıdığı duruma göre değişen birden çok biçimi olabilir mi? Numaralandırmalar (enum), bir değerin aynı tip altında birden fazla ayrı durumdan birini taşıyabilmesini kurar; bir sonraki soru bu durumların kaynaktan nasıl okunduğudur.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat