Yığın Üzerindeki Verilere İşaret Etmek için Box<T>
Kullanma
En basit akıllı işaretçi, türü Box<T>
olarak yazılan bir çeşit kutudur.
Kutular, verileri yığıt yerine yığın (heap) üzerinde saklamanıza olanak tanır.
Yığıtta kalan şey, yığın verisinin işaretçisidir. Yığıt ve yığın arasındaki farkı incelemek için Bölüm 4'e bakın.
Kutular, verilerini yığıt yerine yığın üzerinde saklamak dışında performans ek yüküne sahip değildir. Ancak çok fazla ekstra yetenekleri de yoktur. Onları en çok bu durumlarda kullanacaksınız:
- Boyutu derleme zamanında bilinemeyen bir türünüz olduğunda ve bu türden bir değeri tam boyut gerektiren bir bağlamda kullanmak istediğinizde
- Büyük miktarda veriye sahip olduğunuzda ve sahipliği devretmek istediğinizde ancak bunu yaparken verilerin kopyalanmayacağından emin olmak istediğinizde
- Bir değere sahip olmak istediğinizde ve belirli bir türde olmasından ziyade yalnızca belirli bir tanımı sürekleyen bir tür olmasını önemsediğinizde
İlk durumu “Kutularla Özyinelemeli Türleri Etkinleştirme” bölümünde göstereceğiz. İkinci durumda, büyük miktarda verinin sahipliğinin aktarılması uzun sürebilir çünkü veriler yığıt üzerinde kopyalanır. Bu durumda performansı artırmak için, büyük miktardaki veriyi yığında bir kutu içinde saklayabiliriz. Ardından, yalnızca küçük miktarda işaretçi verisi yığıt üzerinde kopyalanırken, referans verdiği veriler yığın üzerinde tek bir yerde kalır. Üçüncü durum özellik nesnesi olarak bilinir ve Bölüm 17, “Farklı Türlerde Değerlere İzin Veren Tanım Nesnelerini Kullanma” başlıklı bir bölümün tamamını bu konuya ayırmıştır. Yani burada öğrendiklerinizi Bölüm 17'de tekrar uygulayacaksınız!
Verileri Yığın Üzerinde Saklamak için Box<T>
Kullanma
Box<T>
için yığın depolama kullanım durumunu tartışmadan önce,
söz dizimini ve bir Box<T>
içinde depolanan değerlerle nasıl etkileşimde bulunacağımızı ele alacağız.
Liste 15-1, bir i32
değerini yığın üzerinde saklamak için bir kutunun nasıl kullanılacağını gösterir:
Dosya adı: src/main.rs
fn main() { let b = Box::new(5); println!("b = {}", b); }
b
değişkenini, yığın üzerinde ayrılmış olan 5
değerine işaret eden bir Box
değerine sahip
olacak şekilde tanımlarız. Bu program b = 5
yazdıracaktır; bu durumda, kutudaki verilere,
bu veriler yığıtta olsaydı yapacağımız gibi erişebiliriz. Tıpkı sahip olunan herhangi bir değer gibi,
main
'in sonunda b
'nin yaptığı gibi bir kutu kapsam dışına çıktığında, bellekten silinecektir.
Bellekten silme işlemi hem kutu (yığıt üzerinde saklanır) hem de işaret ettiği veri
(yığın üzerinde saklanır) için gerçekleşir.
Yığına tek bir değer koymak çok kullanışlı değildir, bu nedenle kutuları bu şekilde tek başlarına çok sık
kullanmazsınız. Varsayılan olarak saklandıkları yığıtta tek bir i32
gibi değerlere sahip olmak,
çoğu durumda daha uygundur. Kutuların, kutular olmasaydı tanımlamamıza izin verilmeyecek türleri
tanımlamamıza izin verdiği bir duruma bakalım.
Kutularla Özyinelemeli Türleri Etkinleştirme
Özyinelemeli türdeki bir değer, kendisinin bir parçası olarak aynı türde başka bir değere sahip olabilir. Özyinelemeli türler bir sorun teşkil eder çünkü derleme zamanında Rust'ın bir türün ne kadar yer kapladığını bilmesi gerekir. Ancak, özyinelemeli türlerin değerlerinin iç içe geçmesi teorik olarak sonsuza kadar devam edebilir, bu nedenle Rust değerin ne kadar alana ihtiyaç duyduğunu bilemez. Kutular bilinen bir boyuta sahip olduğundan, özyinelemeli tür tanımına bir kutu ekleyerek özyinelemeli türleri etkinleştirebiliriz.
Bir özyinelemeli tür örneği olarak, cons listesini inceleyelim. Bu, fonksiyonel programlama dillerinde yaygın olarak bulunan bir veri türüdür. Tanımlayacağımız cons listesi türü, özyineleme dışında basittir; bu nedenle, üzerinde çalışacağımız örnekteki kavramlar, özyinelemeli türleri içeren daha karmaşık durumlarla karşılaştığınızda yararlı olacaktır.
Cons Listesi Hakkında Daha Fazla Bilgi
Cons listesi, Lisp programlama dili ve lehçelerinden gelen ve iç içe geçmiş çiftlerden oluşan bir
veri yapısıdır. Adı, Lisp'te iki argümanından yeni bir çift oluşturan cons
fonksiyonundan (“construct function”'ın kısaltması)
gelir. Bir değer ve başka bir çiftten oluşan bir çift üzerinde cons
çağırarak,
özyinelemeli çiftlerden oluşan cons listeleri oluşturabiliriz.
Örneğin, burada 1, 2, 3 listesini içeren ve her bir çifti parantez içinde olan bir cons listesinin sözde kod gösterimi yer almaktadır:
(1, (2, (3, Nil)))
Bir cons listesindeki her öğe iki öğe içerir: geçerli öğenin değeri ve bir sonraki öğe.
Listedeki son öğe, bir sonraki öğe olmaksızın yalnızca Nil
adlı bir değer içerir.
Bir cons listesi, cons
fonksiyonunun özyinelemeli olarak çağrılmasıyla oluşturulur.
Özyinelemenin temel durumunu gösteren yaygın kullanılan ad Nil
'dir.
Bunun Bölüm 6'daki “null” or “nil” kavramıyla aynı olmadığına dikkat edin; bu kavram geçersiz veya
olmayan bir değerdir.
Cons listesi Rust'ta yaygın olarak kullanılan bir veri yapısı değildir.
Rust'ta bir öğe listesine sahip olduğunuzda çoğu zaman Vec<T>
kullanmak daha iyi bir seçimdir.
Diğer, daha karmaşık özyinelemeli veri tipleri çeşitli durumlarda kullanışlıdır,
ancak bu bölümde cons listesi ile başlayarak, kutuların fazla dikkat dağıtmadan özyinelemeli
bir veri tipi tanımlamamıza nasıl izin verdiğini keşfedebiliriz.
Liste 15-2, cons listesi için bir enum
tanımını içerir.
Bu kodun henüz derlenmeyeceğini unutmayın çünkü List
türünün bilinen bir boyutu yoktur,
bunu daha sonra göstereceğiz.
Dosya adı: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
Not: Bu örneğin amaçları doğrultusunda yalnızca
i32
değerlerini tutan bir cons listesi yapıyoruz. Bölüm 10'da tartıştığımız gibi, herhangi bir türden değerleri saklayabilen bir cons liste türü tanımlamak için yaygınları kullanabilirdik.
List
türünü 1, 2, 3
listesini saklamak için kullanırsak, Liste 15-3'teki gibi bir kod yazmamız gerekir:
Dosya adı: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
İlk Cons
değeri 1
'i ve başka bir `` değerini tutar.
Bu List
değeri, 2
ve başka bir List
değerini tutan başka bir Cons
değeridir.
Bu List
değeri, 3
'ü ve bir diğer List
değerini tutan bir Cons
değeridir ve son
olarak listenin sonunu işaret eden özyinelemesiz tür olan Nil
'dir.
Liste 15-3'teki kodu derlemeye çalışırsak, Liste 15-4'te gösterilen hatayı alırız:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing drop-check constraints for `List`
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing drop-check constraints for `List` again
= note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, constness: NotConst }, value: List } }`
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to 2 previous errors
Hata, bu türün “sonsuz boyutu” olduğunu gösterir. Bunun nedeni, List
'i özyinelemeli bir değişkenle
tanımlamış olmamızdır: doğrudan kendisinin başka bir değerini tutar. Sonuç olarak, Rust
List
değerini saklamak için ne kadar alana ihtiyacı olduğunu bulamaz. Bu hatayı neden aldığımızı inceleyelim.
İlk olarak, Rust'ın özyinelemeli olmayan bir türdeki bir değeri saklamak için ne kadar alana ihtiyaç
duyduğuna nasıl karar verdiğine bakacağız.
Özyinelemeli Olmayan Bir Türün Boyutunu Hesaplama
Bölüm 6'da enum
tanımlarını tartışırken Liste 6-2'de tanımladığımız Message
enum
'unu hatırlayın:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
Bir Message
değeri için ne kadar alan ayrılacağını belirlemek için Rust, hangi varyantın en fazla alana ihtiyaç
duyduğunu görmek için varyantların her birini gözden geçirir. Rust, Message::Quit
'in herhangi bir alana ihtiyaç
duymadığını, Message::Move
'un iki i32
değerini depolamak için yeterli alana ihtiyaç duyduğunu ve benzerlerini görür.
Yalnızca bir değişken kullanılacağından, bir Message
değerinin ihtiyaç duyacağı en fazla alan, değişkenlerinin en
büyüğünü depolamak için gereken alandır.
Rust, Liste 15-2'deki List
enum
'u gibi özyinelemeli bir türün ne kadar alana ihtiyaç duyduğunu belirlemeye
çalıştığında ortaya çıkan durumla bunu karşılaştırın. Derleyici, i32
türünde bir değer ve List
türünde bir değer tutan
Cons
değişkenine bakarak başlar. Bu nedenle Cons
, i32
'nin boyutu artı bir List
'in boyutuna eşit
miktarda alana ihtiyaç duyar. Derleyici, List
türünün ne kadar belleğe ihtiyacı olduğunu bulmak için Cons
değişkeninden başlayarak tüm sıralı değişkenlere bakar. Cons
değişkeni i32
türünde bir değer ve
List
türünde bir değer tutar ve bu işlem Şekil 15-1'de gösterildiği gibi sonsuza kadar devam eder.
Boyutu Bilinen Özyinelemeli Bir Tür Elde Etmek için Box<T>
Kullanmak
Rust, özyinelemeli olarak tanımlanan türler için ne kadar alan ayırması gerektiğini bulamadığından, derleyici bu yararlı öneriyle birlikte bir hata verir:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ^^^^ ^
Bu öneride “dolayı yönlendirme” (“indirection”), bir değeri doğrudan depolamak yerine, veri yapısını değiştirerek değeri dolaylı olarak depolamamız gerektiği ve bunun yerine değere bir işaretçi depolamamız gerektiği anlamına gelir.
Box<T>
bir işaretçi olduğundan, Rust her zaman bir Box<T>
'nin ne kadar alana ihtiyacı olduğunu bilir:
bir işaretçinin boyutu, işaret ettiği veri miktarına bağlı olarak değişmez. Bu, doğrudan başka bir
List
değeri yerine Cons
değişkeninin içine bir Box<T>
koyabileceğimiz anlamına gelir.
Box<T>
, Cons
değişkeninin içinde olmak yerine yığın üzerinde olacak bir sonraki List
değerine
işaret edecektir. Kavramsal olarak, hala diğer listeleri tutan listelerle oluşturulmuş bir listemiz var,
ancak bu uygulama artık öğeleri birbirinin içine yerleştirmek yerine yan yana yerleştirmeye benziyor.
Liste 15-2'deki List
enum
'unun tanımını ve Liste 15-3'teki List
kullanımını, derlenecek olan
Liste 15-5'teki kodla değiştirebiliriz:
Dosya adı: src/main.rs
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
Cons
varyantı, bir i32
boyutuna ve kutunun işaretçi verilerini depolamak için alana ihtiyaç duyar.
Nil
değişkeni hiçbir değer saklamaz, bu nedenle Cons
değişkeninden daha az alana ihtiyaç duyar.
Artık herhangi bir List
değerinin bir i32
boyutu artı bir kutunun işaretçi verisinin boyutunu kaplayacağını biliyoruz.
Bir kutu kullanarak sonsuz, özyinelemeli zinciri kırdık, böylece derleyici bir List
değerini saklamak için gereken
boyutu bulabilir. Şekil 15-2, Cons
varyantının şimdi nasıl göründüğünü göstermektedir.
Kutular yalnızca dolaylama ve heap tahsisi sağlar; diğer akıllı işaretçi türlerinde göreceğimiz gibi başka özel yetenekleri yoktur. Ayrıca, bu özel yeteneklerin neden olduğu performans ek yüküne de sahip değildirler, bu nedenle dolaylamanın ihtiyaç duyduğumuz tek özellik olduğu cons listesi gibi durumlarda yararlı olabilirler. Bölüm 17'de kutular için daha fazla kullanım alanına da bakacağız.
Box<T>
türü akıllı bir işaretçidir çünkü Box<T>
değerlerinin referanslar gibi ele alınmasını sağlayan
Deref
tanımını sürekler. Box<T>
değeri kapsam dışına çıktığında, Drop
tanımının süreklenmesi nedeniyle
kutunun işaret ettiği yığın verileri de temizlenir. Bu iki tanım, bu bölümün geri kalanında tartışacağımız diğer
akıllı işaretçi türleri tarafından sağlanan işlevsellik için daha da önemli olacaktır.
Şimdi bu iki tanımı daha ayrıntılı olarak inceleyelim.