Ders 02 / 14
Sahiplik ve Taşıma
Bir değerin işleve, iç kapsama ve yapı alanına taşınmasının üçü de aynı sınıfa (taşındığı yerde) giriyor ve üçünde de değer tam olarak bir kez bitiyor; taşımadan sonra eski bağlamanın kullanılamaması derleyicinin derleme zamanında sınadığı bir kural, yeni sahip ise değeri sorunsuz kullanabiliyor.
Yayın Güncelleme
Bu derste
- Bir değerin işleve, iç kapsama ve yapı alanına taşınmasının üçünün de taşındığı yerde sınıfına girdiğini koşturarak gösterir
- Taşımadan sonra eski bağlamanın neden kullanılamadığını açıklar ve yeni sahibin değeri kullanabildiğini kanıtlar
- Taşımanın bir kopyalama olmadığını, defterde her değer için tam olarak bir kayıt oluştuğunu ölçerek doğrular
İçindekiler
Önceki ders bir değerin kapsam sonunda nasıl bittiğini ölçtü ve taşındığı yerde sınıfına yalnız üç örnekle değindi. Bu ders o sınıfı derinleştiriyor: sahiplik bir yerden başka bir yere geçtiğinde bitiş noktasına tam olarak ne oluyor? Eşzamanlılık kursunun (M09/K03) sorduğu “bir goroutine nasıl duracak” sorusu buna benzer görünse de ayrıdır: orada koşan bir birim durur, burada bir değer biter.
Programlama Temelleri kursu (M01/K02) bir değerin bir işleve nasıl geçtiğini — değerle aktarım — kavram olarak kurmuştu; o kursta bu aktarım biçimi Python üzerinden anlatılmış, işleve geçen bir değerin çağıranın elindeki adı etkilemediği gösterilmişti. Bu derste ölçülen, Rust’ın aynı sözdizimine (bir değeri bir işleve argüman olarak yazmak) tamamen farklı bir anlam yüklemesi: bir işleve geçen bir değer, artık işlevin de sahibi olur ve çağıranın elindeki ad geçerliliğini yitirir. Bu sahiplik değişimi derleme zamanında sınanıyor, çalışma zamanında değil. Bir değerin bir referans üzerinden, sahiplik vermeden erişilmesi ayrı bir yol — o yol Ödünç Alma dersinde ele alınacak.
Üç Taşıma Biçimi
Aynı sınıfa (taşındığı yerde) üç ayrı yoldan girilebiliyor. Bir değer bir işleve argüman olarak geçirilebilir; bir değer aynı kapsamın içinde açılan bir iç kapsamda yeni bir ada yeniden bağlanabilir; bir değer bir yapının alanına konabilir. Üçünde de ortak olan şey, eski bağlamanın artık değeri tutmuyor olması — değer aynı kalıyor, yalnız ona hangi adın eriştiği değişiyor.
SK4. Bir değerin iç kapsamda yeni bir ada bağlanması (let yeni_sahip = ic;), önceki
dersteki gölgelemeyle karıştırılmamalı. Gölgeleme aynı ad üzerinde yeni bir değer
yaratıyordu ve eskisi kendi kapsam sonuna kadar ayrıca yaşıyordu. Burada tam tersi
oluyor: aynı değer, farklı bir ada taşınıyor ve eski ad bir daha o değere erişemiyor.
İkisi de bir “yeniden bağlama” gibi görünse de, biri yeni bir ömür başlatıyor, öbürü var
olan bir ömrün sahibini değiştiriyor.
Üç biçim farklı sözdizimleriyle görünse de aynı tek düzeneğin üç kullanım yeridir: Rust’ta
bir değeri Copy işaretli olmayan bir tipte herhangi bir yere yazmak — bir parametreye, bir
değişkene, bir alana — o değeri oradan oraya taşır. Söz dizimi hep aynı görünüyor
(= işareti, bir çağrı), ama arkasındaki anlam her seferinde aynı: eski konumdaki bit
kalıbı yeni konuma taşınıyor ve eski konum artık geçerli sayılmıyor. Bu derste üç ayrı
yerin (parametre, iç kapsam bağlaması, yapı alanı) hepsinin bu tek düzeneğe indirgendiği
ölçülüyor — üç farklı kural değil, bir kuralın üç görünümü.
Üç biçim pratikte de farklı tasarım kararlarına karşılık geliyor. Bir değeri işleve taşımak, işlevin o değer üzerinde son söz sahibi olmasını istediğinde tercih ediliyor — işlev bitince değer de bitiyor, çağıran tarafın onunla bir daha işi olmuyor. Bir değeri iç kapsamda yeniden bağlamak, aynı değeri farklı bir isimle, genellikle daha dar ve geçici bir işlem için tutmak istendiğinde görülüyor. Bir değeri bir yapının alanına koymak ise değeri kalıcı olarak daha büyük bir bütünün parçası yapıyor — bundan sonra değerin ömrü, kendi kapsamına değil, o yapının ömrüne bağlanıyor. Üçü de aynı düzeneği kullanıyor, ama hangi birimin (işlev, kapsam, yapı) değerin yeni sınırını çizeceğine göre seçiliyor.
Bu seçim çoğu zaman veri modelinin kendisinden çıkıyor. Bir kayıt işleyen bir program, kaydı doğrulayan bir işleve taşıyorsa (işleve taşıma), doğrulama bitince kaydın orada kalmasını bekliyordur — çağıran taraf zaten kaydı bir daha kullanmayacaktır. Aynı program bir kaydı geçici olarak dönüştürülmüş bir biçimde tutmak istiyorsa (iç kapsama taşıma), dönüşüm yalnız o dar kapsamda geçerli kalıyor. Bir kayıt kalıcı bir yapının (örneğin bir listenin ya da bir önbelleğin) parçası olacaksa (yapı alanına taşıma), sahiplik baştan o yapıya veriliyor. Üç biçim arasında seçim, kod yazarken “bu değer artık kime ait olacak” sorusuna verilen yanıtla belirleniyor.
Ölçümün Kesiti
Önceki dersin çekirdeğinden yalnız Iz, Drop uygulaması ve gözlem defteri kesiti
alınıyor; A, C, D bölümlerine bu derste ihtiyaç yok. Yardımcı kısım ayrı bir dosyaya
konuyor ki üç taşıma biçimi tek bir akışta yan yana görülebilsin.
Yardımcı dosya kendi başına derlenmiyor — iz.rs dosyasında bir giriş noktası yok, bu
yüzden çalıştırılamıyor, yalnız çalışma dizinine yazılıyor. tasima.rs dosyasının en
başındaki mod iz; satırı bu dosyayı asıl derlemenin içine çekiyor; use iz::{Iz, yeni_defter, okunan}; ise oradan hangi adların doğrudan kullanılacağını belirtiyor. Iz
yapısının ad alanı ve yeni/yeni_defter/okunan işlevleri modülün dışından
erişilebilmesi için pub işaretli; defter alanı ise işaretli değil, çünkü ona yalnız
Izin kendi yöntemleri erişiyor. Görünürlüğün bu şekilde tek tek işaretlenmesi Kurulum ve
Paket Yöneticisi dersinde (M10/K01) zaten kurulmuştu; burada yalnız bir uygulaması var.
// iz.rs — bir degerin bitisini kaydeden olcum yardimcisi
use std::cell::RefCell;
use std::rc::Rc;
pub type Defter = Rc<RefCell<Vec<String>>>;
pub struct Iz {
pub ad: String,
defter: Defter,
}
impl Iz {
pub fn yeni(ad: &str, defter: &Defter) -> Iz {
Iz { ad: ad.to_string(), defter: Rc::clone(defter) }
}
}
impl Drop for Iz {
fn drop(&mut self) {
self.defter.borrow_mut().push(self.ad.clone());
}
}
pub fn yeni_defter() -> Defter {
Rc::new(RefCell::new(Vec::new()))
}
pub fn okunan(defter: &Defter) -> Vec<String> {
defter.borrow().clone()
}
// tasima.rs — uc tasima bicimi: isleve, ic kapsama, yapi alanina
mod iz;
use iz::{Iz, yeni_defter, okunan};
struct Kutu {
icerik: Iz,
}
fn yut(alinan: Iz) {
println!("islev icinde okunan ad: {}", alinan.ad);
}
fn main() {
let defter = yeni_defter();
let tasinan = Iz::yeni("islev-icine", &defter);
yut(tasinan);
println!("islev donduktan sonra defter: [{}]", okunan(&defter).join(", "));
let ic = Iz::yeni("ic-kapsama", &defter);
{
let yeni_sahip = ic;
println!("ic kapsamda okunan ad: {}", yeni_sahip.ad);
}
println!("ic kapsam bittikten sonra defter: [{}]", okunan(&defter).join(", "));
let alan = Iz::yeni("yapi-icine", &defter);
{
let kutu = Kutu { icerik: alan };
println!("yapi alaninda okunan ad: {}", kutu.icerik.ad);
}
println!("yapi kapsami bittikten sonra defter: [{}]", okunan(&defter).join(", "));
let kayitlar = okunan(&defter);
println!();
println!("toplam kayit: {}", kayitlar.len());
for ad in ["islev-icine", "ic-kapsama", "yapi-icine"] {
let adet = kayitlar.iter().filter(|k| k.as_str() == ad).count();
println!("{:<14}{} kayit", ad, adet);
}
}
islev icinde okunan ad: islev-icine islev donduktan sonra defter: [islev-icine] ic kapsamda okunan ad: ic-kapsama ic kapsam bittikten sonra defter: [islev-icine, ic-kapsama] yapi alaninda okunan ad: yapi-icine yapi kapsami bittikten sonra defter: [islev-icine, ic-kapsama, yapi-icine] toplam kayit: 3 islev-icine 1 kayit ic-kapsama 1 kayit yapi-icine 1 kayit
Üç satır da aynı düzeni izliyor: değer önce yeni sahibinde okunuyor (alinan.ad,
yeni_sahip.ad, kutu.icerik.ad), sonra yeni sahibin kendi kapsamı kapanınca deftere
düşüyor. yut işlevinin gövdesi bitince islev-icine hemen deftere yazılıyor — işlevin
kendi kapsamı, taşınan değerin yeni ve son kapsamı oluyor. İç kapsamdaki yeni_sahip ve
yapı alanındaki kutu.icerik için de aynı şey geçerli: ikisi de kendi dar kapsamları
kapanır kapanmaz bitiyor, dış kapsamın kapanmasını beklemiyor.
Erişim biçimi de üç formda farklılaşıyor. İşleve taşınan değere doğrudan parametre adıyla
ulaşılıyor (alinan), iç kapsama taşınan değere yeni bağlamanın adıyla (yeni_sahip), ama
yapı alanına taşınan değerin artık kendi başına bir adı yok — yalnız kutu.icerik yoluyla,
sahibi olan yapı üzerinden erişilebiliyor. Bu üçüncü biçim, taşımanın yalnız “bir değişkeni
başka bir değişkene kopyalamak” olmadığını gösteriyor: değer bir yapının parçası
hâline geldiğinde kendi bağımsız adını kaybediyor, artık yalnız kendisini saran yapının
alan yolu üzerinden görünüyor.
Eski Bağlamanın Kullanılamaması
Kod bu üç taşımadan sonra eski adı (tasinan, ic, alan) bir daha okumuyor — çünkü
okuyamaz. yut(tasinan) çağrısından sonra tasinan.ad’a erişmeye çalışan bir satır
eklenseydi, derleyici bunu reddederdi: sahiplik yutun parametresine geçti ve tasinan
artık geçerli bir değere işaret etmiyor. Aynı kural ic ve alan için de geçerli.
SK5. Bu redd bir çalışma zamanı hatası değil, derleme zamanında sınanan bir kuraldır. Taşınmış bir adı yeniden okumaya çalışan bir kaynak hiç derlenmez; program hiçbir zaman bu satıra ulaşmaz, bu yüzden yanlış kullanım çalışma zamanında hiçbir maliyete yol açmaz. Bu, kabul edilen yazımın (yukarıdaki üç blok) neden hiçbir yerde eski adı tekrar okumadığını da açıklıyor — doğru yazım zaten bu kısıtın içinde kalıyor.
Bu ret, önceki dersteki tek sahip gözlemiyle aynı yerden geliyor: bir değere aynı anda iki ayrı adın erişebilmesi, o değerin hangi adın kapsamı kapanınca biteceğini belirsizleştirirdi. Derleyici bunu, taşıma anında eski adı geçersiz sayarak önlüyor — eski ad hâlâ kaynakta duruyor, ama bir daha kullanılamıyor.
Bu kısıtın neyi önlediğini somutlaştırmak için bir zihin deneyi yeterli: tasinan taşımadan
sonra da geçerli kalsaydı, hem tasinan hem alinan aynı Iz örneğine erişebilirdi. İkisi
de kendi kapsamı kapanınca bu örneği bitirmeye çalışırdı ve aynı değer iki kez bitirilmiş
olurdu — deftere aynı ad iki kez yazılırdı. Derleyici bu durumu hiç doğurmuyor; taşıma
anında yalnız bir adın geçerli kaldığından emin oluyor, geri kalanı (bitişin ne zaman
olacağı) önceki dersin kurallarına bırakıyor.
Taşıma Zincirlenebilir
Üç biçim tek bir taşımayı gösterdi, ama taşıma art arda da olabilir: bir değer bir işleve
girip oradan başka bir işleve, hatta geri çağırana devredilebilir. Aşağıdaki örnek aynı
Iz değerinin iki ayrı işlev sınırını geçip üçüncü bir kapsamda bitmesini ölçüyor.
// zincir.rs — tasima zincirlenebilir: sahiplik bir islevden digerine devredilebilir
mod iz;
use iz::{Iz, yeni_defter, okunan};
fn ikinci_asama(deger: Iz) -> Iz {
println!("ikinci asamada okunan ad: {}", deger.ad);
deger
}
fn ilk_asama(deger: Iz) {
let devredilen = ikinci_asama(deger);
println!("ilk asamaya donen ad: {}", devredilen.ad);
}
fn main() {
let defter = yeni_defter();
let deger = Iz::yeni("zincirli", &defter);
ilk_asama(deger);
println!("zincir bittikten sonra defter: [{}]", okunan(&defter).join(", "));
}
ikinci asamada okunan ad: zincirli ilk asamaya donen ad: zincirli zincir bittikten sonra defter: [zincirli]
deger burada dört ayrı adın elinden geçiyor: main’deki deger, ilk_asama’nın
parametresi, ikinci_asama’nın parametresi ve ikinci_asama’nın döndürdüğü değeri
tutan devredilen. Her geçiş bir taşıma ve her taşımadan sonra bir önceki ad artık
geçersiz. Zincirin sonunda değer devredilene ait ve ilk_asamanın gövdesi bitince orada
bitiyor — mainde değil. Dört ad, tek bir değer, tek bir bitiş: defterde yalnız
zincirli görünüyor, kaç kez taşındığından bağımsız olarak.
Bu, “taşındığı yerde” sınıfının neden bir sayıyla değil bir konumla tanımlandığını
gösteriyor. Değerin kaç kez el değiştirdiği önemli değil; ölçülen şey yalnız son sahibin
kapsamı. ikinci_asamanın değeri geri döndürmesi de bir taşımadır — dönüş değeri, çağıran
tarafın yeni bir bağlamasına taşınıyor; bu yüzden devredilen de kendi payına düşen bir
taşımanın sonucu.
Bir işlevin sahip olduğu bir değeri geri döndürmesi, bu kursta tekrar karşılaşılacak bir düzen. Bir işlev kendi içinde kurduğu ya da parametre olarak aldığı bir değeri çağırana geri vermek istediğinde, seçenek bir referans değil bir sahiplik devri oluyor — değer işlevin dönüş tipi olarak yazılıyor ve dönüş, kendi başına bir taşıma sayılıyor. Bu düzen, Ödünç Denetleyicisi dersinde bir işlevin kendi içinde kurduğu bir değere referans döndürmesinin neden reddedildiğini anlamak için de gerekiyor: reddedilen tam olarak bu — referans döndürmek; kabul edilen ise burada görülen — değerin kendisini döndürmek.
Sınırlayıcı Ölçüm: Taşıma Bir Kopyalama Değildir
Çıktının son üç satırı bunu doğrudan gösteriyor: islev-icine, ic-kapsama ve
yapi-icine adlarının üçü de defterde tam olarak bir kez geçiyor. Değer üç ayrı adı
sırayla taşısaydı bile (burada olmuyor, ama farz edelim), her taşıma bir kopya
oluşturmuyor — aynı Iz örneği, aynı bellek konumu, yalnız erişim adı değişiyor.
SK6. Üç taşıma biçiminin üçü de defterde tam olarak bir kayıt üretti; bu, aynı değerin iki kez bitirilmeye çalışılmasının kaynak düzeyinde imkânsız olduğunu gösteriyor. Sahiplik her zaman tek bir adda durduğu için bitiş noktası da her zaman tek bir yerde tetikleniyor — taşıma bu tekliği bozmuyor, yalnız hangi adın sorumlu olduğunu değiştiriyor.
Taşımanın ömrü kısaltmadığı da aynı ölçümden okunuyor. islev-icine taşınmadan önce de,
taşındıktan sonra da aynı Iz örneği; taşıma bir yeniden oluşturma değil, bir yeniden
adlandırma. Önceki derste görülen dört sınıftan “taşındığı yerde” bu yüzden kendi başına bir
sınıf: değer ne kapsam sonunu bekliyor ne de açık bir çağrı alıyor, yalnız sahibinin kendi
sonunu miras alıyor.
Karşıt durumu düşünmek farkı netleştiriyor. Iz değerleri Clone uygulasaydı ve üç satır
yut(tasinan.clone()), let yeni_sahip = ic.clone(); biçiminde yazılsaydı, defterde altı
kayıt olurdu: özgün üç değer kendi kapsamlarında, üç kopya da kendi kapsamlarında ayrı ayrı
biterdi. Bu derste clone hiç çağrılmadı — taşıma yeni bir Iz örneği kurmuyor, var olanı
elden ele geçiriyor. Defterin üç kaydı, altı değil, bu farkın doğrudan kanıtı.
Bu fark yalnız kayıt sayısında değil, işin niteliğinde de görünüyor. Bir kopyalama, kopyanın
kendi verisini ayrıca kurmayı gerektiriyor — Izin durumunda yeni bir String ayırmayı;
taşıma ise var olan veriye yeni bir sahip atamaktan ibaret, hiçbir yeni ayırma yapmıyor.
Zincirleme örnekte görülen dört el değiştirme de bu yüzden tek bir String üzerinde
gerçekleşti — dört ayrı kopya değil, aynı verinin dört ayrı sahibi.
Özet
- Bir değer üç yoldan taşınabilir — işleve, iç kapsama, yapı alanına — ve üçü de aynı sınıfa (taşındığı yerde) giriyor.
- Taşınan değer yeni sahibinde kullanılabiliyor; eski ad ise taşımadan sonra bir daha okunamıyor, çünkü artık geçerli bir değere işaret etmiyor.
- Eski adın kullanılamaz hâle gelmesi derleme zamanında sınanan bir kuraldır; çalışma zamanında hiçbir maliyeti yok.
- Taşıma bir kopyalama değildir: defterde her değer için tam olarak bir kayıt oluşuyor, aynı değer iki kez bitirilmeye çalışılmıyor.
- Taşıma zincirlenebilir — bir değer birden çok işlev sınırını art arda geçebilir — ama kaç kez el değiştirirse değiştirsin, sonunda yalnız bir bitiş noktası oluyor.
Sonraki Adım
Bu üç taşıma biçiminin ortak noktası, eski adın değeri kaybetmesiydi — sahiplik her seferinde tam olarak bir yerden bir yere gitti. Sıradaki soru bunun tam tersini araştırıyor: bir değere, sahipliği hiç vermeden erişmek mümkün mü? Ödünç Alma dersi, bir değeri ödünç veren adın değeri kaybetmediğini ve ödünç alınan değerin ödünç sırasında hiçbir yere bitmediğini ölçüyor — taşımanın aksine, burada eski sahip hâlâ sahip kalıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.