Bunun basit bir uygulamasına bakalım :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Bu hata ile başarısız olur:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Bu hatayı tamamen anlamak için, değerlerin bellekte nasıl temsil edildiğini ve
bu değerleri taşıdığınızda ne olacağını düşünmeniz gerekir . Combined::new
Değerlerin nerede bulunduğunu gösteren bazı varsayımsal bellek adresleriyle açıklama ekleyelim:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Ne olmalı child
? Değer yeni olduğu gibi taşındıysa parent
, artık geçerli bir değere sahip olacağı garanti edilmeyen belleğe atıfta bulunur. Başka herhangi bir kod parçasının 0x1000 bellek adresindeki değerleri depolamasına izin verilir. Bu belleğe bir tamsayı olduğu varsayıldığında çökmelere ve / veya güvenlik hatalarına neden olabilir ve Rust'un önlediği ana hata kategorilerinden biridir.
Yaşamların önlediği sorun tam olarak budur . Bir ömür, sizin ve derleyicinin bir değerin geçerli bellek konumunda ne kadar geçerli olacağını bilmenizi sağlayan bir meta veri . Rust'un yeni gelenlerin yaptığı yaygın bir hata olduğu için bu önemli bir ayrım. Pas ömrü, bir nesnenin yaratıldığı zaman yok edildiği zaman arasındaki süre değildir !
Bir benzetme olarak, bunu şöyle düşünün: Bir insanın hayatı boyunca, her biri farklı bir adrese sahip birçok farklı yerde kalacaklar. Rust ömrü, şu anda ikamet ettiğiniz adresle ilgilidir , gelecekte ne zaman öleceğinizle ilgili değildir (ölmek de adresinizi değiştirse de). Her hareket ettirdiğinizde, adresiniz artık geçerli olmadığı için alakalı olur.
Bu yaşam süreleri dikkat etmek de önemlidir yok kodunuzu değiştirin; kodunuz ömürleri kontrol eder, ömürleriniz kodu kontrol etmez. Özlü söz "yaşamlar tanımlayıcıdır, kuralcı değildir" dir.
Ömrünü Combined::new
vurgulamak için kullanacağımız bazı satır numaralarına açıklama ekleyelim :
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
Beton süresi içinde parent
, 1 ila 4 olan (I olarak temsil olacak olan dahil [1,4]
). Beton ömrüchild
IS [2,4]
, ve geri dönüş değeri somut kullanım süresi olan [4,5]
. Sıfırdan başlayan somut yaşamlara sahip olmak mümkündür - bu, bir parametrenin ömrünü veya bloğun dışında var olan bir şeyi temsil eder.
child
Kendisinin ömrünün olduğunu [2,4]
, ancak ömrünün değeri olan bir değeri ifade ettiğini unutmayın .[1,4]
. Bu, atıfta bulunulan değer yapılmadan önce referans değeri geçersiz hale geldiği sürece iyidir. child
Bloktan dönmeye çalıştığımızda sorun oluşur . Bu, ömrü doğal uzunluğunun ötesine "uzatır".
Bu yeni bilgi ilk iki örneği açıklamalıdır. Üçüncüsü, uygulanmasına bakmayı gerektirir Parent::child
. Şansı, şöyle görünecektir:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Bu, açık genel ömür boyu parametrelerini yazmaktan kaçınmak için ömür boyu elizyon kullanır . Şuna eşittir:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Her iki durumda da yöntem Child
, beton ömrü ile parametreleştirilmiş bir yapının iade edileceğini
söylüyor self
. Başka bir deyişle, Child
örnek,Parent
onu oluşturana ve bu nedenle bu Parent
örnekten daha uzun yaşayamaz
.
Bu aynı zamanda yaratım fonksiyonumuzda bir şeyin gerçekten yanlış olduğunu anlamamızı sağlar:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Bunu farklı bir biçimde yazılmış görmeniz daha olası olsa da:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
Her iki durumda da, bir bağımsız değişken aracılığıyla herhangi bir ömür boyu parametre sağlanmaz. Bu demektir ki ömür boyuCombined
parametreleştirilecek olan hiçbir şeyle sınırlandırılmadığı - arayan kişinin istediği her şey olabilir. Bu saçmadır, çünkü arayan 'static
yaşam süresini belirleyebilir ve bu koşulu karşılamanın bir yolu yoktur.
Nasıl düzeltebilirim?
En kolay ve en çok önerilen çözüm, bu öğeleri aynı yapıda bir araya getirmeye çalışmamaktır. Bunu yaptığınızda, yapı yuvalama kodunuzun ömrünü taklit eder. Verilere sahip olan türleri bir yapıya birlikte yerleştirin ve ardından gerektiğinde referanslar veya referanslar içeren nesneler almanızı sağlayan yöntemler sağlar.
Ömür boyu izlemenin aşırı hevesli olduğu özel bir durum var: Yığına bir şey koyduğunuzda. Bu, bir
Box<T>
, örneğin . Bu durumda, taşınan yapı yığının içine bir işaretçi içerir. Sivri uç değeri sabit kalacaktır, ancak işaretçinin adresi hareket edecektir. Pratikte, işaretçiyi her zaman takip ettiğiniz için bu önemli değildir.
Kiralık sandık (ARTIK SAĞLANAN VEYA DESTEKLİ) veya owning_ref sandık bu davayı temsil yolları vardır, ancak bunlar taban adresi gerektirir hareket asla . Bu, yeniden tahsis edilen ve yığına ayrılan değerlerin hareketine neden olabilecek mutasyonlu vektörleri dışlar.
Kiralama ile çözülen sorunlara örnekler:
Diğer durumlarda, Rc
veya gibi bir tür referans sayımına geçmek isteyebilirsiniz.Arc
.
Daha fazla bilgi
parent
Yapıya geçtikten sonra , derleyici neden yapıya yeni bir referans alamıyor parent
ve yapıya atayamıyor child
?
Teorik olarak bunu yapmak mümkün olsa da, bunu yapmak büyük miktarda karmaşıklık ve ek yük getirecektir. Nesnenin her taşınışında, derleyicinin referansı "düzeltmek" için kod eklemesi gerekir. Bu, bir yapıyı kopyalamanın artık sadece bazı bitleri hareket ettiren çok ucuz bir işlem olmadığı anlamına gelir. Varsayımsal bir optimize edicinin ne kadar iyi olacağına bağlı olarak, böyle bir kodun pahalı olduğu anlamına gelebilir:
let a = Object::new();
let b = a;
let c = b;
Bunu her hareket için gerçekleşmeye zorlamak yerine , programcı yalnızca bunları çağırdığınızda uygun referansları alacak yöntemler oluşturarak bunun ne zaman olacağını seçer .
Kendine gönderme yapan bir tür
Eğer belirli bir durum var olabilir kendisine referans içeren bir türü oluşturun. Option
Yine de iki adımda yapmak gibi bir şey kullanmanız gerekir :
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
Bu bir anlamda işe yarar, ancak yaratılan değer oldukça kısıtlıdır - asla hareket ettirilemez. Özellikle bu, bir işlevden döndürülemeyeceği ya da herhangi bir değere by-geçirilemeyeceği anlamına gelir. Bir yapıcı işlevi, yukarıdaki yaşam süreleriyle aynı sorunu gösterir:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Ne olmuş Pin
?
Pin
, Rust 1.33'te stabilize edilmiş , modül belgelerinde bulunur :
Bu tür bir senaryonun en iyi örneği, kendinden referanslı yapılar oluşturmak olacaktır, çünkü işaretçilerle bir nesnenin kendisine taşınması, bunları geçersiz kılarak tanımlanmamış davranışa neden olabilir.
"Öz-referans" ın mutlaka bir referans kullanmak anlamına gelmediğine dikkat etmek önemlidir . Gerçekten de, kendini referans alan bir yapı örneği özellikle şöyle diyor:
Derleyiciyi normal bir referansla bu konuda bilgilendiremeyiz, çünkü bu model her zamanki borçlanma kuralları ile tanımlanamaz. Bunun yerine ham bir işaretçi kullanırız , ancak boş olmadığı bilinen bir işaretçi kullanırız , çünkü dizeye işaret ettiğini biliyoruz.
Bu davranış için ham bir işaretçi kullanma yeteneği, Rust 1.0 beri var olmuştur. Gerçekten de, sahip olma-ref ve kiralama, kaputun altında ham işaretçiler kullanır.
Pin
Tabloya eklenen tek şey, belirli bir değerin hareket etmediğinin garanti edilmesinin yaygın bir yoludur.
Ayrıca bakınız:
Parent
veChild
yardımcı olabilir ...