Rustのstaticいろいろ
概要: Rustのstaticの亜種をいろいろ挙げる。
ここでは以下のように分類します。
- 変数
- ローカル束縛 (引数、
let
,match
,for
,if let
,while let
) - 静的変数/定数
- コンパイル時定数 (
const
)- アイテム位置の定数
- トレイトの定数
- 実装の定数
- 静的変数 (
static
)- 非スコープ借用される静的変数
- 組み込み
static
lazy_static!
- futures-0.2
task_local!
- 組み込み
- スコープ借用される静的変数
thread_local!
scoped_thread_local!
- futures-0.1
task_local!
- 非スコープ借用される静的変数
- コンパイル時定数 (
- ローカル束縛 (引数、
目次
const
とstatic
の違い- スコープ借用と非スコープ借用
- 組み込み
static
lazy_static!
thread_local!
scoped_thread_local!
- futures-0.1
task_local!
- futures-0.2
task_local!
const
と static
の違い
const
はコンパイル時に決まる値そのものを定義します。この値はコンパイル時定数が必要な他の場所で使えます。static
は(コンパイル時に決まる値が入っている)場所を定義します。値がコンパイル時に決まるのは初期化の都合上にすぎず、それ以上の意味はありません。
たとえば、次のようなコードを考えます。
use std::sync::atomic::{self, AtomicUsize}; const X: AtomicUsize = AtomicUsize::new(0); static Y: AtomicUsize = AtomicUsize::new(0); fn main() { // どちらもX, Yを5回インクリメントして表示している for _ in 0..5 { // 0が5回表示される println!("X = {}", X.fetch_add(1, atomic::Ordering::SeqCst)); } for _ in 0..5 { // ちゃんとインクリメントされる println!("Y = {}", Y.fetch_add(1, atomic::Ordering::SeqCst)); } }
これを実行すると以下のように出力されます。
X = 0 X = 0 X = 0 X = 0 X = 0 Y = 0 Y = 1 Y = 2 Y = 3 Y = 4
fetch_add
は &self
をとるので、実際には &X
と &Y
が計算されています。この &
の動作は以下のように規定されています。
X
が左辺値なら、そのアドレスをとる。X
が右辺値なら、匿名のローカル変数にX
を代入して、そのアドレスをとる。- ただし、
X
が右辺値の場合であっても、以下の条件が満たされる場合は、一時変数ではなく匿名の静的領域にX
を代入して、そのアドレスをとる。 (RFC1414 rvalue static promotion)&mut
ではなく&
参照をとろうとしている。X
はコンパイル時に決定される。X
は内部可変性をもたない。X
の計算中に内部可変性をもつ関数が使われていない。
ここで X
は内部可変性をもつ定数右辺値ですから、2番目の規則が適用されて、一時変数の値が使われます。その結果毎回新しい AtomicUsize
が生成されるため、毎回0が出力されます。
このように、状態を管理する場所を作りたい場合は、 static
を使います。そのため static
が必要なときは 内部可変性をともなうことがほとんどです。
一方、コンパイル時に決まる値そのものを定義したい場合は const
が適しています。たとえば以下のような場合です。
const X: usize = 0; // constは他のconst/staticの初期化に利用できる static Y: usize = X; fn main() { // constは長さのようなコンパイル時定数に使える let a = [0; X]; }
以前は、static
ではなく const
にしてしまうと借用が常に一時的になってしまって煩わしいという問題がありましたが、rvalue static promotionによりこの問題は改善しています。そのため、「値を定義するときは const
」「場所を定義するときは static
」という原則に近い運用であまり困らないと思います。
スコープ借用と非スコープ借用
Rustの組み込み static
以外にも、 static
に準ずる機能を提供するマクロはいくつかあります。これらは使われ方で「スコープ借用」型と「非スコープ借用」型に分類できます。(※本稿独自の分類)
スコープ借用では、借用ごとにクロージャのコールバックが必要になります。コールバックの終了時に借用の終了が強制されます。
X.with(|x| { ... })
いっぽう、非スコープ借用では &X
や X.get(..)
のような方法で借用をとることができます。
組み込み static
言語に組み込みの static
です。
use std::sync::atomic::{self, AtomicUsize}; static COUNTER: AtomicUsize = AtomicUsize::new(0); fn main() { // インクリメント COUNTER.fetch_add(1, atomic::Ordering::SeqCst); }
ポイント
- スコープ: プロセス全体で共有される。
- 要請:
T: Sync + 'static
- 初期化: コンパイル時定数式でしか初期化できない。 (より高度な初期化には
lazy_static!
を使う) - 借用:
&X
で借用できる。 - drop: dropされない。
組み込み static
の亜種
組み込みの static
は原則として単独で使いますが、以下のような亜種があります。
static mut
:&mut X
での借用が可能になるが、unsafe。排他性を自分で確認する必要がある。static
から借用したものを無理矢理&mut X
として使うとUBになるのでstatic mut
にする必要がある。 たいていの場合はstatic
+ 内部可変性で事足りる。#[thread_local] static
: スレッドローカルになる。不安定機能なので#![feature(thread_local)]
が必要。以下の問題点がある。thread_local!
と違って動かないプラットフォームがある。- dropされない。
extern { static }
: FFIで使われる。
lazy_static!
初回使用時に初期化される静的変数のためのマクロです。
Mutex::new()
等がコンパイル時定数でないために必要になることが多いです。これは Mutex::new()
が内部で Box::new()
を必要としているなどの事情があります。軽量同期ライブラリの parking_lot を使って解決する手もあります。
#[macro_use] extern crate lazy_static; use std::sync::Mutex; lazy_static! { // 文法上の特徴: "static" ではなく "static ref" を使う static ref COUNTER: Mutex<i64> = Mutex::new(0); } fn main() { // インクリメント *COUNTER.lock().unwrap() += 1; }
ポイント
- スコープ: プロセス全体で共有される。
- 要請:
T: Sync + 'static
- 初期化: 初回使用時に初期化するので、コンパイル時定数の必要はない。明示的な初期化も可能。
- 借用:
&X
で借用できる。(Derefが実装されている) - drop: dropされない。
thread_local!
thread_local!
は標準ライブラリマクロで、スレッドローカル変数を作ります。
use std::cell::RefCell; thread_local! { static COUNTER: RefCell<i32> = RefCell::new(0); } fn main() { COUNTER.with(|counter| { // インクリメント *counter.borrow_mut() += 1; }) }
ポイント
- スコープ: スレッドごと
- 要請:
T: 'static
- 初期化: スレッドごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
- 借用:
with
でスコープを作って借用する必要がある。 - drop: 通常はスレッド終了時にdropされるが、保証されているわけではない。
なお、 thread_localクレイトはスレッドごとの静的変数ではなく、スレッドごと×オブジェクトごとの静的領域を提供します。
scoped_thread_local!
scoped_thread_local!
は scoped-tlsクレイトが提供するマクロで、参照をそのまま保管できる特殊なストレージを提供します。何らかのコンテキストを暗黙的に引き回す必要がある場合に便利です。 (基本的には明示的に引き回したほうがいいと思いますが)
#[macro_use] extern crate scoped_tls; scoped_thread_local!( static CONTEXT: i32 ); fn main() { let context = 42; CONTEXT.set(&context, || { CONTEXT.with(|context| { println!("context = {}", context); }) }) }
ポイント
- スコープ: スレッドごと
- 要請:
T: 'static
(設計上はT: ?Sized
になりそうだけどそれは対応していない様子) ただし、実際にはT
ではなく&T
を保管する。 - 初期化:
set
が呼び出されている間だけ値が入っていて、それ以外の期間にwith
を呼ぶとパニックになる。 - 借用:
with
でスコープを作って借用する必要がある。 - drop: 参照渡しなのでdropは関係ない。
futures-0.1 task_local!
futuresの task_local!
はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.1のそれはスコープ借用で、 thread_local!
と似た使い心地です。
#[macro_use] extern crate futures; extern crate tokio; use tokio::prelude::*; use std::cell::RefCell; task_local!( static COUNTER: RefCell<i32> = RefCell::new(0) ); fn main() { tokio::run(future::lazy(|| { COUNTER.with(|counter| { // インクリメント *counter.borrow_mut() += 1; }); Ok(()) })); }
- スコープ: タスクごと
- 要請:
T: Send + 'static
(宣言時はSend
は要求されない) - 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
- 借用:
with
でスコープを作って借用する必要がある。 - drop: タスク終了時にdropされる。
futures-0.2 task_local!
futuresの task_local!
はタスク(軽量スレッド) ごとに異なる記憶領域を擬似的に提供しますが、このインターフェースはfutures-0.1とfutures-0.2で異なります。futures-0.2のそれは非スコープ借用で、面白い構成になっています。contextを引き回しているおかげで、&mut
な借用ができるというのも面白い点といえます。
#[macro_use] extern crate futures; use futures::prelude::*; use futures::executor::ThreadPool; task_local!( static COUNTER: i32 = 0 ); fn main() { ThreadPool::new() .unwrap() .run(futures::future::lazy(|context| { // インクリメント *COUNTER.get_mut(context) += 1; Ok(()) as Result<(), futures::never::Never> })) .unwrap(); }
- スコープ: タスクごと
- 要請:
T: Send + 'static
(宣言時はSend
は要求されない) - 初期化: タスクごとの初回使用時に初期化するので、コンパイル時定数の必要はない。
- 借用: contextのライフタイムだけ借用できるが、同時に1つのtask-local dataしか借用できない。
- drop: タスク終了時にdropされる。
なお事実上のfutures-0.3にあたるRFC2408の現時点でのドラフトでは、contextからLocalMapを削除することが提案されています。したがってfutures-0.3のtask-local storageはfutures-0.1方式に戻るのではないかと思います。
まとめ
const
は値、static
は場所を定義する。static
っぽいことをするマクロは色々あり、使い分けると幸せになれる。