Neden aynı yapıda bir değer ve bu değere başvuru saklayamıyorum?


223

Bir değer var ve bu değeri ve kendi türünde bu değer içinde bir şey için bir başvuru depolamak istiyorum:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

Bazen, bir değere sahibim ve bu değeri ve bu değere bir referansı aynı yapıda saklamak istiyorum:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

Bazen, değerin bir referansını bile almıyorum ve aynı hatayı alıyorum:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

Bu vakaların her birinde, değerlerden birinin "yeterince uzun yaşamadığını" bildiren bir hata alıyorum. Bu hata ne anlama geliyor?


1
İkinci örnek için, bir tanımı Parentve Childyardımcı olabilir ...
Matthieu M.

1
@MatthieuM. Bunu tartıştım, ancak bağlantılı iki soruya dayanarak karar verdim. Bu soruların hiçbiri söz konusu yapının veya yöntemin tanımına bakmadı , bu yüzden insanların bu soruyu kendi durumlarıyla daha kolay eşleştirebileceğini taklit etmenin en iyi olacağını düşündüm. Not Bunu yapmak yanıtında yöntem imzası göstermektedir.
Shepmaster

Yanıtlar:


245

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::newDeğ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::newvurgulamak 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.

childKendisinin ö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. childBloktan 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 'staticyaş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, Rcveya gibi bir tür referans sayımına geçmek isteyebilirsiniz.Arc .

Daha fazla bilgi

parentYapıya geçtikten sonra , derleyici neden yapıya yeni bir referans alamıyor parentve 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. OptionYine 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.

PinTabloya eklenen tek şey, belirli bir değerin hareket etmediğinin garanti edilmesinin yaygın bir yoludur.

Ayrıca bakınız:


1
Böyle bir şey ( is.gd/wl2IAt ) deyimsel kabul edilir mi? Yani, verileri ham veri yerine yöntemlerle göstermek.
Peter Hall

2
Emin @PeterHall, sadece araç Combinedsahibi Childsahip olduğu Parent. Bu, sahip olduğunuz gerçek türlere bağlı olarak anlamlı olabilir veya olmayabilir. Kendi dahili verilerinize referanslar döndürmek oldukça tipiktir.
Shepmaster

Yığın sorununun çözümü nedir?
derekdreery

@derekdreery belki yorumunuzu genişletebilirsiniz? Paragrafın tamamı neden owning_ref sandığından bahsediyor ?
Shepmaster

1
@FynnBecker bir referansı ve bu referansa bir değeri saklamak hala imkansızdır . Pinkendinden referanslı işaretçi içeren bir yapının güvenliğini bilmenin bir yoludur . Aynı amaç için ham bir işaretçi kullanma yeteneği Rust 1.0'dan beri var olmuştur.
Shepmaster

4

Çok benzer derleyici mesajlarına neden olan biraz farklı bir sorun, açık bir referansı saklamak yerine nesne yaşam boyu bağımlılığıdır. Buna bir örnek ssh2 kütüphanesidir. Bir test projesinden daha büyük bir şey geliştirirken , bu oturumdan elde edilen Sessionve Channelelde edilen oturumun bir yapıya konulması, uygulama detaylarını kullanıcıdan gizlemek cazip gelebilir. Ancak, Channeltanımın 'sesstür açıklamasında ömür boyu olduğunu unutmayın , ancak Sessionyok.

Bu, ömürlerle ilgili benzer derleyici hatalarına neden olur.

Bunu çok basit bir şekilde çözmenin bir yolu, Sessionarayanı dışarıda bildirmek ve daha sonra bu Rust Kullanıcı Forumu gönderisindeki cevaba benzer şekilde, yapı içindeki referansı ömür boyu açıklamaktır. SFTP'yi kapsüllerken aynı sorun hakkında konuşurken . Bu zarif görünmeyecek ve her zaman geçerli olmayabilir - çünkü şimdi istediğiniz bir şeyden ziyade başa çıkmak için iki varlığınız var!

Dışarı Dönüşler kiralama sandık veya owning_ref sandık diğer cevabını da bu sorun için çözümler vardır. En tam bu amaç için özel bir nesne vardır owning_ref, düşünelim: OwningHandle. Altta yatan nesnenin hareket etmesini önlemek için, bunu Boxaşağıdaki olası çözümü veren a'yı kullanarak öbek üzerinde ayırırız :

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Bu kodun sonucu olarak Sessionartık kullanamıyoruz , ancak kullanacağımız kodla birlikte saklanıyor Channel. OwningHandleNesne Box, bir yapıya Channelkaydederken , bu dereferences, dereferences , dereferences çünkü biz böyle adlandırır. NOT: Bu sadece benim anlayışım. Güvensizlik tartışmasınaOwningHandle oldukça yakın gözüktüğü için bu doğru olmayabilir .

Burada merak uyandıran bir detay, Sessionmantıksal TcpStreamolarak Channelolması gerektiği gibi benzer bir ilişkiye sahip Sessionolmakla birlikte, mülkiyeti alınmaz ve böyle bir ek açıklama yoktur. Bunun yerine, el sıkışma yönteminin belgelerinin söylediği gibi, bununla ilgilenmek kullanıcının sorumluluğundadır :

Bu oturum sağlanan soketin sahipliğini almaz, iletişimin doğru şekilde gerçekleştirildiğinden emin olmak için soketin bu oturumun ömrünü sürdürdüğünden emin olunması önerilir.

Ayrıca, sağlanan akışın protokole müdahale edebileceğinden, bu oturum süresince başka bir yerde eşzamanlı olarak kullanılmaması şiddetle tavsiye edilir.

Yani TcpStreamkullanımı ile, kodun doğruluğunu sağlamak için tamamen programcıya kalmış. İle, OwningHandle"tehlikeli sihir" in nerede olduğuna dikkat,unsafe {} blok .

Bu konuyla ilgili daha fazla ve daha üst düzey bir tartışma, farklı bir örnek ve güvenli olmayan bloklar içermeyen kiralama sandığını kullanarak çözümünü içeren bu Rust Kullanıcı Forumu iş parçasındadır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.