Rust入門

トップ > Rust入門

目次

Rustとは

インストール

Linux や macOS では次の様にしてインストールできます。

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
$ export PATH="$HOME/.cargo/bin:$PATH"

下記などのコマンドがインストールされます。

rustc		// Rustコンパイラ
rustup		// Rustインストーラ・アップデータ
cargo		// Rustパッケージマネージャ
rustdoc		// ドキュメント生成ツール(rustdoc)
rustfmt		// コーディングスタイル整形ツール

Hello world

おなじみの Hello world は下記になります。拡張子は *.rs で作成します。

fn main() {
    println!("Hello, world!");
}

コンパイルには rustc コマンドを使用します。gcc が必要です。

$ rustc main.rs
$ ./main

Cargoプロジェクト

Hello world を Cargo のプロジェクトとして作成する方法を紹介しておきます。

$ cargo new hello --bin	// プロジェクトを作成する
$ cd hello

src/main.rs がひな型として作成されています。

fn main() {
    println!("Hello, world!");
}

コンパイルだけ行うには build を、コンパイルして実行するには run を行います。

$ cargo build		// コンパイルだけ行う
$ cargo run		// コンパイルして実行する

上記のコンパイルはデバッグ機能を含めた、開発用のコンパイルになっています。--release をつけることで、リリース用のコンパクトで高速なバイナリを作成することができます。

$ cargo build --release

キーワード

as		// 型変換
as		// モジュールの別名
async		// 非同期処理
await		// 非同期処理
break		// ループから抜ける
const		// 変数・定数
continue	// 次のループを続ける
crate		// ルートモジュール
dyn		// トレイトの直接利用
else		// 条件分岐
enum		// 列挙型
extern		// 外部ライブラリ
false		// 真偽値の偽
fn		// 関数
for		// 繰り返し
if		// 条件分岐
impl		// インプリメンテーション
in		// 繰り返し
let		// 変数・定数
loop		// ループ
match		// マッチ
mod		// モジュール定義
move		// クロージャーに所有権を引き渡す
mut		// 変数・定数
pub		// モジュール外公開
ref		// 参照型
return		// 関数の戻り値
Self		// 実装における自分の型
self		// 自オブジェクト
self		// 自モジュール
static		// 静的変数
struct		// 構造体
super		// 親モジュール
trait		// トレイト
true		// 真偽値の真
type		// 型エイリアス
union		// 共用体
unsafe		// 非安全コード
use		// モジュール使用
where		// 型を強要する
while		// 繰り返し

コメント(//)

// ラインコメント

/* 複数行
   コメント */

/// 3連スラッシュはrustdocによるドキュメンテーションに利用されます

データタイプ

bool			// 真偽値(true/false)
i8			// 符号付き8ビット整数
u8			// 符号無し8ビット整数
i16			// 符号付き16ビット整数
u16			// 符号無し16ビット整数
i32			// 符号付き32ビット整数
u32			// 符号無し32ビット整数
i64			// 符号付き64ビット整数
u64			// 符号無し64ビット整数
i128			// 符号付き128ビット整数
u128			// 符号無し128ビット整数
isize			// ポインタサイズと同じ符号付き整数
usize			// ポインタサイズと同じ符号無し整数
f32			// 32ビット浮動小数点数
f64			// 64ビット浮動小数点数
char			// 文字(U+0000~U+D7FF, U+E000~U+10FFFF)
str			// 文字列(&strとして使用することが多い)
(type, type, ...)	// タプル
[type; len]		// 配列
Vec<type>		// ベクタ
&type			// typeへの参照
&mut type		// typeへのミュータブルな参照
&[type]			// type型要素を持つスライス

true		// 真偽値の真(bool)
false		// 真偽値の偽(bool)
12345		// 整数
12_345_678	// カンマの代わりに_を使用して読みやすく
12345u32	// u32型の12345
0xfff		// 16進数
0o777		// 8進数
0b11001100	// 2進数
''		// 文字(char)
"..."		// 文字列(&str)
r"..."		// raw文字列
r#"..."#	// ダブルクォートをそのまま使用できる文字列
b'a'		// 1バイト文字(u8)
b"abc"		// バイト配列(&[u8])
br"..."		// rawバイト配列(&[u8])

変数・定数(let, mut, const)

変数を宣言するには let を使用しますが、Rust ではイミュータブルな(作成後に変更することができない)オブジェクトとして生成されます。

let n = 0;

変更可能な(ミュータブルな)変数を宣言するには mut を使用する必要があります。

let mut n = 0;

定数を定義するには const を用います。

const MAX_POINTS: u32 = 100_000;

型変換(as)

暗黙の型変換は行ってくれません。as を用いて明示的に型変換します。

let x: i32 = 123;
let y: i64 = x as i64;

演算子(+ - ...)

-expr			// 負数
expr + expr		// 加算
expr - expr		// 減算
expr * expr		// 乗算
expr / expr		// 除算
expr % expr		// 剰余
expr & expr		// 論理積(AND)
expr | expr		// 論理和(OR)
expr ^ expr		// 排他的論理和(XOR)
expr << expr		// ビット左シフト
expr >> expr		// ビット右シフト
var = expr		// 代入
var += expr		// var = var + expr と同義
var -= expr		// var = var - expr と同義
var *= expr		// var = var * expr と同義
var /= expr		// var = var / expr と同義
var %= expr		// var = var % expr と同義
var &= expr		// var = var & expr と同義
var |= expr		// var = var | expr と同義
var ^= expr		// var = var ^ expr と同義
var <<= expr		// var = var << expr と同義
var >>= expr		// var = var >> expr と同義
expr == expr		// 比較:等しい
expr != expr		// 比較:等しくない
expr < expr		// 比較:より大きい
expr <= expr		// 比較:以上
expr > expr		// 比較:より小さい
expr >= expr		// 比較:以下
expr && expr		// かつ(AND)
expr || expr		// または(OR)
!expr			// 否定(NOT)

fn(...) -> type	// 関数の型定義
expr;			// 行の終わり
'label			// ラベル
expr..expr		// 範囲
macro!(...)		// マクロ呼び出し
macro![...]		// マクロ呼び出し
macro!{...}		// マクロ呼び出し
[type; len]		// 配列


pat => expr
expr?
&expr
&type
*expr
*type
trait + trait
expr , expr
expr.ident
expr..=expr
..expr
variant(..)
expr...expr
ident: expr
ident @
pat | pat

構造体(struct)

struct構造体 を定義します。他言語のクラスに相当する機能は Rust では構造体で定義します。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 100, y: 200 };
    println!("{} {}", p.x, p.y);
}

共用体(union)

union共用体 を定義します。C言語と連携する際などに使用されます。構造体では x: i32, y: i32 という合計 8バイトの領域が確保されますが、共用体の場合は f1 と f2 は同じメモリを共用します。利用者は、f1 か f2 のいずれかのアドレスを使用することになります。共用体からの値読み出しは安全(スレッドセーフ)ではないため、unsafe ブロックの中で使用する必要があります。

union MyUnion {
    f1: u32,
    f2: u32,
}

fn main() {
    let u = MyUnion { f1: 123 };
    unsafe {
        println!("{}", u.f1);
        println!("{}", u.f2);	// メモリを共用しているのでこちらも123と表示される
    }
}

列挙型(enum)

enum Color {
    Red,
    Green,
    Blue,
}
let color = Color::Red;

タプル(tup)

タプルは下記の様に宣言・利用します。型の異なる要素を含むことができます。要素数は固定です。インデックスに変数を使用することができません。

let tup = (10, "20", 30);
println!("{} {} {}", tup.0, tup.1, tup.2);

配列(array)

配列は下記の様に宣言・利用します。型の異なる要素を含むことはできません。要素数は固定です。インデックスに変数を使用することができます。

let arr = [10, 20, 30];
println!("{} {} {}", arr[0], arr[1], arr[2]);

for v in &arr {
    println!("{}", v);
}

ベクタ(vec)

ベクタは下記の様に宣言・利用します。型の異なる要素を含むことはできません。要素数は可変です。インデックスに変数を使用することができます。

let mut vect = vec![10, 20, 30];
vect.push(40);
println!("{} {} {} {}", vect[0], vect[1], vect[2], vect[3]);

for v in &vect {
    println!("{}", v);
}

ハッシュマップ(HashMap)

ハッシュマップでは文字列を添字に使用することができます。他の言語で連想配列と呼ばれるものです。

use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("x", 10);
map.insert("y", 20);
map.insert("z", 30);
println!("{} {} {}", map["x"], map["y"], map["z"]);

for (k, v) in &map {
    println!("{} {}", k, v);
}

文字列(&str, String)

str は文字列を示しますが、Rust の基本的な文字列型としては &str を使用します。変数に文字列を代入しているというより、スタック領域に確保した文字列のメモリへのポインタを変更している感じになります。

let mut name: &str = "Yamada";
name = "Tanaka";

ライブラリとして提供される文字列型に String があります。こちらは、文字列の連結などが可能です。

// 文字列を初期化する
let mut name = String::from("Yamada");
// 別の文字列を設定する
name = "Tanaka".to_string();
// 文字列に追加する
name.push_str(" Taro");

ヒープ領域(Box)

関数内で使用する i32&str などの変数は通常スタック領域にメモリを確保します。スタックは関数が呼ばれると積み重なっていき、関数が終わると解放されます。これに対し、String や Vec などの型はヒープ領域にメモリを確保します。ヒープ領域は関数が終わっても存在することができます。ヒープ領域のメモリを確保する汎用的な型として Box<T> が定義されています。ヒープ領域のメモリは、所有者 が居なくなった時点で解放されます。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p: Box<Point> = Box::new(Point { x: 100, y: 200 });
    println!("{} {}", p.x, p.y);
}

Drop トレイト を実装すると、メモリ解放時に後処理関数(俗にいうデストラクタ)を呼び出すことができます。

impl Drop for Point {
    fn drop(&mut self) {	// Pointが解放される際に呼び出される
        println!("Bye!");
    }
}

スライス(&var[n..m])

&var[n..m] は、配列 var の中から n番目から m番目の手前までの要素を参照するスライスを返します。n を省略すると先頭から、m を省略すると最後までを参照します。

let s = String::from("ABCDEFGH");
let s1 = &s[0..3];		// 0番目から3番目の手前までのスライス("ABC")
let s2 = &s[3..6];		// 3番目から6番目の手前までのスライス("DEF")
println!("{} {}", s1, s2);	// => ABC DEF

let a = [10, 20, 30, 40, 50, 60, 70, 80];
let a1 = &a[0..3];		// 0番目から3番目の手前までのスライス[10, 20, 30]
let a2 = &a[3..6];		// 0番目から3番目の手前までのスライス[40, 50, 60]
println!("{:?} {:?}", a1, a2);// => [10, 20, 30] [40, 50, 60]

関数(fn)

関数は下記の様に定義します。return は戻り値を呼び出し元に返します。

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

return されない場合は、最後の式が戻り値として返されます。最後のセミコロン ; は記述してはなりません。

fn add(x: i32, y: i32) -> i32 {
    x + y	// セミコロン(;)無し
}

クロージャー(|...|{...})

クロージャー は他の言語で言うところの無名関数やラムダ式に似ています。下記の例では x を受け取り、その二乗を返却するクロージャーを square 変数に代入し、使用しています。

let square = | x: i32 | {
    x * x
};
println!("{}", square(9));

move は、クロージャー内で参照するクロージャー外変数が存在する場合、その所有権をクロージャーに移動させることを宣言します。

let msg = String::from("Hello");	// クロージャー外変数msg
let func = move || {			// 所有権をクロージャーに移動すること宣言
    println!("{}", msg);		// 参照したクロージャー外変数の所有権はクロージャーに移動
};					// クロージャー終了時に所有者が不在となり解放される
func();					// クロージャーを呼び出す
// println!("{}", msg);		// 解放済領域を参照しようとするのでエラー

マクロ(macro_rules!)

// マクロを定義する
macro_rules! log {
    ($x:expr) => { println!("{}", $x); }
}

fn main() {
    // マクロ名! でマクロを呼び出す
    log!("ABC...");
}

条件分岐(if)

if n == 1 {
    println!("One");
} else if n == 2 {
    println!("Two");
} else {
    println!("Other");
}

if文も式なので、次のようにも書けます。

let s = if n == 1 { "OK!" } else { "NG!" };

繰り返し(while)

let mut n = 0;
while n < 10 {
    n += 1;
}

繰り返し(for)

for i in 0..10 {
    println!("{}", i);
}

ループ(loop)

let mut n = 0;
loop {
    n += 1;
    if n == 10 {
        break;
    }
}

ループ制御(break, continue)

break はループを抜けます。continue は次のループを繰り返します。

let mut n = 0;
loop {
    n += 1;
    if n == 2 {
        continue;
    }
    if n == 8 {
        break;
    }
    println!("{}", n);
}

マッチ(match)

let x = 2;
match x {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("More"),
}

インプリメンテーション(impl)

Rust ではクラスはサポートされていませんが、impl によって構造体にメソッドを加えることができます。self は自オブジェクトを示します。

struct Rect { width: u32, height: u32 }

impl Rect {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let r = Rect { width: 200, height: 300 };
    println!("{}", r.area());
}

トレイト(trait)

trait は特質の意味で、構造体が実装すべきメソッドを定義します。他言語の インタフェース(interface) に似ています。例えば、std::fmt::Display トレイトを実装した構造体は println!() の "{}" で、std::fmt::Debug トレイトを実装した構造体は "{:?}" で書き出すことが可能です。

struct Rect { width: u32, height: u32 }

trait Printable { fn print(&self); }
impl Printable for Rect {
    fn print(&self) {
        println!("width:{}, height:{}", self.width, self.height)
    }
}

fn main() {
    let r = Rect { width: 200, height: 300 };
    r.print();
}

上記の例では u64 を扱う Rect も用意するには impl Printable for RectU32 { ... } と impl Printable for RectU64 { ... } の二つを実装する必要がありますが、下記の様にして、任意の型を持つ実装を行うことができます。where はその型が、指定したトレイトを実装している時のみ利用可能であることを示します。

struct Rect<T> { width: T, height: T, }

trait Printable { fn print(&self); }
impl<T> Printable for Rect<T> where T: std::fmt::Display {
    fn print(self: &Rect<T>) {
        println!("{}x{}", self.width, self.height);
    }
}

fn main() {
    let r1: Rect<i32> = Rect{ width: 100, height: 200 };
    let r2: Rect<i64> = Rect{ width: 100, height: 200 };
    r1.print();
    r2.print();
}

<T> など、通常型を指定する箇所に、型(type)ではなくトレイト(trait)を指定する場合は、トレイトであることを明示するために dyn を指定するようになりました。

use std::boxed::Box;

struct Dog {}
struct Cat {}
trait Animal { fn cry(&self); }
impl Animal for Dog { fn cry(&self) { println!("Bow-wow"); } }
impl Animal for Cat { fn cry(&self) { println!("Miaow"); } }

fn get_animal(animal_type: &str) -> Box<dyn Animal> {
    if animal_type == "dog" {
        return Box::new(Dog {});
    } else {
        return Box::new(Cat {});
    }
}

fn main() {
    get_animal("dog").cry();
    get_animal("cat").cry();
}

イテレータ(Iterator)

Iteratorトレイトを実装するオブジェクトを イテレーター と呼びます。イテレータは for で利用することができます。Iterator トレイトでは、next() により次のオブジェクトを返却し、最後に達すると None を返却します。Selfimpl における自分自身の型を示します。

struct Counter {
    max: u32,
    count: u32,
}

impl Counter {
    fn new(max: u32) -> Counter {
        Counter { max: max, count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count < self.max {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new(10);
    for c in counter {
        println!("{}", c);
    }
}

マルチスレッド(thread)

Rust はマルチスレッドに強い言語です。スレッドは次のように記述します。

use std::thread;
use std::time::Duration;

fn main() {
    // スレッドを起動する
    // 引数にクロージャー(ラムダ関数)を指定
    let th = thread::spawn(|| {
        for _i in 1..10 {
            println!("A");
            thread::sleep(Duration::from_millis(100));
        }
    });
    th.join().unwrap();
    println!("Finished");
}

スレッドからスレッド外の変数を参照するには、move によって変数の所有権をスレッドに引き渡すことを明示する必要があります。

fn main() {
    let str = String::from("ABC");
    let th = thread::spawn(move || {	// 所有権を引き渡すことを明示
        for _i in 1..10 {
            println!("{}", str);	// strの所有権を得る
            thread::sleep(Duration::from_millis(100));
        }
    });
    th.join().unwrap();
    println!("Finished");
    // println!("{}", str);		// 所有権移動済のためエラー
}

非同期関数(async, await)

async, await を用いて非同期関数を利用することができます。下記は、Rustの説明書 に記載されているサンプルで、歌を歌いながらダンスができるように書かれているのですが、どうも、歌い終わってからでないとダンスしないみたいで、もう少し分かりやすいシンプルなサンプルを提供できる方はお願いします。

use futures::executor::block_on;

struct Song {
    lyric: String,
}

async fn learn_and_sing() {
    let song = learn_song().await;
    sing_song(song).await;
}

async fn learn_song() -> Song {
    let song = Song { lyric: String::from("La la la...") };
    println!("Learned song");
    return song;
}

async fn sing_song(song: Song) {
    println!("{}", song.lyric);
}

async fn dance() {
    println!("Dance");
}

async fn async_main() {
    let f1 = learn_and_sing();	// 歌を習って歌う
    let f2 = dance();			// ダンスする
    futures::join!(f1, f2);
}

fn main() {
    block_on(async_main());
}

クレート(crate)

クレート は聞きなれない言葉ですが「木箱」の意味で、他言語で言うところのパッケージ、モジュール、ライブラリを意味します(私はカープのクレートさんを思い出しますが)。例えばランダム値を生成する rand クレートを使用するには次のようにします。

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    for _i in 1..10 {
        println!("{}", rng.gen_range(1, 101));
    }
}

上記だけだと E0432(unresolved import) エラーが出てしまいますので、Cargo プロジェクトで作成した Cargo.toml に次の1行を追記します。追記後、cargo build または cargo run すると必要なクレートが自動的にダウンロードされてコンパイルされます。

[dependencies]
rand = "0.7"

クレートの最新バージョンは cargo search で検索することができます。

$ cargo search rand
  :
rand = "0.7.3"

モジュール(mod, pub, use, as)

プログラムを複数のファイルに分割するにはモジュールを用います。mod はモジュールを使用することを宣言します。pub はモジュール外からもその名前にアクセスするために必要です。

./src/main.rs
mod foo;

fn main() {
    foo::foo_func();
}
./src/foo.rs
pub fn foo_func() {
    println!("Foo!");
}

階層的に分割するには次のようにします。

./src/main.rs
mod foo;

fn main() {
    foo::bar::bar_func();
}
./src/foo.rs
pub mod bar;
./src/foo/bar.rs
pub fn bar_func() {
    println!("Bar!");
}

use により使用するモジュールのパスを省略することができます。

./src/main.rs
use foo::bar;	// foo::bar を bar として参照可能になる
fn main() {
    bar::bar_func();
}

as によりモジュール名に別名をつけることもできます。名前の重複を回避することができます。

./src/main.rs
use foo::bar as bbaarr;
fn main() {
    bbaarr::bar_func();
}

* を指定すると子要素をすべて使用できるようになります。

./src/main.rs
use foo::bar::*;
fn main() {
    bar_func();
}

crate はルートモジュール(トップモジュール)を示します。super は親モジュールを示します。self は自モジュールを示します。パス名で言うところのルートディレクトリ(/)や、親ディレクトリ(..)や、カレントディレクトリ(.) に相当します。

./src/foo/bar.rs
pub fn bar_func() {
    crate::foo::bar::bar_hello();	// ルートモジュールのfooのbarのbar_hello()
    super::bar::bar_hello();		// 親モジュールのbarのbar_hello()
    self::bar_hello();			// 自モジュールのbar_hello()
}

pub fn bar_hello() {
    println!("Hello!");
}

参照型(&, *)

& はその変数が指し示す値への参照を示します。参照はポインタとも呼ばれます。* は参照が指し示す値を示します。

let a = 123;
let p = &a;		// 123という値が格納された領域への参照をpに代入する
println!("{}", *p);	// pが参照する領域の値(123)を出力する

ref を使用して次のようにも書けます。

let a = 123;
let ref p = a;
println!("{}", *p);	// => 123

ミュータブルな参照を用いることで、参照先の値を変更することが可能となります。

let mut a = 123;	// ミュータブルな変数aを定義
let p = &mut a;	// ミュータブルな参照pを定義
*p = 456;		// 参照先の値を456に書き換える
println!("{}", a);	// => 456

所有権・移動・参照・借用

JavaJavaScript などでは、ヒープ領域に確保したメモリは、誰からも参照されなくなった後にガベージコレクションによって解放されますが、Rust では、ただひとつの変数がヒープ上のメモリの 所有権(ownership) を持ち、所有者がスコープから消えた時点でヒープ領域も開放されます。

fn func1() {
    let name = String::from("ABC");	// nameがStringの所有権を持つ
    println!("{}", name);
}					// nameがスコープアウトしたので解放される

関数に変数を渡すと所有権が 移動(move) してしまいます。

fn func1() {
    let name = String::from("ABC");
    println!("{}", name);
    func2(name);			// ここで所有権がfunc2()のnameに移動してしまう
    println!("{}", name);		// func2()終了時に開放済の領域を参照しているのでエラー
}

fn func2(name: String) {		// func1()から所有権を奪い取る
    println!("{}", name);
}					// この時点でヒープ領域が解放されてしまう

func2() から戻り値として所有権を返してもらうこともできます。

fn func1() {
    let mut name = String::from("ABC");
    println!("{}", name);
    name = func2(name);		// 所有権を渡した後、返却してもらう
    println!("{}", name);
}

fn func2(name: String) -> String {
    println!("{}", name);
    name				// 所有権を返却する
}

&参照(references) を渡すことで、所有権を渡さないまま関数を呼び出すこともできます。これを 借用(borrowing) とも呼びます。

fn func1() {
    let name = String::from("ABC");
    println!("{}", name);
    func2(&name);			// 参照のみを渡して所有権は渡さない
    println!("{}", name);		// 所有権が残っているので参照可能
}

fn func2(name: &String) {		// func1()から参照のみを借用する
    println!("{}", name);
}					// 参照のみなのでヒープ領域は解放されない

関数内で他の変数に渡しただけでも所有権の移動が発生します。

fn func1() {
    let s1 = String::from("ABC");
    {
        let s2 = s1;		// 所有権がs1からs2に移動
        println!("{}", s2);
    }				// 所有者が居なくなるので解放される
    println!("{}", s1);	// エラー
}

型エイリアス(type)

type を用いて型に 型エイリアス という別名をつけることができます。ただし、異なる別名間の比較や代入でワーニングやエラーを出すことは無いようです。

type Meter = u32;
type Millimeter = u32;
let m: Meter = 12;
let mm: Millimeter = 12000;
println!("{} {}", m, mm);

型を調べる(typeof)

typeof というキーワードが予約されているので、今後実装されるのでしょうが、現状の Rust で変数の型を調べるには下記の様にします。

fn main() {
    let x = 123;
    println!("{}", type_of(x));
}

fn type_of<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

外部関数の呼び出し(extern)

extern によりC言語ライブラリなど他の言語のライブラリを呼び出すことができます。呼び出しは unsafe であることを意識する必要があります。

extern "C" {
    fn abs(x: i32) -> i32;		// C言語のabc()ライブラリを定義
}

fn main() {
    unsafe {
        println!("{}", abs(-123));
    }
}

静的変数(static)

static は静的変数を定義します。値は変動してもよいのですが、変数の位置が固定で複数のスレッドから共通に参照することができます。

static COUNTER;

ただし、単に mut をつけてミュータブル(変更可能)にするとスレッドセーフではない危険な変更となり、unsafe ブロック内でしか読み書きできなくなります。

static mut COUNTER: u32 = 0;

fn main() {
    unsafe {
        println!("{}", COUNTER);
        COUNTER += 1;
        println!("{}", COUNTER);
    }
}

安全に読み書きするには、下記の様に、アトミック性を保証する参照・変更を用いることになります。

use std::sync::atomic::{self, AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn count_up() { COUNTER.fetch_add(1, atomic::Ordering::SeqCst); }
fn get_count() -> u32 { return COUNTER.load(Ordering::SeqCst); }

fn main() {
    println!("{}", get_count());
    count_up();
    println!("{}", get_count());
}

約30年くらい前のプロジェクトで、下記のたった1行のC言語プログラムのバグに悩まされたことを思い出します。

/* スレッドセーフではないので、プログラム的には1行だけど、
 * メモリから読み出す、カウントアップする、メモリに書き戻すという動作であり、
 * 複数スレッドが同時に行うと、カウンターが壊れてしまう。*/
counter++;

Copyright (C) 2020 杜甫々
初版:2020年5月24日 最終更新:2020年6月24日
http://www.tohoho-web.com/ex/rust.html