Box<T>, Rust'taki en basit akıllı işaretçidir: bir değeri stack yerine heap'e koyar, stack'te yalnızca o değeri gösteren işaretçi kalır. Kutu değerin tek sahibidir; kutu kapsam dışına çıktığında hem işaretçi hem de heap'teki veri otomatik olarak serbest bırakılır. Heap'e koymanın dışında ek bir yeteneği ya da ek bir çalışma zamanı maliyeti yoktur. Bu sadelik onu üç durumda vazgeçilmez kılar: boyutu derleme zamanında bilinemeyen tipler (özyinelemeli tipler gibi), kopyalanmadan sahipliği devredilmesi istenen büyük veriler ve somut tipi yerine bir trait'i uygulamasıyla ilgilendiğiniz değerler.

Bu yazıda Box<T> ile değer oluşturmayı ve içindekine erişmeyi, özyinelemeli bir liste tanımlamayı, Box<dyn Trait> kullanımını, akıllı işaretçileri mümkün kılan Deref ve Drop trait'lerini, deref coercion kurallarını ve Box::leak gibi daha az bilinen fonksiyonları denenmiş örneklerle anlatıyoruz. Stack ve heap ayrımını hatırlamak için ownership yazısına göz atabilirsiniz. Çoklu sahiplik gerektiğinde Box yetmez; o konu Rc ve RefCell yazısının alanıdır.

Box ile değer oluşturmak ve erişmek

Box ile değer oluşturmak ve erişmek

Box::new(deger) heap'te yer ayırır ve değeri oraya taşır. Std belgesi sıfır boyutlu tipler için gerçek bir ayırma yapılmadığını da not eder. Kutudaki değere çoğu zaman sanki doğrudan değerin kendisiymiş gibi erişirsiniz: println! ile yazdırabilir, metotlarını çağırabilirsiniz. Değerin kendisine ihtiyaç duyduğunuzda * operatörüyle referansı izlersiniz. *m bir String değerini kutudan dışarı taşır; bu satırdan sonra m artık kullanılamaz, çünkü içeriğin sahipliği icerik değişkenine geçmiştir.

rust
use std::mem::size_of;

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
    let toplam = *b + 1;
    println!("toplam = {toplam}");

    let mut m = Box::new(String::from("Merhaba"));
    m.push_str(", Rust");
    println!("{m} ({} bayt)", m.len());

    let icerik: String = *m;
    println!("{icerik}");

    println!("{}", size_of::<Box<[u8; 1024]>>());
    println!("{}", size_of::<[u8; 1024]>());
    println!("{}", size_of::<Box<[u8]>>());
}
// çıktı: b = 5
// çıktı: toplam = 6
// çıktı: Merhaba, Rust (13 bayt)
// çıktı: Merhaba, Rust
// çıktı: 8
// çıktı: 1024
// çıktı: 16

Son üç satır bellek boyutlarını gösteriyor. Bu çıktı 64 bit bir hedefte alındı; işaretçi boyutu platforma bağlıdır. 1024 baytlık bir diziyi kutuya koyduğunuzda stack'te yalnızca tek bir işaretçi kalır, dizinin kendisi heap'tedir. Böyle bir değeri bir fonksiyona taşıdığınızda kopyalanan şey işaretçidir, veri yerinde durur; kitabın ikinci kullanım durumu tam olarak budur. Box<[u8]> ise iki kat büyüktür; Reference, boyutu dinamik tiplere giden işaretçilerin uzunluk gibi ek bir meta veri taşıdığı için normal işaretçinin iki katı boyutta olduğunu belirtir. Std belgesi, T boyutu bilinen bir tip olduğu sürece Box<T> türünün tek bir işaretçi olarak temsil edildiğini ve C tarafındaki T* ile ABI uyumlu olduğunu garanti eder.

Özyinelemeli tipler: Box neden şart?

Rust her tipin bellekte ne kadar yer kaplayacağını derleme zamanında bilmek ister. Bir enum için bu boyut, en büyük varyantın boyutudur. Bir varyant kendi tipinden bir değeri doğrudan içeriyorsa hesap sonsuza gider: Dugum bir i32 ve bir Liste içerir, o Liste yine bir Dugum olabilir ve bu böyle sürer. Derleyici bu durumda tipin sonsuz boyutlu olduğunu söyler ve araya bir dolaylılık eklemenizi önerir.

rust
enum Liste {
    Dugum(i32, Liste),
    Bos,
}

fn main() {}
// error[E0072]: recursive type `Liste` has infinite size
// help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle

Dolaylılık, değeri doğrudan saklamak yerine ona işaret eden bir işaretçi saklamak demektir. İşaretçinin boyutu gösterdiği verinin ne kadar büyük olduğuna bağlı değildir. Box<Liste> koyduğunuzda her Dugum bir i32 artı bir işaretçi kadar yer kaplar ve hesap sonlu hâle gelir. Kavramsal olarak liste hâlâ iç içe listelerden oluşur, ama bellekte elemanlar birbirinin içinde değil yan yana dizilir. Kitap bu yapıya Lisp kökenli adıyla cons list der ve gerçek kodda liste için çoğunlukla Vec<T> türünün daha iyi bir seçim olduğunu ekler; örnek, özyinelemeli tiplerin mantığını göstermek içindir.

rust
#[derive(Debug)]
enum Liste {
    Dugum(i32, Box<Liste>),
    Bos,
}

use Liste::{Bos, Dugum};

fn toplam(l: &Liste) -> i32 {
    match l {
        Dugum(deger, sonraki) => deger + toplam(sonraki),
        Bos => 0,
    }
}

fn main() {
    let l = Dugum(1, Box::new(Dugum(2, Box::new(Dugum(3, Box::new(Bos))))));
    println!("{l:?}");
    println!("toplam = {}", toplam(&l));
}
// çıktı: Dugum(1, Dugum(2, Dugum(3, Bos)))
// çıktı: toplam = 6

toplam fonksiyonu &Liste alır ve match ile varyantları ayırır. sonraki değişkeninin tipi &Box<Liste> olduğu hâlde onu doğrudan toplam(sonraki) şeklinde verebildik; bu, aşağıda anlatacağımız deref coercion sayesinde olur. Ağaçlar, sözdizimi ağaçları ya da ifade değerlendiriciler gibi yapılarda aynı desen tekrar tekrar karşınıza çıkar. Derleyicinin önerisindeki diğer iki seçenek, Rc ve &, sırasıyla paylaşılan sahiplik ve ödünç alma ihtiyacı olduğunda tercih edilir; referans seçerseniz lifetime parametreleri de gerekir.

Box&lt;dyn Trait&gt; ile farklı tipleri bir arada tutmak

Box<dyn Trait> ile farklı tipleri bir arada tutmak

Üçüncü kullanım durumu trait object'tir. Bir vektöre hem Kare hem Daire koymak istiyorsanız, iki tipin boyutu ve düzeni farklı olduğu için onları doğrudan aynı vektöre koyamazsınız. Her birini kutuya alıp Box<dyn Sekil> olarak saklarsanız vektörün elemanları aynı boyutta işaretçiler olur ve metot çağrıları çalışma zamanında doğru uygulamaya yönlendirilir. Bu dinamik dağıtımın kuralları, hangi trait'lerin dyn ile kullanılabildiği ve generic koddan farkı trait object yazısında ayrıntılı olarak anlatılıyor. Aynı fikir, birbirinden farklı closure'ları tek bir listede tutmak için de kullanılır.

rust
trait Sekil {
    fn alan(&self) -> f64;
}

struct Kare(f64);
struct Daire(f64);

impl Sekil for Kare {
    fn alan(&self) -> f64 {
        self.0 * self.0
    }
}

impl Sekil for Daire {
    fn alan(&self) -> f64 {
        3.14 * self.0 * self.0
    }
}

fn main() {
    let sekiller: Vec<Box<dyn Sekil>> = vec![Box::new(Kare(2.0)), Box::new(Daire(1.0))];
    for s in &sekiller {
        println!("{:.2}", s.alan());
    }
}
// çıktı: 4.00
// çıktı: 3.14

Deref: kutuyu referans gibi kullanmak

Bir tipi akıllı işaretçi yapan iki trait'ten ilki Deref'tir. Bu trait * operatörünün davranışını özelleştirir. Kitap bunu kendi kutu tipimizi yazarak gösterir; aşağıdaki Kutum veriyi heap'e koymaz, yalnızca işaretçi davranışını taklit eder. Deref uygulanmadan *y yazarsanız derleyici tipin dereference edilemeyeceğini söyler. deref metodu iç değere bir referans döndürür. *y yazdığınızda Rust arka planda *(y.deref()) çalıştırır. Metodun değerin kendisini değil referansını döndürmesi bilinçli bir tercihtir: değer döndürseydi, her dereference iç değeri kutudan dışarı taşırdı.

rust
use std::ops::Deref;

struct Kutum<T>(T);

impl<T> Kutum<T> {
    fn new(x: T) -> Kutum<T> {
        Kutum(x)
    }
}

impl<T> Deref for Kutum<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn selamla(ad: &str) {
    println!("Merhaba, {ad}!");
}

fn main() {
    let x = 5;
    let y = Kutum::new(x);
    println!("{}", *y == 5);

    let k = Kutum::new(String::from("Rust"));
    selamla(&k);
    println!("uzunluk: {}", k.len());
}
// çıktı: true
// çıktı: Merhaba, Rust!
// çıktı: uzunluk: 4

Örneğin asıl ilginç kısmı selamla(&k) çağrısıdır. selamla bir &str bekliyor, biz ise &Kutum<String> verdik. Derleyici önce Kutum üzerindeki deref ile &String elde eder, sonra String türünün kendi Deref uygulamasıyla &str türüne ulaşır. Buna deref coercion denir ve fonksiyon ile metot argümanlarında otomatik uygulanır. Kaç kez deref çağrılacağı derleme zamanında çözülür, çalışma zamanına bir maliyeti yoktur. k.len() çağrısının çalışması da aynı mekanizmanın metot çağrılarındaki karşılığıdır.

Kitap deref coercion'ın üç durumda yapıldığını sıralar: T: Deref<Target = U> ise &T türünden &U türüne, T: DerefMut<Target = U> ise &mut T türünden &mut U türüne ve yine T: Deref<Target = U> ise &mut T türünden &U türüne. Ters yön, yani paylaşılan referanstan değiştirilebilir referansa dönüşüm asla yapılmaz, çünkü bu ödünç kurallarının garantisini bozardı. Deref uygulamasını yalnızca gerçekten işaretçi gibi davranan tiplere ekleyin; sadece metot ödünç almak için kullanmak tipin ne olduğunu belirsizleştirir.

Drop: kapsam sonunda temizlik

İkinci trait Drop'tur ve bir değer kapsam dışına çıkarken çalışacak kodu belirler. Box bu trait sayesinde heap'teki alanı serbest bırakır. Kendi tiplerinizde dosya, bağlantı ya da kilit gibi kaynakları bırakmak için kullanabilirsiniz. Değişkenler oluşturuldukları sıranın tersine bırakılır. Bir değeri erken bırakmak istediğinizde Drop::drop metodunu doğrudan çağıramazsınız; derleyici bunu reddeder, çünkü kapsam sonunda aynı değer bir kez daha bırakılacak ve bu çift serbest bırakma hatasına yol açacaktı. Bunun yerine prelude'da bulunan std::mem::drop fonksiyonuna değeri taşırsınız.

rust
struct Kaynak {
    ad: &'static str,
}

impl Drop for Kaynak {
    fn drop(&mut self) {
        println!("{} serbest bırakıldı", self.ad);
    }
}

fn main() {
    let _a = Kaynak { ad: "a" };
    let _b = Box::new(Kaynak { ad: "b (kutuda)" });
    let c = Kaynak { ad: "c" };
    drop(c);
    println!("main sonuna gelindi");
}
// çıktı: c serbest bırakıldı
// çıktı: main sonuna gelindi
// çıktı: b (kutuda) serbest bırakıldı
// çıktı: a serbest bırakıldı

Çıktıda önce drop(c) ile erken bırakılan c, sonra main sonunda ters sırayla _b ve _a görünüyor. _b bir kutu olduğu için önce içindeki Kaynak değerinin drop metodu çalıştı, sonra heap alanı serbest bırakıldı. Değişken adlarının başındaki alt çizgi yalnızca kullanılmayan değişken uyarısını susturur; değer yine kapsamın sonunda bırakılır. Doğrudan çağrı denemesinin hatası aşağıda; derleyici hemen drop(k) yazmanızı öneriyor.

rust
struct Kaynak;

impl Drop for Kaynak {
    fn drop(&mut self) {}
}

fn main() {
    let k = Kaynak;
    k.drop();
}
// error[E0040]: explicit use of destructor method
// help: consider using `drop` function: drop(k);

Kutu dilimleri ve Box::leak

Vec<T> türünü into_boxed_slice ile Box<[T]> türüne çevirebilirsiniz; artık büyümeyecek bir listeyi kapasite bilgisi olmadan saklamanın yoludur. Rust 2024 edition'da bu tipin into_iter() davranışı değişti. Edition rehberine göre önceki edition'larda kutulu.into_iter() yöntem çağrısı dilime dereference edip referans üreten bir iterator döndürüyordu; 2024'te ise IntoIterator uygulamasını çağırır ve elemanları değer olarak verir. Aşağıdaki örnekte let deger: i32 = x; satırının derlenmesi bunu gösteriyor. Bütün örnekler edition 2024 ile derlendi.

rust
fn main() {
    let kutulu: Box<[i32]> = vec![1, 2, 3].into_boxed_slice();
    for x in kutulu.into_iter() {
        let deger: i32 = x;
        print!("{deger} ");
    }
    println!();

    let ayar: &'static mut String = Box::leak(Box::new(String::from("kalıcı")));
    ayar.push('!');
    println!("{ayar}");
}
// çıktı: 1 2 3
// çıktı: kalıcı!

Box::leak kutuyu tüketir ve içindeki değere &'static mut gibi uzun ömürlü bir referans döndürür; bellek bilinçli olarak hiç geri verilmez. Std belgesi bu fonksiyonun programın sonuna kadar yaşaması gereken veriler için uygun olduğunu, belleğin bir gün serbest bırakılması gerekiyorsa Box::into_raw kullanılmasını önerir. Bu fonksiyonun b.leak() değil Box::leak(b) biçiminde çağrıldığına dikkat edin. Box içindeki tipin metotlarıyla çakışmaması için bu tür yardımcılar ilişkili fonksiyon olarak tanımlanmıştır. Box::into_raw ve Box::from_raw ham işaretçilere geçiş yapar; o tarafın kuralları için unsafe yazısına bakın.

Sık yapılan hatalar

Sık yapılan hatalar: Her şeyi kutuya koymak., Paylaşmak için Box kullanmaya çalışmak., Drop sırasına güvenmeden kilit tutmak., Deref ile kalıtım taklit etmek.
  • Her şeyi kutuya koymak. Tek bir i32 gibi küçük değerleri kutulamak yalnızca ayırma maliyeti ekler. Kitabın da belirttiği gibi değerler varsayılan olarak stack'te durmalıdır; kutuyu yukarıdaki üç ihtiyaçtan biri için kullanın.
  • Paylaşmak için Box kullanmaya çalışmak. Box tek sahiplidir. Aynı veriyi iki yapıya koymaya çalışırsanız taşınmış değer hatası alırsınız. Çoklu sahiplik için Rc<T>, thread'ler arası için Arc<T> gerekir.
  • Drop sırasına güvenmeden kilit tutmak. Bir kilidi ya da kaynağı kapsamın sonundan önce bırakmanız gerekiyorsa drop(deger) çağırın ya da değeri iç bir bloğa alın.
  • Deref ile kalıtım taklit etmek. Deref tipe başka bir tipin metotlarını kazandırır ama bunu yalnızca işaretçi benzeri sarmalayıcılarda kullanmak beklenen davranıştır.
  • Boyutu bilinmeyen tipi doğrudan saklamak. dyn Trait ve [T] gibi tipler tek başına alan ya da değişken olamaz; Box, & veya Rc gibi bir işaretçinin arkasında durmalıdır.
İhtiyaçUygun tipNot
Tek sahip, heapBox<T>Ek çalışma zamanı maliyeti yok
Özyinelemeli enum veya structBox<T>Boyutu sonlu hâle getirir
Farklı tipleri ortak trait ile saklamaBox<dyn Trait>Dinamik dağıtım
Tek thread, çoklu sahipRc<T>Referans sayımı
Thread'ler arası çoklu sahipArc<T>Atomik referans sayımı

Tablodaki son satır için Arc ve Mutex yazısına bakabilirsiniz. Akıllı işaretçi zincirinde bir sonraki adım, birden çok sahibe izin veren referans sayımlı tiplerdir.

Sık Sorulan Sorular

Box&lt;T&gt; ile &amp;T arasındaki fark nedir?

&T başka birinin sahip olduğu veriyi ödünç alır ve bir lifetime'a bağlıdır. Box<T> ise verinin sahibidir; onu fonksiyondan döndürebilir, struct'ta saklayabilir ve kapsamı bittiğinde bırakılmasını sağlarsınız.

Box içindeki değeri nasıl geri alırım?

*kutu yazarak değeri dışarı taşıyabilirsiniz; std belgesi bunu kutudan stack'e taşıma olarak örnekler. Bundan sonra kutu kullanılamaz. Yalnızca okumak istiyorsanız referans almanız yeterlidir.

Box&lt;T&gt; kopyalanabilir mi?

Box<T> tek sahipli olduğu için Copy uygulamaz; atama ya da fonksiyona geçirme sahipliği taşır. İç tip Clone uyguluyorsa kutu.clone() yeni bir heap ayırması yapıp içeriği kopyalar. Bu, sayaç artıran Rc::clone davranışından farklıdır: kutunun klonu tamamen bağımsız ikinci bir değerdir ve birinde yaptığınız değişiklik diğerini etkilemez.

Box kullanmak programı yavaşlatır mı?

Kitaba göre kutuların, verinin heap'te durması dışında bir performans yükü yoktur. Heap ayırmanın kendisi bir maliyettir; bu yüzden gerçekten ihtiyaç duyduğunuz yerlerde kullanın.

Box&lt;dyn Trait&gt; yerine generic kullanabilir miyim?

Tek bir somut tiple çalışacaksanız generic ya da impl Trait daha uygundur. Aynı koleksiyonda farklı tipleri karıştırmanız gerekiyorsa trait object gerekir. Karar ölçütleri için generics yazısına ve dyn yazısına bakın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Rc ve RefCell · Trait object ve dyn · Ownership kuralları · Enum ve Option · Rust nedir?