Rust'da neden açık yaşam sürelerine ihtiyaç vardır?


199

Rust kitabının yaşamlar bölümünü okuyordum ve adlandırılmış / açık bir ömür için bu örneğe rastladım:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

Derleyici tarafından engellenen hatanın , iç kapsam tamamlandıktan sonra atanan referansın kullanılmadan sonra kullanılması olduğu açıktır x: fbu nedenle &f.xgeçersiz hale gelir ve bu nedenle geçersiz hale gelir ve atanmamış olması gerekir x.

Sorunum sorun kolayca uzağa analiz olabilirdi olmasıdır olmadan kullanan açık 'a daha geniş kapsamda bir referans yasadışı atama çıkarım yoluyla, örneğin ömür boyu ( x = &f.x;).

Hangi durumlarda ücretsiz kullanımdan sonra (veya başka bir sınıf?) Hatalarını önlemek için açık yaşam sürelerine gerçekten ihtiyaç vardır?



2
Bu sorunun gelecekteki okuyucuları için, lütfen kitabın ilk baskısına bağlandığını ve şimdi ikinci bir baskı olduğunu unutmayın :)
carols10cents

Yanıtlar:


205

Diğer cevapların hepsinin göze çarpan noktaları vardır ( açık bir yaşam süresinin gerekli olduğu somut örnek ), ancak bir anahtar şey eksik: derleyici size yanlış yaptığınızı söyleyecekse neden açık yaşam sürelerine ihtiyaç duyuluyor ?

Bu aslında "derleyici onları çıkarım yaptığında neden açık türlere ihtiyaç duyulur" sorusuyla aynı sorudur. Varsayımsal bir örnek:

fn foo() -> _ {  
    ""
}

Tabii ki, derleyici bir geri döndüğümü görebiliyor, &'static strpeki neden programcı yazmak zorunda?

Ana nedeni, derleyici kodunuzun ne yaptığını görebiliyor olsa da, niyetinizin ne olduğunu bilmiyor olmasıdır.

İşlevler, kod değiştirmenin etkilerini güvenlik duvarı için doğal bir sınır oluşturur. Yaşam sürelerinin koddan tamamen denetlenmesine izin verirsek, masum görünümlü bir değişiklik yaşamları etkileyebilir ve bu da uzaktaki bir işlevde hatalara neden olabilir. Bu varsayımsal bir örnek değil. Anladığım kadarıyla, üst düzey işlevler için tür çıkarımına güvendiğinizde Haskell'de bu sorun var. Rust bu sorunu tomurcukta sıkıştırdı.

Derleyiciye bir verimlilik avantajı da vardır - türleri ve kullanım ömürlerini doğrulamak için yalnızca işlev imzalarının ayrıştırılması gerekir. Daha da önemlisi, programcı için bir verimlilik faydası vardır. Açık yaşamlarımız olmasaydı, bu işlev ne yapar:

fn foo(a: &u8, b: &u8) -> &u8

Çok sayıda kodlama en iyi uygulamalarına karşı gidecek kaynağı incelemeden söylemek imkansızdır.

yasadışı bir atama için daha geniş bir kapsama atıfta bulunarak

Kapsamları vardır aslında, yaşam süreleri. Biraz daha açık bir şekilde, bir ömür 'a, çağrı sitesine bağlı olarak derleme zamanında belirli bir kapsamla özelleştirilebilen genel bir ömür boyu parametresidir .

[...] hatalarını önlemek için açık yaşam sürelerine gerçekten ihtiyaç var mı?

Bir şey değil. Yaşam süreleri hatalarını önlemek için gereklidir, ancak açık yaşam süreleri biraz aklı programcılar ne korumaya ihtiyaç vardır.


19
@jco f x = x + 1Başka bir modülde kullandığınız tür imzası olmayan bazı üst düzey işlevlere sahip olduğunuzu düşünün . Daha sonra tanımı olarak değiştirirseniz, f x = sqrt $ x + 1türü olarak değişir; Num a => a -> abu Floating a => a -> a, förneğin bir Intargümanla çağrılan tüm çağrı sitelerinde tür hatalarına neden olur . Tür imzası olması, hataların yerel olarak gerçekleşmesini sağlar.
fjh

11
"Kapsamlar yaşamlardır, esasen. Biraz daha net, bir ömür 'a, çağrı zamanında belirli bir kapsamla uzmanlaşabilen genel bir yaşam boyu parametresidir." Vay canına, bu gerçekten harika, aydınlatıcı bir nokta. Kitaba bu açıkça dahil edilmişse bunu istiyorum.
corazza

2
@fjh Teşekkürler. Sadece grok olup olmadığını görmek için - nokta, eklemeden önce tür açıkça belirtilmiş sqrt $olsaydı, değişiklikten sonra sadece yerel bir hata oluşacaktı ve diğer yerlerde çok fazla hata olmayacaktı (eğer yapmazsak çok daha iyi) gerçek türü değiştirmek istemez)?
corazza

5
@jco Aynen. Bir tür belirtmemek, bir işlevin arabirimini yanlışlıkla değiştirebileceğiniz anlamına gelir. Haskell'deki tüm üst düzey öğelere açıklama eklemenin şiddetle teşvik edilmesinin nedenlerinden biri de budur.
fjh

5
Ayrıca bir işlev iki başvuru alır ve bir başvuru döndürürse, bazen ilk başvuruyu bazen de ikincisini döndürür. Bu durumda, iade edilen referans için bir ömür boyu çıkarım yapmak imkansızdır. Açık yaşam süreleri böyle bir durumu önlemeye / açıklığa kavuşturmaya yardımcı olur.
MichaelMoser

94

Aşağıdaki örneğe bakalım.

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

Burada açık yaşamlar önemlidir. Bu, sonucunun fooilk argümanı ( 'a) ile aynı kullanım ömrüne sahip olması nedeniyle derlenir , bu nedenle ikinci argümanından daha uzun ömürlü olabilir. Bu, imzasındaki ömür boyu adlarla ifade edilir foo. fooDerleyiciye yapılan çağrıda argümanları değiştirdiyseniz, bunun yyeterince uzun yaşamadığından şikayet ederdiniz :

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here

16

Aşağıdaki yapıdaki ömür boyu ek açıklama:

struct Foo<'a> {
    x: &'a i32,
}

bir Fooörneğin içerdiği referanstan ( xalan) fazla çıkmaması gerektiğini belirtir .

Çünkü Pas kitapta rastladım örnek bu göstermek değil fve ydeğişkenler aynı anda kapsam dışına çıkar.

Daha iyi bir örnek şudur:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

Şimdi, fişaret ettiği değişken gerçekten çok daha iyi f.x.


9

Bu kod parçasında yapı tanımı dışında açık bir yaşam süresi olmadığını unutmayın. Derleyici, yaşamları mükemmel bir şekilde çıkarabilir main().

Bununla birlikte, tip tanımlarında açık yaşamlar kaçınılmazdır. Örneğin, burada bir belirsizlik var:

struct RefPair(&u32, &u32);

Bunlar farklı yaşamlar mı olmalı yoksa aynı mı olmalılar? Kullanım açısından önemlidir struct RefPair<'a, 'b>(&'a u32, &'b u32), çok farklıdır struct RefPair<'a>(&'a u32, &'a u32).

Şimdi, sağladığınız gibi basit durumlar için, derleyici , diğer yerlerde olduğu gibi teorik olarak yaşamları kaldırabilir , ancak bu gibi durumlar çok sınırlıdır ve derleyicide ekstra karmaşıklığa değmez ve netlikteki bu kazanç, en azından şüpheli.


2
Neden çok farklı olduklarını açıklayabilir misiniz?
AB

@AB İkincisi, her iki referansın da aynı kullanım ömrünü paylaşmasını gerektirir. Bu, refpair.1'in refpair2'den daha uzun yaşayamayacağı ve tam tersi anlamına gelir - bu nedenle her iki referansın da aynı sahiple bir şeye işaret etmesi gerekir. Birincisi, sadece RefPair'in her iki parçasını da geçmesini gerektirir.
llogiq

2
@AB, her iki yaşamın birleştirilmiş olduğu için derler - yerel yaşamlar daha küçük olduğundan 'static, 'staticyerel yaşamların kullanılabileceği her yerde kullanılabilir, bu nedenle örneğinizde p, yaşam boyu parametresi yerel yaşam süresi olarak çıkarılır y.
Vladimir Matveev

5
@AB , her iki girdi yaşam süresinin , yani bu durumda yaşamın kesişme noktası olacağı RefPair<'a>(&'a u32, &'a u32)anlamına gelir . 'ay
fjh

1
@llogiq "RefPair'in her iki parçasını da geçmesini gerektirir"? Ben tersi olsa ... a & u32 RefPair olmadan hala mantıklı olabilir, refs ölü RefPair garip olurdu.
qed

6

Kitaptaki durum tasarım açısından çok basit. Yaşam süresi konusu karmaşık kabul edilir.

Derleyici, birden çok bağımsız değişkeni olan bir işlevde kullanım ömrünü kolayca çıkaramaz.

Ayrıca, kendi isteğe bağlı sandığımın imzası gerçekte olan OptionBoolbir as_sliceyöntemi olan bir türü vardır :

fn as_slice(&self) -> &'static [bool] { ... }

Derleyicinin bunu anlayabilmesinin kesinlikle bir yolu yoktur.


IINM, iki bağımsız değişkenli bir işlevin dönüş türünün ömrünü kısaltmak, durdurma sorununa eşdeğer olacaktır - IOW, sınırlı bir sürede karar verilemez.
dstromberg


4

Bir işlev bağımsız değişken olarak iki başvuru alır ve bir başvuru döndürürse, işlevin uygulanması bazen ilk başvuruyu, bazen de ikinci başvuruyu döndürebilir. Belirli bir çağrı için hangi referansın döndürüleceğini tahmin etmek imkansızdır. Bu durumda, döndürülen referans için bir ömür boyu çıkarım yapmak imkansızdır, çünkü her bir argüman referansı farklı bir ömür ile farklı bir değişken bağlamaya işaret edebilir. Açık yaşam süreleri böyle bir durumu önlemeye veya açıklığa kavuşturmaya yardımcı olur.

Benzer şekilde, bir yapı iki referans içeriyorsa (iki üye alanı olarak), o zaman yapının bir üye fonksiyonu bazen ilk referansı bazen de ikinci referansı döndürebilir. Yine açık yaşamlar bu tür belirsizlikleri önler.

Birkaç basit durumda, derleyicinin yaşamları çıkarabileceği ömür boyu elizyon vardır .


1

Örneğinizin çalışmamasının nedeni Rust'un yalnızca yerel yaşam ve tür çıkarımına sahip olmasıdır. Önerdiğiniz şey küresel çıkarım gerektirir. Ömrü seçilemeyen bir referansınız olduğunda açıklama eklenmelidir.


1

Rust'a yeni gelen biri olarak benim anlayışım, açık yaşamların iki amaca hizmet ettiği.

  1. Bir işleve açık bir ömür boyu ek açıklama koymak, o işlevin içinde görünebilecek kod türünü kısıtlar. Açık yaşam süreleri, derleyicinin programınızın istediğinizi yaptığından emin olmasını sağlar.

  2. Siz (derleyici) bir kod parçasının geçerli olup olmadığını kontrol etmek istiyorsanız, siz (derleyici) çağrılan her işlevin tekrar tekrar bakmak zorunda kalmayacaksınız. Doğrudan bu kod parçası tarafından çağrılan işlev ek açıklamalarına bir göz atmak yeterlidir. Bu, programınızın sizin için (derleyici) akıl yürütmesini daha kolay hale getirir ve derleme sürelerini yönetilebilir hale getirir.

Nokta 1'de, Python ile yazılmış aşağıdaki programı düşünün:

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

hangisi yazdıracak

array([[1, 0],
       [0, 0]])

Bu tür davranışlar beni her zaman şaşırtıyor. Olan şey, dfhafızayı paylaşmaktır ar, bu yüzden bazı dfdeğişikliklerin içeriğinde workbu değişiklik de bulaşır ar. Bununla birlikte, bazı durumlarda bu, bellek verimliliği nedenleriyle (kopya yok) tam olarak istediğiniz şey olabilir. Bu koddaki gerçek sorun, fonksiyonun second_rowikinci yerine ilk satırı döndürmesidir; hata ayıklama iyi şanslar.

Bunun yerine Rust ile yazılmış benzer bir programı düşünün:

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

Bunu derlerken,

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

Aslında iki hata alıyorsunuz, rolleri 'ave 'bdeğişimleri olan bir tane var. Ek açıklamasına bakıldığında second_row, çıktının &mut &'b mut [i32], yani çıktının ömür boyu referansa 'b(ikinci satırın ömrü) bir referans olması gerektiğini buluyoruz Array. Ancak, ilk satırı döndürdüğümüz için (ki bu da ömür boyu vardır 'a), derleyici ömür boyu uyumsuzluktan yakınır. Doğru yerde. Doğru zamanda. Hata ayıklama bir esinti.


0

Ömür boyu açıklamayı belirli bir ref ile ilgili bir sözleşme olarak sadece alıcı kapsamda geçerli iken kaynak kapsamda geçerliliğini koruduğunu düşünüyorum. Aynı ömür boyu daha fazla referans bildirmek kapsamları birleştirir, yani tüm kaynak referanslarının bu sözleşmeyi yerine getirmesi gerekir. Böyle bir açıklama derleyicinin sözleşmenin yerine getirilip getirilmediğini kontrol etmesini sağlar.

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.