Rust学習メモ2
クレート
- クレートはバイナリかライブラリのいずれかに分類される
- クレートは、関連した機能を1つのスコープにまとめる
- クレートルートはクレートのルートモジュールを作るソースファイルを指す
- クレートルートはRustコンパイラの開始点となる
パッケージ
- パッケージは1つ以上のクレートから構成され、特定の機能群を提供する
- 各パッケージはCargo.tomlファイルでクレートをどのようにビルドするか説明する
- 各パッケージ0個か1個のライブラリクレートと、任意の数のバイナリクレートを持つ
モジュール
- モジュールはクレート内のコードをグループ化する
- モジュールは要素のプライバシー(公開か非公開か)を設定する
- デフォルトは非公開で、
pub
キーワードを使用することで公開になる。 - モジュールを定義するには
mod
キーワードに続いてモジュールの名前を指定する - モジュールの中に他のモジュールを定義することができる
- モジュールの中には構造体、enum、定数、トレイト、関数を定義することもできる
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
パス
- パスには絶対パスと相対パスの2種類が存在する
- 絶対パスはクレートの名前または
crate
文字列を使用し、クレートルートからスタートする - 相対パスは、
self
、super
または今のモジュール内の識別子を使用し、現在のモジュールからスタートする - 絶対パスも相対パスも、その後に
::
で区切られた1つ以上の識別子が続く
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
pubキーワードについて
- 構造体やenumも
pub
を使うことによって公開できる - 構造体は公開したいフィールド毎に個別にpubを指定する必要がある
- enumの場合は
enum
の前にpub
を指定するだけでよい
useキーワードについて
-
use
で指定したパスは、use
以降パス内の要素がローカルにあるかのように呼び出せる -
use
で指定するパスは絶対パスでも相対パスでもよい - 関数を
use
するときは親モジュールまでを書くのが慣例になっている - 構造体やenumを
use
で指定するときはフルパスを書くのが慣例になっている - 同じ名前の2つの要素を
use
することはできない -
as
キーワードを使って別名を与えることができる -
use
キーワードでスコープに持ち込んだ名前は非公開になる(依存関係のあるモジュールがあるとエラーになる?) -
pub use
を使うことで回避できる - 共通のパスをまとめて
use std::{self, cmp::Ordering, io};
と書くこともできる -
use std::collections::*;
のように書くとすべての公開要素を持ち込むことができる
エラー処理
- Rustでは、回復可能なエラーと回復不能なエラーの2種類のエラーがある
- Rustには例外の機構が存在しない
回復不能なエラー
- 回復不能なエラーの例には、ファイルが見つからない、配列の範囲外アクセスなどがある
- 回復不能なエラーには
panic!
マクロがある
panic!
- 環境変数
RUST_BACKTRACE=1
を設定すると、呼び出された全関数の一覧を表示する - 標準では、パニックが発生すると、プログラムはスタックを遡り各関数のデータを片付ける(巻き戻し)
回復可能なエラー
- 回復可能なエラーには
Result<T,E>
値がある - Result型では
Ok
とErr
の2つの列挙子が定義されている
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
//ファイルを作成しようとしましたが、問題がありました
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
Result
型のメソッドであるunwrap()
はOk
列挙子の場合にはOk
の中身を返す。
Error
列挙子の場合にはpanic!を実行するが、エラーメッセージは指定できないため、エラーの原因を特定しにくい。これに対してexpect()
は引数にエラーメッセージを指定することができる。
エラーの委譲
Rustでは関数内でエラーを処理する代わりに、 呼び出し元にエラーを返して処理することができる。
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
Rustでは、?
演算子を用いて記述することもできる。
ただし、戻り値でResult
を返す関数内のみにおいて使うことができる。
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
ジェネリック型
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
トレイト
トレイトはある特定の型が持っていて、他の型と共有できる機能を定義する。トレイトを使用することで、共有動作を抽象的な方法で定義することができる。また、トレイト境界(trait bounds)を使って、ジェネリック型が特定の動作を持つ任意の型であることを指定することができる。
トレイトを定義する
trait
キーワードに続いてトレイト名を定義し、このトレイトを実装する型が定義しなければならない動作を定義する。
トレイトには、複数のメソッドを含むことができ、各メソッドシグニチャは行毎に並べられ、各行はセミコロンで終わる。
定義したトレイトを型に実装するには、impl
キーワードトレイト名、for
実装させる型の順番に記述する。
注意するべき制限の1つとして、トレイトか対象の型が自分のクレートに固有であるときのみ、型に対してトレイトを実装できる。つまり、外部のトレイトを外部の型に対して実装することはできない。この制限によって、自分のコードが他人のコードを壊したり、その逆が起きないことを保証する。
pub trait Summary {
fn summarize(&self) -> String;
}
impl Summary for 型名 {
fn summarize(&self) -> String {
//具体的な実装
}
}
トレイトのメソッドに対してデフォルトの動作を定義することもできる。その場合、メソッドのシグニチャだけでなく、処理も定義する。
pub trait Summary {
fn summarize(&self) -> String {
// "(もっと読む)"
String::from("(Read more...)")
}
}
impl Summary for 型名 {}
トレイトを使用する
型の部分にimpl トレイト名
を指定することで、トレイト名を実装している任意の型を表す。
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
トレイト境界構文
impl トレイト名
は糖衣構文であり、実際にはトレイト境界と呼ばれる。
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
複数の引数を持つ関数を定義するときに、両方の引数が同じ型であることを強制するにはトレイト境界を使って表現する必要がある。
pub fn notify(item1: &impl Summary, item2: &impl Summary) {...}
pub fn notify<T: Summary>(item1: &T, item2: &T) {...}
前者はSummaryトレイトを実装する型であればitem1とitem2の型が異なっていても構わないが、後者はitem1とitem2が同じ型でなくてはならない。
複数のトレイト境界を指定する
トレイト名を+
で連結することにより、指定したトレイトの両方を実装していなくてはならないことを表現できる。
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
where句を使ってトレイト境界を読みやすくする
複数のジェネリック型の引数をもつ関数は、関数名と引数リストの間に大量のトレイト境界に関する情報を含むことになり、関数のシグネチャが読みにくい。
そのような場合、where
句を用いて読みやすくすることができる。
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
ライフタイム
Rustの大きな特徴として、Rustにおいて参照はすべてライフタイム(生存期間)を持つ。
ライフタイムとは、その参照が有効になるスコープを指す。
多くの場合、ライフタイムは暗黙的に推論されるが、明示しなければならない場合もある。
ライフタイムの目的はダングリング参照を回避することである。
以下のコードでは、rはxへの参照を保持するが、外側のスコープではxはすでに終了している。
参照の対象が参照よりも長生きしていない。
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
Rustには借用チェッカーがあり、コンパイラはこのコードに対してエラーを出力する。
ライフタイムを注釈する
以下のコードはエラーになる。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
longestが返す参照はx,yのどちらなのか実行するまでわからない。
そしてxとyのライフタイムが戻り値のライフタイムとどう関係するかもわからない?
(string1とstring2とresultはすべて同じライフタイムだから問題ないと思うが、具体的な使用例を調べてエラーとしているのではない?スコープの異なるxとyを使用した場合をあらかじめ考慮してエラーにしている感じ?)
ライフタイムの注釈は次のように記述する。
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
longestにライフタイム注釈を記述すると次のようになる。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}