Rust の基礎知識
●Box<T>
- Rust で「ヒープ領域」からメモリを取得するときは Box<T> を使う
- Box<T> は C++ (C++11) の「スマートポインタ」の動作に近い
- Box<T> がスコープから外れたとき、取得したメモリは自動的に解放される
- メモリの取得はメソッド new() で行う
Box::new(データ) -> Box<データ型>
格納されているデータは * で取得できる (デリファレンス)
トレイト Drop のメソッド drop() を実装すると、メモリを解放するときに処理を実行することができる
リスト : Box の簡単な使用例
// 点
struct Point { x: f64, y: f64 }
// Point を廃棄したときにメッセージを表示する
impl Drop for Point {
fn drop(&mut self) {
println!("Drop Point!");
}
}
fn main() {
let a = Box::new(10);
println!("{}", *a); // * でデリファレンス
// a でも表示できる
let b = a; // move
// println!("{}", a); コンパイルエラー
println!("{}", b);
{
let p1 = Box::new(Point {x: 0.0, y: 0.0});
println!("{}, {}", p1.x, p1.y); // (*p1).x, (*p1).y と同じ
let Point {x: c, y: d} = *p1; // パターンマッチ
println!("{}, {}", c, d);
}
// ここで p1 が廃棄される
println!("----- end ------")
// main() が終了したら b が廃棄される
}
10
10
0, 0
0, 0
Drop Point!
----- end ------
リスト : 簡単な連結リスト
#[derive(Debug)]
enum List {
Nil,
Cons(i32, Box<List>)
}
impl Drop for List {
fn drop(&mut self) {
println!("Drop List {:?}", self);
}
}
impl List {
// 空リストを返す
fn new() -> List {
List::Nil
}
// 連結リストの先頭にデータを追加する
fn cons(self, x: i32) -> List {
List::Cons(x, Box::new(self))
}
}
fn main() {
let a = List::new();
let b = a.cons(1); // a は move
println!("{:?}", b);
{
let c = b.cons(2); // b は move
println!("{:?}", c);
}
// ここで c がすべて廃棄される
println!("----- end -----");
}
Cons(1, Nil)
Cons(2, Cons(1, Nil))
Drop List Cons(2, Cons(1, Nil))
Drop List Cons(1, Nil)
Drop List Nil
----- end -----
- Rust の場合、連結リストは簡単に定義できるようにみえるが、コンパイルすると warning が表示される
- 連結リストまみれでRustを学ぶ によると、これは「だめな実装」とのこと
- また、この方法では所有権が移動するので、関数型言語のようにリストを共有して使うこともできない
- M.Hiroi は Rust の「所有権」を甘くみていたようだ (難しい)
●トレイトオブジェクト
- トレイトオブジェクトはプログラムの実行時にメソッドを選択する仕組み (動的ディスパッチ)
- C++ の仮想関数によく似ている
- トレイトを Foo とすると、トレイトオブジェクトのデータ型は &dyn Foo, &mut dyn Foo, Box<dyn Foo> (Rust 2021)
- トレイトオブジェクトはトレイトを実装したデータ型を「型キャスト」するか「型強制」することで生成する
リスト : トレイトオブジェクトの簡単な例題
trait Foo {
fn method(&self);
}
struct Bar;
struct Baz;
impl Foo for Bar {
fn method(&self) { println!("Bar method"); }
}
impl Foo for Baz {
fn method(&self) { println!("Baz method"); }
}
fn call_method(func: &dyn Foo) {
func.method(); // 動的ディスパッチ
}
fn call_method_box(func: Box<dyn Foo>) {
func.method();
}
fn main() {
let x = Bar;
let y = Baz;
call_method(&x as &dyn Foo);
call_method(&y);
call_method_box(Box::new(x));
call_method_box(Box::new(y));
}
Bar method
Baz method
Bar method
Baz method
リスト : 図形オブジェクト
// 図形のトレイト
trait Figure {
fn area(&self) -> f64;
fn kind(&self) -> &str;
// デフォルトメソッド
fn print(&self) {
println!("{}: area = {:.3}", self.kind(), self.area());
}
}
// 三角形
struct Triangle {
altitude: f64, base_line: f64
}
impl Triangle {
fn new(a: f64, b: f64) -> Triangle {
Triangle { altitude: a, base_line: b }
}
}
impl Figure for Triangle {
fn area(&self) -> f64 {
self.altitude * self.base_line / 2.0
}
fn kind(&self) -> &str {
"Triangle"
}
}
// 四角形
struct Rectangle {
width: f64, height: f64
}
impl Rectangle {
fn new(w: f64, h: f64) -> Rectangle {
Rectangle { width: w, height: h}
}
}
impl Figure for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn kind(&self) -> &str {
"Rectangle"
}
}
// 円
struct Circle {
radius: f64
}
impl Circle {
fn new(r: f64) -> Circle {
Circle { radius: r }
}
}
impl Figure for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn kind(&self) -> &str {
"Circle"
}
}
// 図形の合計値を求める
fn figure_sum(data: &[&dyn Figure]) -> f64 {
let mut a = 0.0;
for fig in data {
a += fig.area();
}
a
}
fn main() {
let a = Triangle::new(2.0, 2.0);
let b = Rectangle::new(2.0, 2.0);
let c = Circle::new(2.0);
a.print();
b.print();
c.print();
// トレイトオブジェクトを配列に格納する
let d = [&a as &dyn Figure, &b as &dyn Figure, &c as &dyn Figure];
println!("{:.3}", figure_sum(&d));
}
Triangle: area = 2.000
Rectangle: area = 4.000
Circle: area = 12.566
18.566
●クロージャの基本
- Rust の「クロージャ (closure)」は Lisp / Scheme の「ラムダ式 (lambda)」のこと
- Rust では「ラムダ (lambda)」と呼ばれることもある
- ラムダは名前のない関数 (匿名関数, anonymous function)
|仮引数: データ型, ...| -> データ型 { 処理; ... }
仮引数と返り値のデータ型を省略すると Rust が推論してくれる
本体の処理が一つしかない場合は { } を省略できる
ラムダを評価すると「クロージャ」が生成される
このとき、参照可能な局所変数がクロージャに保存される
参照可能な局所変数の集合を「環境」と呼ぶ
クロージャの呼び出し方は関数と同じ
リスト : クロージャの簡単な使用例
fn main() {
let a = 10;
let add_a = |x| x + a; // 変数 a を immutable で借用
println!("{}", add_a(20));
let mut b = 0;
{
let mut inc_b = || b += 1; // 変数 b を mutable で借用
inc_b();
inc_b();
inc_b();
inc_b();
inc_b();
// println!("{}", b); コンパイルエラー
}
// mutable な借用は一つしか使用できない
// inc_b が廃棄されると b にアクセスできる
println!("{}", b);
let mut c = 0;
{
// move クロージャ (所有権をクロージャに移動する)
let mut add_c = move |x| {c += x; c};
// 数値の場合、クロージャ内に値がコピーされ、
// その値が書き換えられる
println!("{}", add_c(100));
println!("{}", add_c(200));
}
println!("{}", c); // c の値は 0 のまま
}
30
5
100
300
0
- クロージャを生成するとき、環境を保持する構造体に特別なトレイト Fn, FnMut, FnOnce が実装される
- Fn : 変数を immutable な参照で捕捉する
- FnMut : 変数を mutable な参照で捕捉する
- FnOnce : 変数の所有権を移動する
- クロージャのデータ型は トレイト(引数のデータ型, ...) -> 返り値のデータ型 になる
- Fn は FnMut を継承し、FnMut は FnOnce を継承している
- Rust はクロージャを生成するとき、できるだけ immutable な参照を使う
●クロージャと高階関数
- クロージャを高階関数に渡す場合、関数とクロージャではデータ型が異なるので、関数と同じ方法で渡すことはできない
- クロージャを関数に渡すには 2 つの方法がある
- 一つは高階関数をジェネリクスで定義して、型パラメータの境界条件にクロージャの型を指定する
- この場合、通常の関数も渡すことができる
- もう一つ、トレイトオブジェクトを渡す方法がある
- 仮引数のデータ型に &dyn クロージャの型 を指定する
- クロージャを渡すときは、ラムダの先頭に & を付ける
- 通常の関数を渡すことも可能、その場合は関数名の前に & を付ける
リスト : クロージャを受け取る高階関数
fn apply1<F, V>(func: F, x: V) -> V where F: Fn(V) -> V {
func(x)
}
fn apply11<V>(func: &dyn Fn(V) -> V, x: V) -> V {
func(x)
}
fn apply2<F, V>(func: F, x: V, y: V) -> V where F: Fn(V, V) -> V {
func(x, y)
}
fn apply22<V>(func: &dyn Fn(V, V) -> V, x: V, y: V) -> V {
func(x, y)
}
fn main() {
fn square(x: f64) -> f64 { x * x }
println!("{}", apply1(|x| x * x, 123));
println!("{}", apply11(&|x| x * x, 123));
println!("{}", apply1(square, 1.111));
println!("{}", apply11(&square, 1.111));
fn add(x: f64, y: f64) -> f64 { x + y }
println!("{}", apply2(|x, y| x + y, 1, 2));
println!("{}", apply22(&|x, y| x + y, 1, 2));
println!("{}", apply2(add, 1.1, 2.2));
println!("{}", apply22(&add, 1.1, 2.2));
}
15129
15129
1.234321
1.234321
3
3
3.3000000000000003
3.3000000000000003
- 簡単な例題として、マッピング、フィルター、畳み込みを行う高階関数を示す
- Rust の場合、これらの高階関数は「イテレータ」として定義されている
- イテレータはあとで説明する
リスト : 簡単な高階関数
// マッピング
fn map<F, T, U>(func: F, xs: &Vec<T>) -> Vec<U>
where F: Fn(T) -> U, T: Copy, U: Copy {
let mut ys: Vec<U> = vec![];
for x in xs {
ys.push(func(*x));
}
ys
}
// フィルター
fn filter<F, T>(pred: F, xs: &Vec<T>) -> Vec<T>
where F: Fn(T) -> bool, T: Copy + PartialEq {
let mut ys: Vec<T> = vec![];
for x in xs {
if pred(*x) { ys.push(*x); }
}
ys
}
// 畳み込み
fn reduce<F, T, U>(func: F, a: U, xs: &Vec<T>) -> U
where F: Fn(U, T) -> U, T: Copy, U: Copy {
let mut acc = a;
for x in xs {
acc = func(acc, *x);
}
acc
}
fn main() {
let xs = vec![1,2,3,4,5,6,7,8];
println!("{:?}", map(|x| x * x, &xs));
println!("{:?}", map(|x| x as f64 * 1.1, &xs));
println!("{:?}", filter(|x| x % 2 == 0, &xs));
println!("{}", reduce(|a, x| a + x, 0, &xs));
}
[1, 4, 9, 16, 25, 36, 49, 64]
[1.1, 2.2, 3.3000000000000003, 4.4, 5.5, 6.6000000000000005, 7.700000000000001, 8.8]
[2, 4, 6, 8]
36
●クロージャを返す
- クロージャを返すにはトレイトオブジェクトを使う
- データ型は Box<dyn Fn(データ型, ...) -> データ型> になる
- クロージャを返すときには若干の制約がある
- ひとつはジェネリクスを使うことができないこと
- もうひとつは move クロージャを使うこと
リスト : クロージャを返す
// x を加算するクロージャを返す
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |y| x + y)
}
// 簡単なジェネレータ
// (Rust にはイテレータがあるので、このような使い方はあまりしないと思う)
fn make_counter(init: i32) -> Box<dyn FnMut() -> i32> {
let mut c = init - 1;
Box::new(move || { c += 1; c})
}
fn main() {
let add10 = make_adder(10); // 10 を加算する関数になる
let add20 = make_adder(20); // 20 を加算する関数になる
println!("{}", add10(1));
println!("{}", add20(2));
let mut cnt = make_counter(1); // 1, 2, 3, ... を返す
for _ in 0..10 {
println!("{}", cnt());
}
}
11
22
1
2
3
4
5
6
7
8
9
10
●ライフタイムの基本
- Rust は参照 (借用) を「ライフタイム」を使って管理している
- あるデータを参照したとき、参照元のデータが廃棄されたあと、その参照が残っていてはいけない
- 通常はコンパイラが参照の有効期間をチェックする (借用チェッカー)
- チェックできないときは、参照の有効期間をライフタイムを使って明示的に指定する
- ライフタイムは 'a や 'b のように ' を付けて表す (ライフタイムパラメータ)
- 'static は特別なライフタイムパラメータ (有効期間はプログラム全体)
- 文字列リテラルのデータ型は &'static str
- static 変数の参照も同様
- 指定方法はジェネリクスの型パラメータと同じ
- < ... > の中でライフタイムパラメータを定義する
- データ型の指定でそれを使用する
fn foo(x: &i32) { ... } => fn foo<'a>(x: &'a i32) { ... }
関数の場合、ライフタイムパラメータを省略すると、引数はすべて異なるライフタイムになる
参照を返すときライフタイムパラメータを省略すると、引数と同じライフタイムになる
ライフタイムが異なる引数が複数ある場合、返り値のライフタイムを指定する必要がある
リスト : ライフタイムパラメータの指定
// 配列の探索
fn find<'a, 'b, T: PartialEq>(item: &'b T, xs: &'a [T]) -> Option<&'a T> {
for x in xs {
if x == item { return Some(x); }
}
None
}
fn main() {
let xs = [1,2,3,4,5,6,7,8];
for x in 0 .. 10 {
match find(&x, &xs) {
Some(v) => println!("{}", v),
None => println!("None")
}
}
}
None
1
2
3
4
5
6
7
8
None
- 関数 find() でライフパラメータの指定を省略するとコンパイルエラー
- 返り値 Option<&T> は配列 xs の要素への参照
- 返り値のライフタイムが xs のライフタイムよりも長くなってはいけない
- ライフタイムパラメータ 'a を使って、2 つのライフタイムが同じであることをコンパイラに指示する
- Rust ver 1.62.1 の場合、ライフタイムパラメータ 'b を省略してもコンパイルできる
- struct で参照を格納するときはライフタイムパラメータが必要になる
リスト : 構造体のライフタイム
struct Foo<'a> {
x: &'a i32
}
fn main() {
let y = 123;
let z = Foo { x: &y };
println!("{}", z.x);
// let z1; コンパイルエラー
// {
// let y1 = 456;
// z1 = Foo { x: &y1 };
// }
// println!("{}", z1.x);
}
y と z はライフタイムが同じなのでコンパイルできる
y1 と z1 では、y1 のライフタイムが z1 よりも短いのでコンパイルエラーになる
リスト : メソッドのライフタイム
struct Foo<'a> {
x: &'a i32
}
impl <'a> Foo<'a> {
fn foo(&self) ->&i32 { self.x }
fn foo1(&self, y: &i32) ->&i32 {
println!("{},{}", self.x, y);
self.x
}
fn foo2<'b>(&self, y: &'b i32) ->&'b i32 {
println!("{},{}", self.x, y);
y
}
}
fn main() {
let y = 123;
let z = Foo { x: &y };
println!("{}", z.foo());
let y1 = 456;
println!("{}", z.foo1(&y1));
println!("{}", z.foo2(&y1));
}
123
123,456
123
123,456
456
- メソッド foo() の返り値のライフタイムは引数と同じになる
- メソッド foo1() のように、複数の引数があってライフタイムパラメータの指定がない場合、返り値のライフタイムは self と同じになる
- メソッド foo2() のように、self と異なるライフタイムを指定したい場合はライフタイムパラメータを明示する
●type
- Rust は type でデータ型に別名を付けることができる (タイプエイリアス)
type 別名 = データ型;
type はデータ型に別名を付けるだけで、新しいデータ型を定義するわけではない
type はトレイトの中で型パラメータの定義に使用できる
Rust ではこれを「関連型」という
リスト : 関連型
trait Foo {
type A;
type B;
// メソッドの仕様で型パラメータ Self::A, Self::B を使用する
...
}
impl Foo for Bar {
type A = i32; // 具体的なデータ型を指定する
type B = f64;
// メソッドの定義で型パラメータ Self::A, Self::B を使用する
// または具体的なデータ型を使用してもよい
...
}
トレイトで定義した型パラメータを使用するときは Self::型パラメータ とする
impl でメソッドを実装するときは、Self::型パラメータ もしくは実際のデータ型を使う
リスト : 点と距離
// f64 で二次元座標を保持する
struct Point1 {
x: f64, y: f64
}
// f32 で二次元座標を保持する
struct Point2 {
x: f32, y: f32
}
// f64 で三次元座標を保持する
struct Point3D {
x: f64, y: f64, z: f64
}
// 距離を求めるトレイト
trait Distance {
type Item;
fn distance(&self, p: &Self) -> Self::Item;
}
// 実装
impl Distance for Point1 {
type Item = f64;
fn distance(&self, p: &Self) -> Self::Item {
let dx = self.x - p.x;
let dy = self.y - p.y;
(dx * dx + dy * dy).sqrt()
}
}
impl Distance for Point2 {
type Item = f32;
fn distance(&self, p: &Self) -> Self::Item {
let dx = self.x - p.x;
let dy = self.y - p.y;
(dx * dx + dy * dy).sqrt()
}
}
impl Distance for Point3D {
type Item = f64;
fn distance(&self, p: &Self) -> Self::Item {
let dx = self.x - p.x;
let dy = self.y - p.y;
let dz = self.z - p.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
}
fn main() {
let p1 = Point1 { x: 0.0, y: 0.0 };
let p2 = Point1 { x: 10.0, y: 10.0 };
let p3 = Point2 { x: 0.0f32, y: 0.0f32 };
let p4 = Point2 { x: 10.0f32, y: 10.0f32 };
let p5 = Point3D { x: 0.0, y: 0.0, z: 0.0 };
let p6 = Point3D { x: 10.0, y: 10.0, z: 10.0 };
println!("{}", p1.distance(&p2));
println!("{}", p3.distance(&p4));
println!("{}", p5.distance(&p6));
}
14.142135623730951
14.142136
17.320508075688775
●イテレータ
- Rust ではトレイト Iterator を実装すると「イテレータ (iterator)」になる
- Iterator は関連型 Item とメソッド next() を実装するだけ
- Iterator には便利なデフォルトメソッドが多数用意されている
リスト : Iterator トレイト
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// デフォルトメソッドの定義
...
}
next() は値を Option 型に格納して返す
値が無くなった場合は None を返す
Iterator を実装すると for 文を使用できるようになる
リスト : 整数列の生成
// start 以上 end 未満の整数列を生成する
struct IntSeq {
start: i32, end: i32
}
// 初期化
impl IntSeq {
fn new(s: i32, e: i32) -> IntSeq {
IntSeq { start: s, end: e }
}
}
// イテレータの実装
impl Iterator for IntSeq {
type Item = i32;
fn next(&mut self) -> Option<i32> {
if self.start < self.end {
let x = self.start;
self.start += 1;
Some(x)
} else {
None
}
}
}
fn main() {
let mut s0 = IntSeq::new(0, 5);
while let Some(x) = s0.next() {
println!("{}", x);
}
for x in IntSeq::new(5, 10) {
println!("{}", x);
}
let mut s1 = 10 .. 15;
while let Some(x) = s1.next() {
println!("{}", x);
}
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- 配列の参照やベクタは for 文で要素を順番に取り出すことができるがイテレータではない
リスト : 配列と for 文 (1)
fn main() {
let a = [1, 2, 3, 4, 5];
for x in &a { // for x in a { ... } とするとコンパイルエラー
println!("{}", x);
}
}
配列やベクタをイテレータに変換するにはメソッド iter(), iter_mut(), into_iter() を使う
- iter() は要素の参照 (immutable) を返すイテレータを生成する
- iter_mut() は要素の参照 (mutable) を返すイテレータを生成する
- into_iter() は要素を返すイテレータを生成する (要素は move される)
リスト : 配列と for 文 (2)
fn main() {
let a = [1, 2, 3, 4, 5];
for x in a.iter() {
println!("{}", x);
}
}
イテレータから値を受け取り、異なる値のイテレータを生成するメソッドを「イテレータアダプタ」と呼ぶ
map(), filter() などの便利な高階関数はイテレータアダプタとして用意されている
イテレータから値を受け取り、最終的な処理結果を返すメソッドを「コンシューマ」と呼ぶ
イテレータの要素を集積する collect() や畳み込みを行う fold() など便利なメソッドが多数用意されている
リスト : map, filter, fold, collect の簡単な使用例
fn change(x: i32) -> String {
if x % 15 == 0 {
"FizzBuzz".to_string()
} else if x % 3 == 0 {
"Fizz".to_string()
} else if x % 5 == 0 {
"Buzz".to_string()
} else {
format!("{}", x)
}
}
fn main() {
let xs: Vec<_> = (1..101).map(change).collect();
println!("{:?}", xs);
let ys = vec![1,2,3,4,5,6,7,8,9,10];
{
let a: Vec<_> = ys.iter().map(|x| x * x).collect();
println!("{:?}", a);
let b: Vec<_> = ys.iter().filter(|&x| x % 2 == 0).collect();
println!("{:?}", b);
println!("{}", ys.iter().fold(0, |a, x| a + x));
}
let c: Vec<_> = ys.into_iter().filter(|x| x % 2 == 1).collect();
println!("{:?}", c);
// println!("()", ys); コンパイルエラー
}
- Vec<_> のように要素のデータ型に _ を指定すると、要素のデータ型を推論してくれる
- collect::<Vec<_>>() のように指定することもできる
- iter() は要素への参照を渡すので、filter() のラムダではパターンマッチで要素の値を求めている
["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz",
"13", "14", "FizzBuzz", "16", "17", "Fizz", "19", "Buzz", "Fizz", "22", "23",
"Fizz", "Buzz", "26", "Fizz", "28", "29", "FizzBuzz", "31", "32", "Fizz", "34",
"Buzz", "Fizz", "37", "38", "Fizz", "Buzz", "41", "Fizz", "43", "44", "FizzBuzz",
"46", "47", "Fizz", "49", "Buzz", "Fizz", "52", "53", "Fizz", "Buzz", "56", "Fizz",
"58", "59", "FizzBuzz", "61", "62", "Fizz", "64", "Buzz", "Fizz", "67", "68",
"Fizz", "Buzz", "71", "Fizz", "73", "74", "FizzBuzz", "76", "77", "Fizz", "79",
"Buzz", "Fizz", "82", "83", "Fizz", "Buzz", "86", "Fizz", "88", "89", "FizzBuzz",
"91", "92", "Fizz", "94", "Buzz", "Fizz", "97", "98", "Fizz", "Buzz"]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[2, 4, 6, 8, 10]
55
[1, 3, 5, 7, 9]
●モジュール
- モジュールは mod モジュール名 { ... } で定義する (mod は module の略)
- モジュールで定義された名前は基本的に非公開 (private)
- 同じモジュール内からアクセスできるが、モジュールの外側からアクセスすることはできない
- モジュールの外に公開する場合は pub を付ける (pub は public の略)
- 公開された名前は モジュール名::名前 でアクセスする
- モジュールは入れ子にすることができる
- 構造体の場合、pub を指定してもフィールドは private のまま
- フィールドも公開したい場合はフィールド名に pub を付ける
- impl で定義したメソッドを公開する場合も pub を付ける
リスト : モジュールの使用例
mod foo {
// 公開 (foo::bar::function() でアクセスできる)
pub mod bar {
pub fn function() {
println!("call foo::bar::function");
}
}
// 非公開 (foo の中でのみ有効)
mod baz {
pub fn function() {
println!("call foo::baz::function");
}
}
// 構造体 Foo (public)
pub struct Foo {
// フィールド a は public, b は private
pub a: i32, b: i32
}
impl Foo {
pub fn new(a: i32, b: i32) -> Foo {
Foo { a: a, b: b }
}
pub fn add(&self) -> i32 { self.a + self.b }
}
pub fn function() {
println!("call foo::function");
baz::function();
}
}
fn main() {
foo::function();
foo::bar::function();
// foo::baz::function(); コンパイルエラー
let a = foo::Foo::new(1, 2);
println!("{}", a.a);
// println!("{}", a.b); コンパイルエラー
println!("{}", a.add());
}
call foo::function
call foo::baz::function
call foo::bar::function
1
3
use 名前1:: ... ::名前N::名前 as 別名; // 名前を別名でアクセスできる
use 名前1:: ... ::名前N::名前; // 名前だけでアクセスできる
use 名前1:: ... ::名前N::{名前a, 名前b ...}; // 複数の名前を指定できる
use 名前1:: ... ::名前N::*; // 名前N で公開されているものすべて
ブロックの中で use 宣言を使用すると、有効範囲はそのブロックに限定される
たとえば、main() の中で use foo::*; とすると、foo:: を省略することができる
use foo::Foo; とすると foo::Foo::new() は Foo::new() でアクセスできる
use foo::{Foo, function} とすると foo::function() も function() でアクセスできる
- モジュールをファイルに切り出す場合、mod モジュール名; だけを指定する
- この場合、Rust は モジュール名.rs または モジュール名/mod.rs をモジュールとして認識する
- このとき、ファイルの中で改めて mod を宣言する必要はない
- モジュール名.rs でモジュールを切り出した場合、同じ方法で入れ子のモジュールを切り出すことはできないようだ (コンパイルエラー)
test.rs -- メインプログラム
foo/mod.rs -- モジュール foo
/bar/mod.rs -- モジュール bar
/baz/mod.rs -- モジュール baz
図 : ファイルの構成
リスト : メインプログラム
mod foo;
fn main() {
foo::function();
foo::bar::function();
// foo::baz::function(); コンパイルエラー
let a = foo::Foo::new(1, 2);
println!("{}", a.a);
// println!("{}", a.b); コンパイルエラー
println!("{}", a.add());
}
リスト : モジュール foo/mod.rs
pub mod bar;
mod baz;
pub struct Foo {
pub a: i32, b: i32
}
impl Foo {
pub fn new(a: i32, b: i32) -> Foo {
Foo { a: a, b: b }
}
pub fn add(&self) -> i32 { self.a + self.b }
}
pub fn function() {
println!("call foo::function");
baz::function();
}
リスト : モジュール foo/bar/mod.rs
pub fn function() {
println!("call foo::bar::function");
}
リスト : モジュール foo/baz/mod.rs
pub fn function() {
println!("call foo::baz::function");
}
$ rustc test.rs
$ ./test
call foo::function
call foo::baz::function
call foo::bar::function
1
3
●クレート
- 「クレート (crate)」は木箱という意味だが、Rust ではコンパイルの単位を表す
- 一つのクレートをコンパイルして実行ファイルまたはライブラリを生成する
- Rust の標準ライブラリはクレートで管理されている
- クレートは複数のモジュールを格納することができる
- たとえば、標準ライブラリのクレート std には便利なモジュールが多数用意されている
- ライブラリを生成するときはオプション --create-type=lib を指定する
リスト : クレイト cfoo.rs
pub fn function() {
println!("cfoo::function");
}
rustc --crate-type=lib cfoo.rs
これでライブラリ libcfoo.rlib が生成される
クレートを使用するには extern create クレート名; と宣言する
- Rust 2018 以降、extern create は不要になった (宣言はあってもよい)
- cargo でビルドするときは dependencies にクレート名とパスを記述すること
- rustc でコンパイルするときは --extern でクレート名を渡す必要がある
リスト : クレート cfoo の使用例 (test1.rs)
extern crate cfoo;
fn main() {
cfoo::function();
}
クレート内で定義された名前は クレート::名前 でアクセスできる
ライブラリがカレントディレクトリにある場合、オプション -L . でパスを追加する
rustc -L . test1.rs でコンパイルできる
$ rustc -L . test1.rs
$ ./test1
cfoo::function
extern create を宣言しない場合
$ rustc --extern cfoo -L . test1.rs
$ ./test1
cfoo::function
クレート名とソースファイル名が異なっていても、オプション -crate-name を指定すれば、クレートを生成することができる
リスト : クレイト cbar (cbar/lib.rs)
pub fn function() {
println!("cbar::function");
}
$ rustc --crate-type=lib --crate-name cbar cbar/lib.rs
カレントディレクトリに libcbar.rlib が生成される
リスト : クレイト cbar の使用例 (test2.rs)
extern crate cbar;
fn main() {
cbar::function();
}
rustc -L . test2.rs でコンパイルできる
$ rustc -L . test2.rs
mhiroi@DESKTOP-FQK6237:~/work/rust$ ./test2
cbar::function
--crate-type, --crate-name はアトリビュートで指定できる
リスト : アトリビュートでの指定方法 (cbar/lib.rs)
#![crate_type = "lib"]
#![crate_name = "cbar"]
pub fn function() {
println!("cbar::function");
}
この場合は rustc cbar/lib.rs でコンパイルできる
実際にアプリケーションやライブラリを開発するときは、ビルドツール Cargo を使ったほうが良い
●cargo によるクレートの作成
- cargo でライブラリを作成するにはコマンド cargo new で --lib を指定する
cargo new --lib path
デフォルトで path がクレート名になる
$ cargo new --lib mylib
Created library `mylib` package
$ cd mylib
$ tree
.
├── Cargo.toml
└── src
└── lib.rs
$ cat Cargo.toml
[package]
name = "mylib"
version = "0.1.0"
edition = "2021"
[dependencies]
プログラムは src/lib.rs に記述する
$ cat src/lib.rs
pub fn function() {
println!("mylib::function");
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
#[test] はテスト用の関数であることを表す (テストはあとで説明する)
cargo build で target/debug に libmylib.rlib が作成される
$ cargo build
Compiling mylib v0.1.0 (/home/mhiroi/rust/mylib)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.87s
$ ls
Cargo.lock Cargo.toml src target
$ ls target/debug
build deps examples incremental libmylib.d libmylib.rlib
クレート mylib を使用するプロジェクトをビルドするとき、mylib も自動的にビルドされる
ローカルなクレート mylib を使用するには、プロジェクトの設定ファイル Cargo.toml の dependencies にパスを記述する
$ cargo new samplelib
Created binary (application) `samplelib` package
$ ls mylib samplelib
mylib:
Cargo.toml src
samplelib:
Cargo.lock Cargo.toml src target
$ cat Cargo.toml
[package]
name = "samplelib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at
https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
mylib = { path = "../mylib" }
$ cat src/main.rs
// extern crate mylib; は不要
fn main() {
mylib::function();
}
あとはビルドするだけ
$ cd samplelib
$ cargo build
Locking 1 package to latest compatible version
Adding mylib v0.1.0 (/home/mhiroi/rust/mylib)
Compiling mylib v0.1.0 (/home/mhiroi/rust/mylib)
Compiling samplelib v0.1.0 (/home/mhiroi/rust/samplelib)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
$ target/debug/samplelib
mylib::function
もちろん、cargo run でも実行できる
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/samplelib`
mylib::function
●外部クレートの使い方
- Rust は標準ライブラリのほかにも、優れたライブラリ (クレート) が多数公開されている
- 特に、下記サイトで公開されているクレートは簡単に導入することができる
- crates.io: Rust Package Registry (https://crates.io/)
- 使い方は Cargo.toml の dependencies にクレート名とバージョンを記述するだけ
- クレートのインストールとビルドは cargo がやってくれる
- 簡単な例として、クレート num の BigInt (多倍長整数) を使って階乗を計算する
- 2024 年 12 月時点で、num のバージョンは 0.4.3
$ cargo new samplenum
Created binary (application) `samplenum` package
$ cd samplenum
$ ls
Cargo.toml src
$ cat Cargo.toml
[package]
name = "samplenum"
version = "0.1.0"
edition = "2021"
[dependencies]
num = "0.4.3"
リスト : 階乗 (BigInt 版, main.rs)
use num::bigint::BigInt;
use num::One;
fn fact(n: i32) -> BigInt {
if n == 0 {
BigInt::one()
} else {
n * fact(n - 1)
}
}
fn main() {
for n in 20 .. 31 {
println!("{}", fact(n));
}
}
$ cargo build
Updating crates.io index
Locking 8 packages to latest compatible versions
Downloaded num-iter v0.1.45
Downloaded num v0.4.3
Downloaded autocfg v1.4.0
Downloaded num-integer v0.1.46
Downloaded num-complex v0.4.6
Downloaded num-rational v0.4.2
Downloaded num-bigint v0.4.6
Downloaded num-traits v0.2.19
Downloaded 8 crates (272.9 KB) in 1.13s
Compiling autocfg v1.4.0
Compiling num-traits v0.2.19
Compiling num-integer v0.1.46
Compiling num-complex v0.4.6
Compiling num-bigint v0.4.6
Compiling num-iter v0.1.45
Compiling num-rational v0.4.2
Compiling num v0.4.3
Compiling samplenum v0.1.0 (/home/mhiroi/rust/samplenum)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.07s
$ target/debug/samplenum
2432902008176640000
51090942171709440000
1124000727777607680000
25852016738884976640000
620448401733239439360000
15511210043330985984000000
403291461126605635584000000
10888869450418352160768000000
304888344611713860501504000000
8841761993739701954543616000000
265252859812191058636308480000000
コンパイルされたクレートは taget/debug/deps に格納される
リリース版は taget/release/deps に格納される
$ cargo run --release
Compiling autocfg v1.4.0
Compiling num-traits v0.2.19
Compiling num-integer v0.1.46
Compiling num-complex v0.4.6
Compiling num-bigint v0.4.6
Compiling num-iter v0.1.45
Compiling num-rational v0.4.2
Compiling num v0.4.3
Compiling samplenum v0.1.0 (/home/mhiroi/rust/samplenum)
Finished `release` profile [optimized] target(s) in 10.57s
Running `target/release/samplenum`
2432902008176640000
51090942171709440000
1124000727777607680000
25852016738884976640000
620448401733239439360000
15511210043330985984000000
403291461126605635584000000
10888869450418352160768000000
304888344611713860501504000000
8841761993739701954543616000000
265252859812191058636308480000000
●エラー処理の基本
- Rust のエラー処理は Option や Result を使ってエラーを返すことが基本
- マクロ panic! はエラーメッセージを表示してプログラムを終了する
- Rust ではこれを「パニック (panic)」という
- パニックの種類によっては std::panic::catch_unwind() で捕捉することもできるようだ
- Option と Result のメソッド unwrap() は Some(T) や Ok(T) から T を取り出す
- None や Err(E) の場合、unwrap() はパニックする
- メソッド unwrap_or(default) を使うとパニックせずに default を返すことができる
- Option のまま Some の値を処理したい場合はメソッド map() を使う
- map() は引数の関数に Some の値を渡して実行し、その値を Some に格納して返す
- None の場合は何もせずに None を返す
- map() に渡す関数が Option を返す場合、返り値は Option の入れ子になる
- これを一重の Option にしたい場合はメソッド and_then() を使う
- 繰り返しになるが、and_then() に渡す関数は Option を返すことに注意
- and_then() は関数の返り値をそのまま返すだけ
- 関数型言語の flatmap() と同じ
- Result にも map() や and_then() がある
- Err(E) に作用する map_err() や or_else() もある
- Option には Result に変換するメソッド ok_or(err) がある
- None の場合、引数 err が Reuslt の Err(E) にセットされる
リスト : Option, Result と map() の使用例
fn main() {
let a = [1, 2, 3, 4, 5];
let r1 = a.iter().find(|&x| x % 3 == 0);
println!("{}", r1.map(|&x| x * 10).unwrap());
let r2 = a.iter().find(|&x| x % 6 == 0);
println!("{}", r2.map(|&x| x * 10).unwrap_or(0));
println!("{}", "12345".parse::<i32>().map(|x| x * 2).unwrap());
println!("{}", "abcde".parse::<i32>().map(|x| x * 2).unwrap_or(0));
}
- find(pred) はイテレータのコンシューマ
- 述語 pred が真を返す要素を Option に格納して返す
- parse() は文字列のメソッドで、文字列を指定したデータ型の数値に変換する
- 結果は Result に格納して返す
30
0
24690
0
マクロ try! を使うと、Err(E) が返された時点でリターンする (try! を使うとワーニング)
- 後置演算子 ? を使うと、Err(E) が返された時点でリターンする
- この時の返り値は Err(E)
- Ok(T) が返された場合は、そこから T を取り出す
- Rust ではこれを「早期リターン」という
- 返り値のデータ型は Result でないとコンパイルエラー
リスト : 後置演算子 ? の使用例
fn parse_buff(buff: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
let mut r: Vec<i32> = vec![];
for s in buff {
let n = s.parse()?;
r.push(n)
}
Ok(r)
}
fn main() {
let s0 = ["123", "456", "789"];
println!("{:?}", parse_buff(&s0));
let s1 = ["123", "abc", "789"];
println!("{:?}", parse_buff(&s1));
}
Ok([123, 456, 789])
Err(ParseIntError { kind: InvalidDigit })
●ファイル入出力
- Rust の場合、ファイルをオープンするとそれに対応するファイル構造体 File が生成される
- 標準入出力は std::io::Stdin, std::io::Stdout, std::io::Stderr
- それぞれ std::io::stdin(), std::io::stderr(), std::io::stderr() で取得する
- ファイルのリードオープンは std::fs::File::open(filename) -> Result<File>
- ファイルのライトオープンは std::fs::File::create(filename) -> Result<File>
- filename はパスを表す std::path::Path か、文字列や String で指定する
- ファイル入出力の Result は std::io::Result のことで次のように定義されている
type Result<T> = Result<T, Error>;
Error は std::io::Error のことで、ファイル入出処理のエラータイプを表す
File 構造体がスコープから外れたとき、オープンしたファイルは自動的にクローズされる
リードオープンした File にはトレイト Read が実装されている
Read の主なメソッド
- fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
- read() は配列 buf にデータを読み込む
- Result に読み込んだバイト数を格納して返す
- fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>
- fn read_to_string(&mut self, buf: &mut String) -> Result<usize>
- read_to_end() と read_to_string() はファイルを最後まで読み込み、バッファ buf に格納する
- fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>
- read_exact() は配列 buf にデータを読み込む
- 配列 buf を満たすことができなければエラー
- fn bytes(self) -> Bytes<Self>
- ファイルから 1 byte ずつ読み込むイテレータを生成する
- 要素は Result<u8>
- ファイルの終端に到達したらイテレータは None を返す
ライトオープンした File にはトレイト Write が実装されている
Write の主なメソッド
- fn write(&mut self, buf: &[u8]) -> Result<usize>
- fn flush(&mut self) -> Result<()>
- バッファリングされている場合、バッファ内のデータを出力する
- fn write_all(&mut self, buf: &[u8]) -> Result<()>
File 構造体の入出力処理はバッファリングを行わないので実行速度が遅い
バッファリングを行うには std::io::BufReader, std::io::BufWriter を使う
コンストラクタは new(File)
BufReader にはトレイト Read, BufRead が実装されている
BufRead の便利なメソッド
- fn read_line(&mut self, buf: &mut String) -> Result<usize>
- 配列 buf に 1 行読み込む
- 改行文字 (Unix 系は \n, Windows は \r\n) は buf に格納される
- 読み込んだバイト数を Result に格納して返す
- fn lines(self) -> Lines<Self>
- ファイルから 1 行ずつ読み込むイテレータを生成する
- 要素は Result<String>
- read_line() と違って、改行文字は取り除かれることに注意
- ファイルの終端に到達したらイテレータは None を返す
BufWriter にはトレイト Write が実装されている (トレイト BufWrite は存在しない)
リスト : 単純な echo (1)
use std::io::prelude::*;
use std::io::{self, BufReader, BufWriter};
// 標準入力 -> 標準出力 (バイト単位)
fn main() {
let reader = BufReader::new(io::stdin());
let mut writer = BufWriter::new(io::stdout());
for c in reader.bytes() {
writer.write(&[c.unwrap()]).unwrap();
}
}
- use std::io::prelude::*; は Read, BufRead, Write, Seek をインポートする
- use std::io::{self, ...}; の self は use std::io; と同じ
リスト : 単純な echo (2)
use std::io::prelude::*;
use std::io::{self, BufReader};
// 標準入力 -> 標準出力 (行単位)
fn main() {
let reader = BufReader::new(io::stdin());
for ls in reader.lines() {
println!("{}", ls.unwrap());
}
}
- コマンドライン引数は std::env::args を介して取得する
- args() はイテレータを生成する (要素は String)
リスト : コマンドライン引数の取得 (test3.rs)
fn main() {
for x in std::env::args() {
println!("{}", x);
}
}
$ ./test3 foo bar baz
./test3
foo
bar
baz
oops
リスト : ファイルの連結 (cat0.rs)
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
fn concat_file(filename: &String) -> std::io::Result<()> {
let file = File::open(filename)?;
let mut reader = BufReader::new(file);
let mut buff = String::new();
reader.read_to_string(&mut buff)?;
println!("{}", buff);
Ok(())
}
fn main() {
for filename in std::env::args().skip(1) {
match concat_file(&filename) {
Ok(_) => (),
Err(err) => println!("{}: {:?}", &filename, err)
}
}
}
メソッド description() はショートエラーメッセージを返す
- description() は非推奨になった
- err は {:?} で Debug 表示する、または err.to_string() でエラーメッセージを表示する
リスト : ファイルのコピー (cp0.rs)
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
const BUFF_SIZE: usize = 128;
fn copy_file(src: &String, dst: &String) -> std::io::Result<()> {
let infile = File::open(src)?;
let outfile = File::create(dst)?;
let mut buff: [u8; BUFF_SIZE] = [0; BUFF_SIZE]; // バッファ
let mut reader = BufReader::new(infile);
let mut writer = BufWriter::new(outfile);
loop {
let size = reader.read(&mut buff)?;
writer.write(&mut buff[..size])?;
if size < BUFF_SIZE { break; }
}
Ok(())
}
fn main() {
let args: Vec<_> = std::env::args().collect();
if args.len() < 3 {
println!("usage: cp0 src_file dst_file");
} else {
match copy_file(&args[1], &args[2]) {
Ok(_) => (),
Err(err) => println!("{:?}", err)
}
}
}
リスト : ファイルのコピー (cp1.rs)
use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
fn copy_file(src: &String, dst: &String) -> std::io::Result<()> {
let infile = File::open(src)?;
let outfile = File::create(dst)?;
let mut reader = BufReader::new(infile);
let mut writer = BufWriter::new(outfile);
let mut buff: Vec<u8> = vec![];
reader.read_to_end(&mut buff)?;
// ベクタは immutable なスライスに変換できる
writer.write_all(&buff)?;
Ok(())
}
fn main() {
let args: Vec<_> = std::env::args().collect();
if args.len() < 3 {
println!("usage: cp1 src_file dst_file");
} else {
match copy_file(&args[1], &args[2]) {
Ok(_) => (),
Err(err) => println!("{:?}", err)
}
}
}