🔭

【Rust】変数のスコープまとめ

2022/07/02に公開

Rustの特徴的な言語仕様として、「所有権とライフタイム」というのがあります。
これと上手に付き合うには、「この変数がいつまで生きているのか」つまり「変数のスコープ」を知ることがとても重要です。これがわからないと、Rustのボローチェッカーに頻繁に指摘されてもどうすればいいのかわからなかったりするでしょう。
Rustで躓くポイントの一つだと思います。

そこでこの記事では、「変数のスコープ」について、極力具体的なコードを挙げて掘り下げていきたいと思います。

その前に、変数のスコープを確認しやすくするために、以下のような定義の構造体を定義して、生成と破棄のタイミングで標準出力されるようにします。

struct MyStruct<T: Display> {
    value: T,
}

impl<T: Display> MyStruct<T> {
    fn new(value: T) -> Self {
        // MyStructが生成されるタイミングで実行
        println!("new {}", value);
        Self { value }
    }

    fn value(&self) -> &T {
        &self.value
    }
}

impl<T: Display> Drop for MyStruct<T> {
    fn drop(&mut self) {
        // MyStructが破棄されるタイミングで実行
        println!("drop {}", &self.value);
    }
}

以降、MyStruct構造体は、この構造体を使用しているものとします。

基本

関数のスコープ

関数内に所有権がある変数は、関数を抜けるタイミングで破棄されます。

下記の例では、"drop A"が"end main"よりも先に出力されていることからわかります。

fn my_fn() {
    println!("start my_fn");
    let a = MyStruct::new("A".to_string()); // new A
    println!("use {}", a.value());
    println!("end my_fn");
    // drop A
}

println!("start main");
my_fn();
println!("end main");
start main
start my_fn
new A
use A
end my_fn
drop A
end main

そのため、関数内に所有権がある変数の参照は、関数外に出せません。

fn my_fn<'a>() -> &'a String {
    println!("start my_fn");
    let a = MyStruct::new("A".to_string()); // new A
    println!("use {}", a.value());
    println!("end my_fn");
    a.value()
    // drop A
}
error[E0515]: cannot return reference to local variable `a`
  --> src\main.rs:14:5
   |
14 |     a.value()
   |     ^^^^^^^^^ returns a reference to data owned by the current function

ブロックのスコープ

スコープを定義したい場合は、{}でブロックを作ると、その内部が変数のスコープになります。この時、ブロック内に所有権がある変数は、ブロックを抜けるタイミングで破棄されます。

下記の例では、"drop A"が"end main"よりも先に出力されていることからわかります。

println!("start main");
{
    println!("start block");
    let a = MyStruct::new("A".to_string()); // new A
    println!("use {}", a.value());
    println!("end block");
    // drop A
}
println!("end main");
start main
start block
new A
use A
end block
drop A
end main

そのため、ブロック内に所有権がある変数の参照は、ブロック外に出せません。

println!("start main");
let a = {
    println!("start block");
    let a = MyStruct::new("A".to_string()); // new A
    println!("use1 {}", a.value());
    println!("end block");
    a.value()
    // drop A
};
println!("use2 {}", a);
println!("end main");
error[E0597]: `a` does not live long enough
  --> src\main.rs:10:9
   |
5  |     let a = {
   |         - borrow later stored here
...
10 |         a.value()
   |         ^^^^^^^^^ borrowed value does not live long enough
11 |     };
   |     - `a` dropped here while still borrowed

ループのスコープ

ループ中に作成した変数はそのループが完了すると、破棄されます。

下記の例では、出力結果から、次のループが始まる前にdropされていることがわかります。

println!("start main");
let mut i = 0;
loop {
    println!("start loop");
    let a = MyStruct::new("A".to_string() + &i.to_string()); // new A
    println!("use {}", a.value());
    if i < 2 {
        i += 1;
    } else {
        break;
    }
    println!("end loop");
    // drop A
}
println!("end main");
start main
start loop
new A0
use A0
end loop
drop A0
start loop
new A1
use A1
end loop
drop A1
start loop
new A2
use A2
drop A2
end main

宣言順によるスコープ差

後に宣言された変数から先に破棄されます。引数は後ろから先に破棄されます。

fn my_fn(arg1: MyStruct<String>, arg2: MyStruct<String>) {
    println!("start my_fn");
    let a = MyStruct::new("A".to_string());
    let b = MyStruct::new("B".to_string());
    println!("end my_fn");
}

my_fn(MyStruct::new("ARG1".to_string()), MyStruct::new("ARG2".to_string()));

new ARG1
new ARG2
start my_fn
new A
new B
end my_fn
drop B
drop A
drop ARG2
drop ARG1

その他、構造体の宣言の後ろ順から破棄されるなど、細かいルールが決まっています。そのへんは
https://doc.rust-lang.org/reference/destructors.html
あたりに載っているので、そちらを参照してください。

一時的なスコープ

一時変数(右辺値)のスコープ

// let a = MyStruct::new("A".to_string());
MyStruct::new("A".to_string());

のように、letで変数を束縛しない場合、生成されたけど束縛されなかった一時変数は、生成された式が終わった時点で破棄されます。

下記の例では、"drop A"が"end main"よりも先に出力されていることからわかります。

println!("start main");
println!("use {}", MyStruct::new("A".to_string()).value()); // new A drop A
println!("end main");
start main
new A
use A
drop A
end main

以下はletで束縛した場合です。"end main"が"drop A"よりも先に出力されているので、main関数終了まで破棄されていないことがわかります。

println!("start main");
let a = MyStruct::new("A".to_string()); // new A
println!("use {}", a.value());
println!("end main");
// drop A
start main
new A
use A
end main
drop A

生成されたけど束縛されなかった一時変数は、生成された式が終わった時点で破棄されるため、一時変数の参照を使用することはできません。

println!("start main");
let a: &String = MyStruct::new("A".to_string()).value(); // new A drop A
println!("use {}", a); // error MyStructのdrop後に、MyStruct::valueの参照を使用している!!
println!("end main");
error[E0716]: temporary value dropped while borrowed
 --> src\main.rs:6:22
  |
6 |     let a: &String = MyStruct::new("A".to_string()).value();
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        - temporary value is freed at the end of this statement
  |                      |
  |                      creates a temporary which is freed while still in use
7 |     println!("use {}", a);
  |                        - borrow later used here
  |
  = note: consider using a `let` binding to create a longer lived value

エラーメッセージに"temporary value"の文言が出てきたら、この辺の話だと思ってください。

また、あまり重要ではありませんが、let _で受けた場合は、束縛した扱いされず、一時変数として即時破棄されます。

println!("start main");
let _ = MyStruct::new("A".to_string());
println!("end main");
start main
new A   
drop A  
end main

let _a等、アンダースコアで始まる変数名で受けた場合は、通常のlet同様にスコープを抜けるまで破棄されません。

println!("start main");
let _a = MyStruct::new("A".to_string());
println!("end main");
start main
new A
end main
drop A

matchのスコープ(if letwhile letforも同様)

match式でパターンマッチする変数は、match式全体がスコープです。また、match式のアーム内もそれぞれスコープになっています。

println!("start main");
match MyStruct::new("A".to_string()) { // new A
    a => {
        println!("start match");
        let b = MyStruct::new("B".to_string()); // new B
        println!("use {}", a.value());
        println!("use {}", b.value());
        println!("end match");
    }, // drop B
} // drop A
println!("end main");
start main
new A
start match
new B
use A
use B
end match
drop B
drop A
end main

match式でパターンマッチする変数のスコープがmatch式全体ということは、束縛されない一時変数が破棄されるのもmatch式の終了時点です。

println!("start main");
match MyStruct::new("A".to_string()).value() { // new A
    a => {
        println!("start match");
        println!("use {}", a);
        println!("end match");
    },
} // drop A
println!("end main");
start main
new A
start match
use A
end match
drop A
end main
println!("start main");
let a = match MyStruct::new("A".to_string()).value() { // new A
    a => {
        println!("start match");
        println!("use1 {}", a);
        println!("end match");
        a
    },
    // drop A
};
println!("use2 {}", a); // error MyStructのdrop後に、MyStruct::valueの参照を使用している!!
println!("end main");
error[E0716]: temporary value dropped while borrowed
  --> src\main.rs:5:19
   |
5  |     let a = match MyStruct::new("A".to_string()).value() {
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use
...
12 |     };
   |      - temporary value is freed at the end of this statement
13 |     println!("use2 {}", a);
   |                         - borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

一時変数なので、ブロックで囲ってもブロック内が一つの式であれば生きたままです。

println!("start main");
match { MyStruct::new("A".to_string()).value() } { // new A
    a => {
        println!("start match");
        println!("use {}", a);
        println!("end match");
        a
    },
    // drop A
};
println!("end main");
start main 
new A
start match
use A
end match
drop A
end main

MyStructが、let a = MyStruct::new("A".to_string())のように束縛された場合は、ブロックのスコープと同様の結果(コンパイルエラー)となります。

println!("start main");
match {
    let a = MyStruct::new("A".to_string()); // new A
    a.value()
    // drop A
} { 
    a => {
        println!("start match");
        println!("use {}", a);
        println!("end match");
        a
    },
};
println!("end main");
error[E0597]: `a` does not live long enough
 --> src\main.rs:7:9
  |
5 |       match {
  |  ___________-
6 | |         let a = MyStruct::new("A".to_string());
7 | |         a.value()
  | |         ^^^^^^^^^ borrowed value does not live long enough   
8 | |     } { // new A
  | |     -
  | |     |
  | |_____`a` dropped here while still borrowed
  |       borrow later used here

ちなみに余談ですが、この性質を利用して、macro_rules!で式を受け取る際に、マクロの引数が一時的なスコープになっていても変数が破棄されないように、matchを使うテクニックがよく使われます。
下記の例はstd::assert_eqのソースコードです。

macro_rules! assert_eq {
    ($left:expr, $right:expr $(,)?) => {
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    let kind = $crate::panicking::AssertKind::Eq;
                    // The reborrows below are intentional. Without them, the stack slot for the
                    // borrow is initialized even before the values are compared, leading to a
                    // noticeable slow down.
                    $crate::panicking::assert_failed(kind, &*left_val, &*right_val, $crate::option::Option::None);
                }
            }
        }
    };
    ($left:expr, $right:expr, $($arg:tt)+) => {
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    let kind = $crate::panicking::AssertKind::Eq;
                    // The reborrows below are intentional. Without them, the stack slot for the
                    // borrow is initialized even before the values are compared, leading to a
                    // noticeable slow down.
                    $crate::panicking::assert_failed(kind, &*left_val, &*right_val, $crate::option::Option::Some($crate::format_args!($($arg)+)));
                }
            }
        }
    };
}

今回はmatchでの検証のみ載せていますが、if letwhile letformatchと同じです。

構造体を作る式

クロージャのスコープ

クロージャは、クロージャ本体のスコープとクロージャ内部のスコープが存在します。

クロージャ内で作成した変数は、クロージャ内がスコープとなり、クロージャ外からクロージャににmoveした変数は、クロージャのスコープ=変数のスコープとなります。

下記の例では、"drop closure"、"drop B"、"end main"の順に出力されていることからわかります。

println!("start main");
let b = MyStruct::new("B".to_string()); // new B
{
    println!("new closure");
    let closure = move || {
        println!("start closure");
        let a = MyStruct::new("A".to_string()); // new A
        println!("use {}", a.value());
        println!("use {}", b.value());
        println!("end closure");
        // drop A
    };
    closure();
    println!("drop closure");
    // drop B
}
println!("end main");
start main
new B
new closure
start closure
new A
use A
use B
end closure
drop A
drop closure
drop B
end main

moveせずに参照だけしている変数は、もとの変数のスコープのままとなります。

下記の例では、"drop closure"、"end main"、"drop B"の順に出力されていることからわかります。

println!("start main");
let b = MyStruct::new("B".to_string()); // new B
{
    println!("new closure");
    let closure = || {
        println!("start closure");
        let a = MyStruct::new("A".to_string()); // new A
        println!("use {}", a.value());
        println!("use {}", b.value());
        println!("end closure");
        // drop A
    };
    closure();
    println!("drop closure");
}
println!("end main");
// drop B
start main
new B
new closure
start closure
new A
use A
use B
end closure
drop A
drop closure
end main
drop B

asyncブロックのスコープ

asyncブロックもクロージャと同様、本体のスコープと内部のスコープが存在します。
しかし、Futureは基本的に'staticFuture自体の所有権を要求される場合が多いため、あまり意識する必要はないかもしれません。クロージャと同じく、moveした変数はFutureと同じスコープとなります。

(Futureの検証のために、async-stdクレートを使用しています。)
https://async.rs/

println!("start main");
let c = MyStruct::new("C".to_string()); // new C
{
    let future = async move {
        println!("start future");
        let a = MyStruct::new("A".to_string()); // new A
        println!("use {}", a.value());
        async_std::task::yield_now().await;
        let b = MyStruct::new("B".to_string()); // new B
        println!("use {}", b.value());
        async_std::task::yield_now().await;
        println!("use {}", c.value());
        println!("end future");
        // drop B
        // drop A
    };
    async_std::task::block_on(future);
    // drop C
}
println!("end main");
start main
new C
start future
new A
use A
new B
use B
use C
end future
drop B
drop A
drop C
end main

下記のような、moveせずに参照のみ使うのは、あまり実用する機会はないですが、クロージャと同じで元のスコープと同じになります。

println!("start main");
let c = MyStruct::new("C".to_string()); // new C
{
    let future = async {
        println!("start future");
        let a = MyStruct::new("A".to_string()); // new A
        println!("use {}", a.value());
        async_std::task::yield_now().await;
        let b = MyStruct::new("B".to_string()); // new B
        println!("use {}", b.value());
        async_std::task::yield_now().await;
        println!("use {}", c.value());
        println!("end future");
        // drop B
        // drop A
    };
    async_std::task::block_on(future);
}
println!("end main");
// drop C
start main
new C
start future
new A
use A
new B
use B
use C
end future
drop B
drop A
end main    
drop C

また、asyncブロック内で、awaitをまたぐブロックも作成でき、内部にawaitを含むスコープを定義できます。

下記の例では、asyncブロック内のブロックの最後に"drop B"が出力されていることから、ブロックがスコープになっていることがわかります。

println!("start main");
let c = MyStruct::new("C".to_string()); // new C
{
    let future = async move {
        println!("start future");
        let a = MyStruct::new("A".to_string()); // new A
        {
            println!("use {}", a.value());
            async_std::task::yield_now().await;
            let b = MyStruct::new("B".to_string()); // new B
            println!("use {}", b.value());
            async_std::task::yield_now().await;
            println!("use {}", c.value());
            // drop B
        }
        println!("end future");
        // drop A
    };
    async_std::task::block_on(future);
    // drop C
}
println!("end main");
start main
new C
start future
new A
use A
new B
use B
use C
drop B
end future
drop A
drop C
end main

これは一見普通に見えるかもしれません。しかし、FutureFurure::pollメソッドを通して実行されますが、一回の実行でawaitまで実行され、次回の実行は中断したawaitから再開されます。つまり、awaitをまたぐ場合は、その前後は一連の処理ではなく、中断した時の状態はFutureを実装した構造体に保存されています。なので、見た目はただのブロックのスコープですが、処理が分割されている特殊なスコープとなります。

また、asyncブロック内の変数の参照をawaitをまたいで保持することも可能です。

println!("start main");
{
    let future = async move {
        println!("start future");
        for val in [
            MyStruct::new("A".to_string()).value(),
            MyStruct::new("B".to_string()).value(),
            MyStruct::new("C".to_string()).value(),
        ] {
            async_std::task::yield_now().await;
            println!("use {}", val);
        }
        println!("end future");
    };
    async_std::task::block_on(future);
}
println!("end main");
start main
start future
new A
new B
new C
use A
use B
use C
drop C
drop B
drop A
end future
end main

これも一見普通に感じるかもしれませんが、先程の説明であったように、awaitで一旦処理を中断するときに、awaitをまたぐ参照も含めて中断した時の状態がFutureを実装した構造体に保存されます。この参照は、Future内の変数を参照しているため、自己参照となります。

通常、Rustでは自己参照は安全に定義できないのですが、Pinによってmoveを禁止されている場合に限り自己参照は安全になります。FutureFurure::pollメソッドを通して実行されますが、poll実行時は必ずFuturePinでくるむ必要があります。そして、asyncブロックで定義したFutureUnpinを実装していないため、一度Pinでくるんだ後は、Pinを解除することができず、永遠にPinに包まれたままとなります。つまり、一度Pinで包んだ後は、安全に自己参照を作ることができるのです。これが保証されているため、awaitをまたぐ参照を保持することが可能となっています。

参考
https://tech-blog.optim.co.jp/entry/2020/03/05/160000

Generatorのスコープ

Generator(2022年7月現在、まだUnstableな機能)は、Futureとほぼ同じ仕組みとなっています。というより、Generatorを特化した定義がFutureです。なので、スコープの性質もほぼ同じです。

#![feature(generators, generator_trait)]

println!("start main");
let c = MyStruct::new("C".to_string()); // new C
{
    let mut generator = || {
        println!("start generator");
        let a = MyStruct::new("A".to_string()); // new A
        {
            println!("use {}", a.value());
            yield;
            let b = MyStruct::new("B".to_string()); // new B
            println!("use {}", b.value());
            yield;
            println!("use {}", c.value());
            // drop B
        }
        println!("end generator");
        // drop A
    };
    loop {
        match std::pin::Pin::new(&mut generator).resume(()) {
            std::ops::GeneratorState::Yielded(()) => {},
            std::ops::GeneratorState::Complete(()) => break,
        }
    }
}
println!("end main");
// drop C
start main
new C
start generator
new A
use A
new B
use B
use C
drop B
end generator
drop A
end main
drop C

しかし、このGeneratorの構文で作成したGeneratorは、デフォルトでUnpinを実装しています。そのため、自己参照、つまり、yieldをまたぐ参照を保持することが禁止されています。

#![feature(generators, generator_trait)]

println!("start main");
{
    let mut generator = || {
        println!("start future");
        for val in [
            MyStruct::new("A".to_string()).value(),
            MyStruct::new("B".to_string()).value(),
            MyStruct::new("C".to_string()).value(),
        ] {
            yield;
            println!("use {}", val);
        }
        println!("end future");
    };
    loop {
        match std::pin::Pin::new(&mut generator).resume(()) {
            std::ops::GeneratorState::Yielded(()) => {},
            std::ops::GeneratorState::Complete(()) => break,
        }
    }
}
println!("end main");
error[E0626]: borrow may still be in use when generator yields
  --> src\main.rs:10:17
   |
10 |                 MyStruct::new("A".to_string()).value(),
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
14 |                 yield;
   |                 ----- possible yield occurs here

デフォルトでは禁止されていますが、Generatorもmoveを禁止すれば(=Unpinを実装しなければ)、安全に自己参照することも可能です。そして、Unpinを実装しないパターンのGeneratorを定義する構文も用意されています。

Generatorの定義前(move等を記述する場所)にstaticと付け加えると、Unpinを実装しないGeneratorを作成できます。

#![feature(generators, generator_trait)]

println!("start main");
{
    let generator = static || {
        println!("start future");
        for val in [
            MyStruct::new("A".to_string()).value(),
            MyStruct::new("B".to_string()).value(),
            MyStruct::new("C".to_string()).value(),
        ] {
            yield;
            println!("use {}", val);
        }
        println!("end future");
    };
    // !UnpinのGeneratorはPin::newに渡せないため、Box::pinを作成する
    let mut generator = Box::pin(generator);
    loop {
        match generator.as_mut().resume(()) {
            std::ops::GeneratorState::Yielded(()) => {},
            std::ops::GeneratorState::Complete(()) => break,
        }
    }
}
println!("end main");
start main
start future
new A       
new B       
new C       
use A       
use B
use C
drop C
drop B
drop A
end future
end main

これでUnpinと引き換えに自己参照が可能となり、通常のスコープと同じ感覚で使用することができます。ちなみに、Generatorresumeメソッドにわたす引数、戻り値のライフタイムは、Generatorよりも長い必要があるため、Generatorの開始~終了まで保持することができます。逆を言うと、Generatorよりも短いライフタイムの参照は渡す・受け取ることができません。

Generatorはまだ不安定な機能であり、仕様が今後変更される可能性があります。(私的には、このままの仕様で安定化は無いと思っています。)

余談

本筋で書きたかったことはここまでです。残りは、この記事を読んで気になりそうなことに少し触れたいと思います。

Dropトレイトの影響

ここまで話してきた変数のスコープは、実際にはもう少し短くなることが多いです。Rustのコンパイラは構文を解析して、変数が必要無くなった時点で破棄された扱いとなります。
この動作はrust2018からの仕様で、"non-lexical lifetime"と呼ばれています。この仕様に関しては

https://qiita.com/_EnumHack/items/8b6ecdeb52e69a4ff384

の記事に詳しく説明されており、おすすめです。しかし、今までの例で示した結果では、変数が不要になったあともスコープの限界まで生存しています。これは、Drop処理を実行する必要があるため、もう少し正確に言うと、自身か自身のメンバがDropトレイトを実装している場合に、生存期間がスコープの限界まで伸びます。

例えば、下記のコードは、一見&aが残ってるうちにaを書き換えているため、エラーとなるように見えますが、MyStructDropを実装していない場合は、aを書き換える前にa_refが不要になるため、コンパイルできます。Dropを実装している場合は、aを書き換えたあとにa_refdrop処理が必要になるため、&aが残っている間にaを書き換えることになり、コンパイルエラーとなります。

let mut a = MyStruct::new("A".to_string());
{
    let a_ref = MyStruct::new(&a);
    println!("use {}", a_ref.value());
    a = MyStruct::new("B".to_string());
}
error[E0506]: cannot assign to `a` because it is borrowed
 --> src\main.rs:8:8
  |
7 |        let a_ref = MyStruct::new(&a);
  |                                  -- borrow of `a` occurs here
8 |        a = MyStruct::new("B".to_string());
  |        ^ assignment to borrowed `a` occurs here
9 |     }
  |     - borrow might be used here, when `a_ref` is dropped and runs the `Drop` code for type `MyStruct`

この辺のもう少し厳密なケースについて、
https://doc.rust-jp.rs/rust-nomicon-ja/dropck.html
に載っているので気になる方がご参照ください。

このように、既存の構造体にDropトレイトを実装するのは、それだけで破壊的変更になるので、注意しましょう。

mutableな変数の書き換え

変数は、スコープによる破棄以外に、書き換えられることによっても破棄されます。Rustのコンパイラはこの破棄による生存チェックもある程度行ってくれます。

例として、以下のコードはコンパイルが通ります。

let mut a = MyStruct::new("a".to_string());
let mut a_ref = &a;
for i in 1..3 {
    a = MyStruct::new("a".to_string() + &i.to_string());
    a_ref = &a;
    println!("use {}", a_ref.value());
    a = MyStruct::new("x".to_string());
}

ループ中でa_refを使うタイミングでaが生存していることをコンパイラが判断できています。これを以下のようにa書き換え後にa_refにアクセスするとしっかりエラーとなります。

let mut a = MyStruct::new("a".to_string());
let mut a_ref = &a;
for i in 1..3 {
    a = MyStruct::new("a".to_string() + &i.to_string());
    a_ref = &a;
    println!("use {}", a_ref.value());
    a = MyStruct::new("x".to_string());
}
println!("use {}", a_ref.value());
error[E0506]: cannot assign to `a` because it is borrowed
  --> src\main.rs:11:9
   |
9  |         a_ref = &a;
   |                 -- borrow of `a` occurs here
10 |         println!("use {}", a_ref.value());
11 |         a = MyStruct::new("x".to_string());
   |         ^ assignment to borrowed `a` occurs here
12 |     }
13 |     println!("use {}", a_ref.value());
   |                        ------------- borrow later used here

このチェックは、構文を解析した結果であり、具体的な値までチェックはしていないため、下記のコードはa_refは常に有効ですが、エラーとなります。

let mut a = MyStruct::new("a".to_string());
let mut a_ref = &a;
for i in 1..3 {
    a = MyStruct::new("a".to_string() + &i.to_string());
    a_ref = &a;
    println!("use {}", a_ref.value());
    if false {
        a = MyStruct::new("x".to_string());
    }
}
println!("use {}", a_ref.value());
error[E0506]: cannot assign to `a` because it is borrowed
  --> src\main.rs:12:13
   |
9  |         a_ref = &a;
   |                 -- borrow of `a` occurs here
...
12 |             a = MyStruct::new("x".to_string());
   |             ^ assignment to borrowed `a` occurs here
...
15 |     println!("use {}", a_ref.value());
   |                        ------------- borrow later used here

ただし、Rustはこの辺の処理の改善に結構力を入れているため、将来変更される可能性があります。

一時変数のライフタイム延長

この記事で使用しているMyStructには、わざわざfn value(&self) -> &Tプロパティを作っているですが、これを使わずにフィールドに直接let value = &MyStruct::new("a".to_string()).valueのようにアクセスすると、コンパイラが気を利かせてselfのライフタイムをvalueの生存中は生きているように延長してくれます。

下記のコードは、以前のチャプターでエラーとなったコードです。

println!("start main");
let a: &String = MyStruct::new("A".to_string()).value(); // new A drop A
println!("use {}", a); // error MyStructのdrop後に、MyStruct::valueの参照を使用している!!
println!("end main");

このコードをフィールドに直接アクセスするよう変更すると、MyStructの生存期間が伸び、コンパイルは通るようになります。

println!("start main");
let a: &String = &MyStruct::new("A".to_string()).value; // new A 
println!("use {}", a);
println!("end main");
// drop A
start main
new A
use A
end main
drop A

こう見ると不思議な気持ちになりますが、実際にプログラミングしているときに注意して見てみると、このような生存期間の延長によく助けられていることに気が付くと思います。例えばlet x = &1;というコードも、生存期間の延長によって成り立っています。

もう少し詳しく知りたい方は、
https://doc.rust-lang.org/reference/destructors.html#temporary-lifetime-extension
にそこそこ詳しく載っていますのでご参照ください。

Discussion