Rustで可変のグローバル変数を使おうとすると意外にややこしい。Cの感覚で導入しようとして苦戦したので、簡単にまとめておく。
ここではシングルスレッド向けのものだけを考え、マルチスレッド向けの話はしない。調べるにあたり、こちらの記事やこちらの記事を参考にさせて頂いた。リンク先のほうが説明も詳しいし、マルチスレッドについても言及されているので、より進んだ理解を得たい人はそちらを参照してほしい。
static mutを使う方法(非推奨)
static mut NUMS: Vec<i32> = Vec::new();
fn main() {
unsafe {
NUMS.push(1);
NUMS.push(2);
assert_eq!(Some(&1), NUMS.get(0));
assert_eq!(Some(&2), NUMS.get(1));
}
}
まず一番最初に思いつくのがこの方法だと思う。
この場合、データを操作したりするにはunsafeブロックで囲う必要がある。unsafeで囲わないと、mutable statics can be mutated by multiple threads
というメッセージ付きでエラーになる。マルチスレッドでの安全性が保証できないよ、ということだ。
じゃあシングルスレッドでしか使わないなら問題ないのだろうか、と思わなくもないが、少なくとも自分にはそのリスクを明確に説明できないし、コンパイラによるチェックの恩恵も得られなくなってしまうので、極力避けるべきだろう。
RefCellを使う方法
use std::cell::RefCell;
thread_local!(
static NUMS: RefCell<Vec<i32>> = {
let v: Vec<i32> = Vec::new();
RefCell::new(v)
}
);
fn main() {
NUMS.with(|v| {
v.borrow_mut().push(1);
v.borrow_mut().push(2);
assert_eq!(Some(&1), v.borrow().get(0));
assert_eq!(Some(&2), v.borrow().get(1));
})
}
この方法がCとかのグローバル変数の感覚に一番近い気がする。
thread_local!の中で値を宣言し、操作はwithの中で行うことで、unsafeで囲うことなくグローバルなデータを扱うことができる。操作するときにborrowもしくはborrow_mutが必要なことに注意。
thread_local!という名前からもなんとなく察せられるように、スレッドごとに値を保持するため、マルチスレッド環境の場合には別の考慮が必要になる。
RefCellとRcを使う方法
use std::cell::RefCell;
use std::rc::Rc;
thread_local!(
static NUMS: Rc<RefCell<Vec<i32>>> = {
let v: Vec<i32> = Vec::new();
Rc::new(RefCell::new(v))
}
);
fn main() {
let v = NUMS.with(|v| v.clone());
v.borrow_mut().push(1);
v.borrow_mut().push(2);
assert_eq!(Some(&1), v.borrow().get(0));
assert_eq!(Some(&2), v.borrow().get(1));
}
RefCellだけを使った場合、withの中に処理を書かなければいけないのでネストが深くなったり、少し見づらいと感じるかもしれない。そんなときは、Rcを使うことでwithの外に変数を持ち出し、ネストを1段階減らすことができる。一度持ち出してしまえば、あとはwithの中で書いたのと同じように操作できる。
ただし、Rcは1つのデータを複数で所有するようなときに使われるため、Rcの使われ方としては少し直感的でないのかもしれないし、withで囲われていたほうがグローバルな変数を使っているという目印にもなるかもしれないので、このあたりは好みなんだろうと思う。
あと、詳しいことは検証していないが、Rcを使う分のオーバーヘッドなども発生しているかもしれない。
まとめ
- static mutは危険なので極力使わないほうがいい。
- thread_local!とRefCellを組み合わせることで、安全に扱うことができる。
- さらにRcを組み合わせると、少しすっきりと書くことができる。