Rust 輪読会 20181203
第6章 式
pp. 121-140
6.1 式言語
Rust プログラムは C言語一派と似ている
Cは式 (expression) と文 (statement) に明確な区別がある
式と文とは?
code:C(c)
S * (fahr - 32) / 9 // これは式
// これは文
for(; begin != end; begin++){
if(*begin == target) break;
}
式には値があり, 文にはない
Rust は式言語
Cの制御フロー(if, switch) は文で書く
if, switch は式の中で使うことが出来ない
三項演算子は式レベルの if の代わり
Rustは全て式で書く
if, match も式の中で使え, 値も作ることができる
code:if, match で値を作る例(rust)
let status =
if cpu.temprature <= MAX_TEMP{
HttpStatus::Ok
}else{
HttpStatus::ServerError // 鯖溶けた
};
println!("Inside the cat, you see {}.",
match vat.contents{
Some(brain) => brain.desc();
None => "nothing of interest"
});
6.2 ブロックとセミコロン
Rust ではブロックも式, つまり値を生み出す
Rust の行末には大抵セミコロンをつける
これはC, Java とかと一緒
Rust においてセミコロンがついている行はCのように実行される
セミコロンがない場合には, ブロックが値を生むようになる
code:ブロック例(rust)
let display_name = match post.author(){
// セミコロンなし, author.name()の返り値がdisplay_nameに入る
Some(author) => author.name(),
None => {
// 式 + セミコロンは, メソッドが呼ばれるだけで返り値は捨てられる
let nerwork_info = post.get_network_metadata()?;
let ip = nerwork_info.client_address();
// セミコロンなし, メソッドが呼ばれ, 返り値がdisplay_nameに入る
ip.to_string()
}
};
つまり式にセミコロンがない場合それが関数の返り値または, ブロックの返り値になる
Rust のセミコロンには意味があるためコンパイルエラーの内容がC等とは異なる
code:コンパイルエラー(rust)
fn getint() -> i32{
let v = (3, 3);
v.0; // 返り値に間違えてセミコロンをつけた場合
}
errorE0308: mismatched types --> src\main.rs:5:19
|
5 | fn getint() -> i32{
| ___________________^
6 | | let v = (3, 3);
7 | | v.0;
| | - help: consider removing this semicolon
8 | | }
| |_^ expected i32, found () // i32を返すはずが, ()になっている
|
= note: expected type i32
found type ()
Rust コンパイラはわざとセミコロンを打ち込んであると考える
6.3 宣言
code:宣言方法(rust)
let name: type = expr // 名前, 型, 式
// 最初に初期化しなくても制御フロー等で初期化も可
let name;
// 一度だけしか値を書き換えることはないので mut 宣言しなくて良い
if user.has_nickname{
name = user.nickname();
}else{
name = generate_unique_name();
user.register(&name);
}
Rust では変数に同じ名前をつけることが可能 (シャドウイングっていうらしい?)
再宣言も可能
code:変数名ダブリ(rust)
for line in file.lines(){
let line = line?; // 前者はString型, 後者はResult型
...
}
ブロック内でアイテム(fn, struct, use 等)の宣言が行える
つまり, ブロックにモジュールをすべて宣言できる
ブロック内でアイテムを宣言した場合はそのブロック内であればどこでも使える
アイテム外のローカル変数は使えないことに注意 (クロージャーでなんとかできる)
code:アイテム宣言(rust)
fn getint() -> i32{
fn read_int() -> i32{
// コマンドラインから一行読み込む
let mut line = String::new();
std::io::stdin().read_line(&mut line).ok();
// 空白で区切る
let vec: Vec<&str> = line.split_whitespace().collect();
vec0.parse().unwrap_or(0) }
let n = read_int();
println!("{}", n);
return n;
}
6.4 if と match
if の条件は, 必ず bool 型でないといけない
C みたいに暗黙に真偽を変換しないので
code:if(rust)
// c++
int n = 1;
// 暗黙的に変換してる, 0以外なら基本的にTrue
if(n) cout << n << endl;
// Rust
let n = 1;
if n == 1 {
println!({}, n);
}
何も該当しなかった場合の else 文は省略可能
match 文は C で言うところの switch 文
code:match(rust)
let n = getint();
match n {
n if n < 10 => println!("less 10"),
n if n >= 10 => println!("more 10"),
100 => println!("100"),
// 上の if で数値の比較までは見ていない
// 全部パターン網羅していなければワイルドカードパターンが必要
// _ は switch 文における default の役割
_ => println!("nothing")
}
match 文で定数値で比較する場合
C 同様にジャンプテーブルにコンパイルして最適化するらしい
その場合は定数値だけの配列を作成し, match 文は配列へのアクセスとしてコンパイルされる
match が万能なところ
=> の左にいろんなパターンをおける
Option 型, Result 型, 等々
詳しくは10章
if, match においてすべてのブロックは同じ型の値を生成する必要がある
code:例(rust)
let s = if n == 10 {10} else { 100 }; // ok
let s = if n == 10 {10} else { "100" }; // だめ
let s = if n == 10 {10}; // これも条件にあわない場合 () が返るのでだめ
6.4.1 if let
Option 型, Result 型からデータを取り出す時に便利
使う必要はあまりない
match 式においてパターンが1つのときには有効
if 式でパターン比較を行いときに使うと便利
code:if let(rust)
// 与えられた変数がパターンにマッチするか見る
if let Some(cookie) = request.session_cookie{
return restore_session(cookie);
}else{
// パターンマッチ失敗した時に実行したいこと
}
6.5 ループ
Rust にはループ式が4つある
ループを値を生むが常に () が返る
code:ループ(rust)
// conditon は必ず bool 型
while condition{}
// if let と同じ
while let pattern = condition{}
// 6.7へ
loop{}
// よくみるやつ
for pattern in collection{}
for i in 0..20{} // .. 演算子は範囲を表す
for(int i = 0; i < 20; i++) // これと一緒
範囲がループで使えるのはイテレータ型を実装しているから
ループの際気をつけること
コレクションに対する操作の場合は, 参照を渡す必要あり
参照を渡さないと値が捨てられる可能性があるため
code:捨てられる場合(rust)
for s in strings{
println!({}, s); // ここで移動してしまう
// ここでドロップする
}
code:捨てられないとき(rust)
// &mut strings で渡せば変更もできる
for s in &strings{
println!({:?}, s);
}
ループにはラベル付が可能であり
複数ネストされたループを同時に終わらせたいとき等に有効
code:例(rust)
'label:
for i in 0..20{
for j in 0..20{
// 積が 100 になったとき外側のループも終わる
if i * j == 100{
break 'label;
}
}
}
6.6 return 式
基本的な使い方は C とかと同じ
return のあとに値がなければ, () を返す
code:return(rust)
// Ok ならその下の行まで処理を行い
// Err ならその場で Err(err) を返り値にして関数自体終わり
let out = match File::create(filename){
Ok(f) => f;
Err(err) => return Err(err)
};
6.7 なぜ Rust に loop 式があるのか
無限ループなんていらなくない?
と思うけど必要 (コンパイラが優秀なので)
code:無限ループ(rust)
// 一見みると良さそうだがだめなところがある
let mut n = 1;
while true{
if n == 0{ break; }
n += 1;
}
コンパイルする際はループの繰り返し条件までは見ていない
もし, このブロックの返り値が i32 型だった場合
返す値が () になるのでコンパイラはエラーをだす
code:エラー(rust)
errorE0308: mismatched types --> src\main.rs:23:5
|
14 | fn getint() -> i32{
| --- expected i32 because of return type
...
23 | / while true{
24 | | if n == 0{break;}
25 | | n += 1;
26 | | }
| |_____^ expected i32, found ()
|
= note: expected type i32
found type ()
こうした制御フローの解析の仕方をフローを考慮した解析と呼ばれる
コンパイラは以下のことを確認している
関数すべてのパスが期待した通りの値を返すのか
ローカル変数の未初期化をチェック
到達できないコードはあるか?
loop式は, 「したいことをその通りに書く」ために用意されている
また, loop 式は発散し, 値を生み出さない
それように ! 型がある
code:!(rust)
fn tmp -> !{
loop{
println!{"Hello World!."};
}
// 返り値があるとコンパイルエラーになる
}
6.8 関数呼び出しとメソッド呼び出し
関数呼び出しは他言語と同じ
Rust は参照とその参照の指す値を明確に区別する
i32 型に &i32 型を渡すとエラーになる
例外は . 演算子
.演算子が自動的に参照を解決してるらしい
static メソッドと static でないメソッドの違いは他のオブジェクト指向言語の違いと同じ
Vec::new(), HashMap::new() とか
メソッドは連鎖できる
code:連鎖(rust)
// スペースで区切ったあとに要素をまとめるメソッドを呼び出している
let vec: Vec<&str> = line.split_whitespace().collect();
// 第一要素にアクセスして unwarp して返す
vec0.parse().unwrap_or(0) Rust の関数呼び出しやメソッド呼び出しでは, Vec<T> のようなジェネリック型の構文が使えない
ジェネリック構文をあまり使ったことがないのであまりピンとこない・・・
code:例(rust)
// Vec<i32> の Vec "<" が比較演算子として見られている
return Vec<i32>::with_capacity(1000); // チェイン失敗
return Vec::<i32>::with_capacity(1000); // メソッド呼び出しと認識してくれる
return Vec::with_capacity(1000); // 返り値が Vec<i32> ならok
// これもcollect "<" が比較演算子として見られている
let ramp = (0..n).collect<Vec<i32>>(); // same error
let ramp = (0..n).collect::<Vec<i32>>(); // ok
let ramp: Vec<i32> = (0..n),collect(); // 変数の型が与えられているのでok
::<..> は Rust コミュニティでターボフィッシュと呼ばれているらしい
ターボフィッシュでぐぐるとルアーが出てくる
形は似てそう
6.9 フィールドと要素
構造体のフィールドには他言語と似たようにアクセスできる
Player.name, Player.rank 等々
タプルの要素は番号で指定する
一番目の要素にアクセスしたいとき
v = (3, 3); => v.0 // -> 3
.演算子の左側は参照, スマートポインタであっても自動的に参照解決してくれる
配列, スライス, ベクタの要素にアクセスするときも他言語と同じ
array[i]
大括弧の左側の値は自動的に参照解決している
代入文の左辺に現れることのできる値を左辺値という
code:lvalue(rust)
let mut n = 0;
n = 1; // n is lvalue.
let mut m = n; // n is lvalue.
配列等から要素を取り出すとき
code:get(rust)
// midpoint から end までを借用する
// second が生存している間ずっと借用しているので array は先に死ねない
.. の前後は省略できその場合は半開区間となる
省略した場合はデータの始点と終点が基準になる
code:半開区間(rust)
.. // 全区間
a.. // start -> a
..b // end -> b
a..b // start -> a, end -> b
6.10 参照演算子
5章でやったやつ
単項演算子*は参照で指される値にアクセスするのに使う
. 演算子がメソッド等にアクセスすると自動的に参照解決するので
参照が指す値全体を書いたり読んだりする時に使う
C におけるポインタの値を読むのと同じ感じ
code:C(c++)
int &n;
*n = 1;
cout << *n << endl; // -> 1
cout << n << endl // -> adress
6.11 算術演算子, ビット演算子, 比較演算子, 論理演算子
基本的なことは他言語と同じ
デバッグビルドでオーバーフローを検知してくれる
チェックさせないこともできる
warapping_add 等のメソッドがある
ビット反転には ~ ではなくて, ! を使う
偽を判定する時に !n はエラーになるということ
ビットシフトは, 符号付き整数であれば符号拡張, 符号なしならゼロ拡張を行う
ビット単位演算は比較演算よりも高い優先度を持つ
code:優先順位(rust)
x & BIT != 0; // という比較は
// C だと
x & (BIT != 0)
// Rust だと
(x & BIT) != 0
// になる
6.12 代入
インクリメント ++ やデクリメント -- はない
連鎖代入はできない a = b = 3 とか
値は所有権ごと移るので所有権辺りに気をつける必要がある
6.13 型キャスト
型キャストは明示的に行う必要あり
型キャストは as キーワードで行える
code:キャスト(rust)
let x = 17;
let index = x as usize; // usize にコンバート
組み込み数値型の間ではキャストが可能
i64 -> i32 へは丸めが行われる
code:covert(rust)
let v: i64 = 1000000000000000000; // 1e18
let vv = v as i32; // コンパイルは通るが値は壊れる -> -1486618624
当然逆もok
符号付きなら幅の広い符号付きへ
符号なしならゼロ拡張される
浮動小数点値から小さすぎる整数値へのキャストは未定義動作になっている
これはコンパイラのバグ
code:未定義(rust)
let v = 1.04E+17 as u8;
安全でないポインタ型に関するキャストも許される
bool 型, char 型, enum 型は任意の型にキャストできる
その逆はだめ
例えば u16 から char へキャストが禁止されているのは
0xd800 のような値が Unicode のサロゲートコードポイントになるため
標準ライブラリの std::char::from_u32() は Option<char> を返すらしい
Unicode テキストはライブラリに頼ろう!
u8 は char にキャスト可能
u8 のすべての取りうる整数は Unicode のコードポイントになるから
参照型に関する型変換は単純なのでキャストなしでok
mut 参照から mut でない参照へのキャストは自動的に行われる
型 &String の値は, 型 &str に自動変換される
型 &Vec<i32> の値は &[i32] に自動変換される
これらは Deref 自動変換型 (deref coercions) と呼ばれる
詳しくは13章
6.14 クロージャ
なんもわからん
軽量関数のような値をクロージャーという
通常クロージャーは | で囲われる
code:例(rust)
let is_even = |x| x % 2 == 0; // bool 型が is_even に入る
// 明示的に書くことも可能
let is_even = |x :u64| -> bool {x % 2 == 0};
クロージャーは関数呼び出しと同じ構文
詳しくは14章
6.15 優先順位と結合性
構文例例は pp138 - 139 参照
連鎖する演算子は左結合で処理させる
連鎖できる演算子は他言語とほぼ同じ
6.16 その先へ
式とは, 我々が「実行するコード」と考える
式はコンパイルされる部分だし, 言語全体から見ると一部
プログラムは仕事するだけではない
プログラムとユーザは対話する必要がある
テスト可能でなければならない
改善するために整理され柔軟でなければならない
他サービスやコードと連携できなければならない
こうしたことに対応するためによりよいデータ構造はこれ以降(9章)で説明