Ders 11 / 14
Referans Sayımı
Bir Rc değerinin sayacının her kopyada arttığı, her bitişte azaldığı ve değerin ancak sayaç sıfıra inince deftere düştüğü ölçülüyor; bir referans almanın bu sayacı hiç değiştirmediği ve hangi kopyanın bırakıldığının değil kaç kopyanın kaldığının belirleyici olduğu gösteriliyor.
Yayın Güncelleme
Bu derste
- Bir paylaşılan değerin sayacının her kopyada arttığını, her bitişte azaldığını koşturarak gösterir
- Sayacın sıfıra inmesiyle değerin deftere düşmesinin aynı ana denk geldiğini doğrular
- Bir referans almanın sayacı değiştirmediğini, yalnızca kopyalamanın değiştirdiğini örnekle ayırt eder
İçindekiler
Önceki ders, öbekteki bir değerin taşınsa da, erken bırakılsa da, zincirlense de hep tek bir sahibi olduğunu ölçtü. Bu dersin sorusu tam o varsayımı kaldırıyor: bir değerin sahibi birden çok olabilir mi? Yanıt evet — ama bu evet bir kaçamak değil, kaynaktan okunabilen bir sayaçla izlenen, belirlenimci bir paylaşımdır. Sayacın izlediği şey bir değerin bitiş noktasıdır; Eşzamanlılık kursunun (M09/K03) ölçtüğü koşan bir birimin durması bu kursun sorusu değil.
Rc<T>, bir değeri öbeğe koyup kaç bağlamanın o değere ortak eriştiğini sayan bir kaptır.
Değer, bu sayaç sıfıra düşünceye kadar yaşar; sayaç sıfıra indiği an, önceki derste görülen
tekil sahiplik biter ve değer defterine düşer. Bu ders o sayacı doğrudan okuyarak, her
kopyalamada ve her bitişte ne olduğunu, hangi bağlamanın “sahip” sayıldığını ve hangisinin
sayılmadığını ölçüyor.
Neden Bir Referans Yetmez
Ödünç Alma dersi, bir referansın ödünç aldığı değerden daha uzun yaşayamayacağını
ölçmüştü; Yaşam Süresi Kavramı dersi bu sınırı bir geçerlilik aralığı olarak adlandırdı.
Bu sınır, aynı değere birden çok yerden erişmek isteyen ama kaynaktaki tek sahiplik
bağlamasının ömrüyle sınırlı kalmak istemeyen bir kullanım için yetersiz kalır: referans,
sahibinin kapsamı kapanınca geçersiz olur, kaç yerden okunursa okunsun — sahibinin kendisi
hâlâ tektir. Rc, bu sınırı bir kopyanın kendisini de bir sahip yaparak aşıyor: artık
ödünç alan bir taraf değil, kendi başına bir sahip olan bir kopya söz konusu ve bu kopya
orijinal bağlamadan bağımsız olarak kendi kapsamının sonuna kadar yaşayabiliyor.
Sahiplik Sayılabilir Hâle Geliyor
Bir değer Rc::new ile öbeğe konduğunda sayaç bire kurulur. Rc::clone çağrıldığında yeni
bir kopya üretilir, sayaç bir artar; kopyalardan biri bittiğinde sayaç bir azalır. Bu
kopyalar birbirinin aynı değerine erişir — her biri kendi ayrı bir değer taşımaz,
hepsi tek bir Izyi paylaşır.
PS5. — “sayacın her kopyada arttığı ve her bitişte azaldığı” iddiası, aşağıdaki koşumun
1, 2, 3, 2 sırasıyla basılan sayaç değerleriyle doğrulanıyor.
// referans_sayaci.rs — Rc'nin sayaci her kopyada artiyor, her bitiste azaliyor
use std::cell::RefCell;
use std::rc::Rc;
type Defter = Rc<RefCell<Vec<String>>>;
struct Iz {
ad: String,
defter: Defter,
}
impl Iz {
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());
}
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(Vec::new()));
{
let paylasilan: Rc<Iz> = Rc::new(Iz::yeni("paylasilan", &defter));
println!("kurulunca sayac: {}", Rc::strong_count(&paylasilan));
let ikinci = Rc::clone(&paylasilan);
println!("bir kopyadan sonra sayac: {}", Rc::strong_count(&paylasilan));
{
let _ucuncu = Rc::clone(&paylasilan);
println!("iki kopyadan sonra sayac: {}", Rc::strong_count(&paylasilan));
println!("ic kapsam biterken defter uzunlugu: {}", defter.borrow().len());
}
println!("ic kapsam bittikten sonra sayac: {}", Rc::strong_count(&paylasilan));
println!("ikinci kopya hala okunabilir: {}", ikinci.ad);
}
let kayit = defter.borrow();
println!("defterdeki kayit sayisi: {}", kayit.len());
println!("kayitlar: {}", kayit.join(", "));
}
kurulunca sayac: 1 bir kopyadan sonra sayac: 2 iki kopyadan sonra sayac: 3 ic kapsam biterken defter uzunlugu: 0 ic kapsam bittikten sonra sayac: 2 ikinci kopya hala okunabilir: paylasilan defterdeki kayit sayisi: 1 kayitlar: paylasilan
İç kapsam kapanırken _ucuncu bitiyor, ama defter uzunluğu sıfır kalıyor: _ucuncunun
bitişi yalnızca sayacı ikiye indiriyor, Iz değerinin kendisini bitirmiyor. paylasilan
ve ikinci hâlâ aynı değere erişebiliyor — ikinci.ad okunduğunda hâlâ paylasilan
metnini veriyor, çünkü ikisi aynı değeri paylaşıyor. Dış kapsam kapandığında geri kalan iki
kopya da bitiyor, sayaç sıfıra iniyor ve ancak o an defterde tek bir kayıt beliriyor.
Üç kopyaya rağmen defterde tek kayıt olması, paylaşımın değeri çoğaltmadığını, yalnızca
ona erişen yol sayısını çoğalttığını gösteriyor.
Kopyalamak Veri Kopyalamak Değildir
Yukarıdaki koşumda Iz::yeni yalnızca bir kez çağrılıyor — paylasilan kurulurken.
ikinci ve _ucuncu, Rc::clone(&paylasilan) ile üretiliyor; bu çağrı ne Iz::yeniyi
yeniden çalıştırıyor ne de ad alanını yeniden kuruyor. ikinci.adin hâlâ paylasilan
metnini vermesi bir rastlantı değil: ikinci ayrı bir Iz değeri taşımıyor, paylasilan
ile aynı değere bakan ikinci bir erişim yolu taşıyor. Bu, bir dizginin ya da bir
listenin .clone() çağrısından farklıdır — onlarda içerik gerçekten kopyalanır ve
sonradan biri değiştirilse öbürü değişmez. Rc::clone, adındaki fiile rağmen, taşıdığı
değeri değil yalnızca sayacı ve erişimi çoğaltıyor. Bu fark, ilerleyen bir derste
paylaşılan verinin değiştirilmesi gündeme geldiğinde önemli sonuçlar doğuruyor: iki
kopyadan biri üzerinden yapılan bir değişiklik, öbür kopyadan da görülebilir hâle gelir,
çünkü aslında değişen tek bir değerdir.
Her Değerin Kendi Sayacı Vardır
Bir programda birden çok paylaşılan değer varsa, her birinin sayacı ayrıdır. Bir değerin kopyalanması ya da bitmesi, başka bir değerin sayacına dokunmaz.
// bagimsiz_sayaclar.rs — farkli degerlerin sayaclari birbirinden bagimsizdir
use std::cell::RefCell;
use std::rc::Rc;
type Defter = Rc<RefCell<Vec<String>>>;
struct Iz {
ad: String,
defter: Defter,
}
impl Iz {
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());
}
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(Vec::new()));
let birinci: Rc<Iz> = Rc::new(Iz::yeni("birinci-deger", &defter));
let ikinci: Rc<Iz> = Rc::new(Iz::yeni("ikinci-deger", &defter));
let birinci_kopya = Rc::clone(&birinci);
println!("birinci sayac: {}", Rc::strong_count(&birinci));
println!("ikinci sayac: {}", Rc::strong_count(&ikinci));
drop(birinci_kopya);
drop(birinci);
println!("defter (yalniz birinci bitti): {}", defter.borrow().join(", "));
drop(ikinci);
println!("defter (ikisi de bitti): {}", defter.borrow().join(", "));
}
birinci sayac: 2 ikinci sayac: 1 defter (yalniz birinci bitti): birinci-deger defter (ikisi de bitti): birinci-deger, ikinci-deger
birinci bir kez kopyalandığı için sayacı iki, ikinci hiç kopyalanmadığı için sayacı
bir. birincinin iki kopyası da bittiğinde ikincinin sayacı hâlâ bir olarak kalıyor —
ikinci bu değişiklikten habersiz. Aynı defter her iki değer için de ortak kullanılıyor
(bu bir kayıt aracı, sayılan değerin kendisi değil), ama her Iznin kendi Rc sarmalayıcısı
kendi sayacını tutuyor. Bir programda kaç ayrı paylaşılan değer varsa, o kadar ayrı ve
birbirinden bağımsız sayaç vardır.
Paylaşım Sahiplik Kuralını İhlal Etmiyor, Genelliyor
Önceki derste kurulan “tek sahip” kuralı burada bozulmuyor, genelleniyor: sahip artık tek
bir bağlama değil, sayaç sıfıra düşene kadar aktif kalan kopyaların kümesidir. Sayaç
sıfıra indiği an kesin olarak bellidir ve bu an, koşumda her zaman aynı sırayla üretilen
bir kayıttır — bir sonraki adımın ne yapacağına bağlı bir tahmin değil, kaynaktaki kopya
sayısının doğrudan sonucudur. Bu, tek iş parçacığında çalışan sıradan bir tam sayı gibi
davranır: Rc::clone çağrısı bir kopya üretip sayacı artırıyor, kopyanın bitişi sayacı
azaltıyor ve iki işlem de aynı yerden — aynı kapsamdan — yürüyor.
Sayaç, tek tek adlandırılmış bağlamalarla sınırlı değildir; bir döngüde üretilen kopyalar da aynı kurala uyar. Aşağıdaki koşum beş kopyayı bir listeye topluyor, sonra listeyi tek seferde boşaltıyor.
// toplu_kopyalama.rs — bir donguyle uretilen kopyalar temizlenince sayac baslangica doner
use std::rc::Rc;
fn main() {
let paylasilan: Rc<i32> = Rc::new(7);
let mut kopyalar: Vec<Rc<i32>> = Vec::new();
for _ in 0..5 {
kopyalar.push(Rc::clone(&paylasilan));
}
println!("bes kopyadan sonra sayac: {}", Rc::strong_count(&paylasilan));
kopyalar.clear();
println!("kopyalar temizlenince sayac: {}", Rc::strong_count(&paylasilan));
}
bes kopyadan sonra sayac: 6 kopyalar temizlenince sayac: 1
Beş kopya listeye eklenince sayaç altıya çıkıyor — ilk kurulan paylasilan da bir kopya
sayılıyor. Liste tek seferde boşaltıldığında beş kopya art arda bitiyor ve sayaç bire
dönüyor; paylasilan kendisi hâlâ elde olduğu için değer henüz bitmiyor. Bu örnek Iz
düzeneğini kullanmıyor, çünkü burada ölçülen şey bir bitiş kaydı değil, sayacın kendisinin
toplu işlemler altında da aynı kuralı izlediği.
Sayılan Şey Sahiptir, Ödünç Değil
Sayaç, yalnızca Rc::clone çağrılarını sayar. Paylaşılan değere bir referans almak —
onu okumak için ödünç almak — sayacı hiç değiştirmez, çünkü ödünç alan taraf değerin
sahibi değildir, yalnızca ona geçici olarak erişir.
PS6. — “bir referans almanın sayacı değiştirmediği” iddiası, aşağıdaki koşumda
kurulunca sayac ile odunc alip okuduktan sonra sayac satırlarının aynı değeri
(1) vermesiyle doğrulanıyor.
// odunc_sayaca_girmez.rs — bir referans almak Rc sayacini degistirmiyor
use std::cell::RefCell;
use std::rc::Rc;
type Defter = Rc<RefCell<Vec<String>>>;
struct Iz {
ad: String,
defter: Defter,
}
impl Iz {
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());
}
}
fn oku(deger: &Iz) -> usize {
deger.ad.len()
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(Vec::new()));
let paylasilan: Rc<Iz> = Rc::new(Iz::yeni("olcum-degeri", &defter));
println!("kurulunca sayac: {}", Rc::strong_count(&paylasilan));
let uzunluk = oku(&paylasilan);
println!(
"odunc alip okuduktan sonra sayac: {} (uzunluk {})",
Rc::strong_count(&paylasilan),
uzunluk
);
let kopya = Rc::clone(&paylasilan);
println!("kopyaladiktan sonra sayac: {}", Rc::strong_count(&paylasilan));
drop(kopya);
println!("kopyayi birakinca sayac: {}", Rc::strong_count(&paylasilan));
}
kurulunca sayac: 1 odunc alip okuduktan sonra sayac: 1 (uzunluk 12) kopyaladiktan sonra sayac: 2 kopyayi birakinca sayac: 1
oku işlevi &Iz alıyor, Rc<Iz>nin kendisini değil — bu yüzden çağrı sayacı hiç
etkilemiyor. Sayaç yalnızca Rc::clone çağrısıyla ikiye çıkıyor ve kopya açıkça
bırakıldığında yeniden bire dönüyor. Ödünç Alma dersinde kurulan “ödünç alınan değer
bitmiyor” ilkesi burada da geçerli: bir referans, değerin sahiplik defterine hiç
girmiyor; yalnızca kopyalar sahip sayılıyor, ödünç alanlar sayılmıyor.
Bu ayrım küçük görünse de yanlış anlaşılmaya açıktır, çünkü Rc<Iz> üzerinden bir
işleve &paylasilan geçirmek ile Rc::clone(&paylasilan) geçirmek yüzeysel olarak
benzer görünür — ikisi de bir & işareti taşıyabilir. Aradaki fark şudur: oku(&paylasilan)
çağrısı Rc<Iz>yi ödünç alıp içindeki &Izye otomatik olarak iniyor, hiçbir yeni sahip
kurmuyor; Rc::clone(&paylasilan) ise açıkça yeni bir sahip kopyası üretiyor. Kod
okunurken bu iki niyeti ayırt etmenin yolu, çağrının clone adını taşıyıp taşımadığına
bakmaktır — sayaç yalnızca o adı taşıyan çağrıya tepki verir.
Sıra Değil, Sayı Belirleyici
Önceki derste bir kabın içindekilerin bitiş sırası ölçülmüştü — hangi değerin önce, hangisinin sonra bittiği belirleyiciydi. Referans sayımında durum farklıdır: hangi kopyanın hangi sırayla bırakıldığı değeri bitirmez, yalnızca kaç kopyanın kaldığı belirler.
PS7. — “değerin, hangi kopyanın değil son kalan kopyanın bırakılmasıyla bittiği” iddiası, aşağıdaki koşumda dört kopyadan ilk kurulanın ortadan bırakılmasına rağmen defterin boş kalmasıyla, yalnızca son kopya giderken dolmasıyla doğrulanıyor.
// sira_onemsiz.rs — Rc'de hangi kopyanin birakildigi degil, kac kopya kaldigi belirleyici
use std::cell::RefCell;
use std::rc::Rc;
type Defter = Rc<RefCell<Vec<String>>>;
struct Iz {
ad: String,
defter: Defter,
}
impl Iz {
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());
}
}
fn main() {
let defter: Defter = Rc::new(RefCell::new(Vec::new()));
let paylasilan: Rc<Iz> = Rc::new(Iz::yeni("son-kopya-birakilinca-biten", &defter));
let a = Rc::clone(&paylasilan);
let b = Rc::clone(&paylasilan);
let c = Rc::clone(&paylasilan);
println!("dort kopyadan sonra sayac: {}", Rc::strong_count(&a));
drop(b);
println!("ortadaki kopya birakilinca sayac: {}", Rc::strong_count(&a));
println!("defter hala bos mu: {}", defter.borrow().is_empty());
drop(paylasilan);
println!("ilk kurulan kopya birakilinca sayac: {}", Rc::strong_count(&a));
println!("defter hala bos mu: {}", defter.borrow().is_empty());
drop(a);
drop(c);
let kayit = defter.borrow();
println!("son kopya da birakilinca kayit: {}", kayit.join(", "));
}
dort kopyadan sonra sayac: 4 ortadaki kopya birakilinca sayac: 3 defter hala bos mu: true ilk kurulan kopya birakilinca sayac: 2 defter hala bos mu: true son kopya da birakilinca kayit: son-kopya-birakilinca-biten
Dört kopyadan b bırakılıyor, sonra ilk kurulan paylasilan bırakılıyor — kuruluş
sırasına göre ne ilk ne son olan bir kopya, sonra da en eski kopyanın kendisi gidiyor. İki
bırakmadan sonra bile defter boş kalıyor, çünkü geriye hâlâ iki kopya (a ve c) var.
Değer ancak bu ikisi de bırakıldığında biter. Bir önceki derste bir kabın içindekilerin
bitişi hangi sırayla eklendiğine bağlıydı; burada bitiş yalnızca kaç kopyanın
kaldığına bağlı — hangi kopyanın adı, hangi sırayla bırakıldığı sonucu değiştirmiyor. Bu
fark, iki kap türü arasındaki temel ayrımı özetliyor: bir liste ya da bir Box, tek bir
sahiplik zincirinin konumunu kaydeder ve bu yüzden sıraya duyarlıdır; bir Rc,
sahiplik zincirinin uzunluğunu kaydeder ve bu yüzden yalnızca sayıya duyarlıdır.
Yukarıdaki koşumlarda kullanılan açık drop çağrısı, Öbekte Tekil Sahiplik dersinde
görülen açık çağrı fikrini burada da sürdürüyor, ama sonucu farklıdır. Orada bir Box
üzerinde açık çağrı değerin kendisini erkenden bitiriyordu — tek sahip olduğu için başka
seçenek yoktu. Burada aynı çağrı yalnızca bir kopyayı bırakıyor; değerin kendisi,
geriye başka kopya kaldığı sürece bitmiyor. Aynı drop işlevi, tek sahiplikte kesin bir
bitiş, paylaşımlı sahiplikte ise koşullu bir azaltma anlamına geliyor — anlam, çağrıldığı
değerin sahiplik biçimine göre değişiyor.
Dört koşum bir araya geldiğinde, referans sayımının sahiplik kuralını nasıl genellediği netleşiyor. Sayaç her zaman tek bir yerden, tek bir sırayla güncelleniyor; kopyalamak veriyi değil erişimi çoğaltıyor; bir referans hiçbir zaman bu sayıma girmiyor; ve bitişi belirleyen şey kopyaların kimliği değil miktarıdır. Bu dört özellik birlikte, hâlâ belirlenimci ve kaynaktan okunabilir bir bitiş noktası sağlıyor — sayaç sıfıra indiği an, tıpkı önceki derste tek sahipli bir değerin kapsam sonu gibi, koşumda kesin ve tekrarlanır biçimde görülüyor.
Özet
Rc<T>, bir değeri öbeğe koyup ona erişen kopya sayısını sayan bir kaptır.- Sayaç her
Rc::cloneçağrısında artar, her kopyanın bitişinde azalır. - Değer, ancak sayaç sıfıra indiğinde defterine düşer — ömrü kabıyla birlikte, son kalan kopyanın bitişinde biter; bu an kaynaktan okunabilir ve koşumda her zaman aynı sırayla üretilir.
- Bir referans almak sayacı değiştirmez; yalnızca kopyalamak değiştirir.
- Değerin bitişi hangi kopyanın bırakıldığına değil, kaç kopyanın kaldığına bağlıdır.
Rc::clonetaşınan değeri değil, sayacı ve erişimi çoğaltır; kopyalar aynı değere bakar.
Sonraki Adım
Bu derste ölçülen sayaç, tek bir bağlamda, sıradan bir tam sayı gibi güncellendi: her kopyalama ve her bitiş aynı kapsamdan, aynı sırayla yürüdü. Sıradaki ders aynı sayma işini yapan ama sayacın kendisini farklı bir biçimde güncelleyen bir türü ölçüyor — davranış aynı kalıyor, yalnızca sayacın güncellenme biçimi değişiyor; hangi bağlamda bu farkın gerektiği, bu dersin kapsamının dışında kalıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.