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
structbildirimi 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.