Open5

Rust学習メモ2

Kentaro AzumaKentaro Azuma

クレート

  • クレートはバイナリかライブラリのいずれかに分類される
  • クレートは、関連した機能を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文字列を使用し、クレートルートからスタートする
  • 相対パスは、selfsuperまたは今のモジュール内の識別子を使用し、現在のモジュールからスタートする
  • 絶対パスも相対パスも、その後に::で区切られた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::*;のように書くとすべての公開要素を持ち込むことができる
Kentaro AzumaKentaro Azuma

エラー処理

  • Rustでは、回復可能なエラーと回復不能なエラーの2種類のエラーがある
  • Rustには例外の機構が存在しない

回復不能なエラー

  • 回復不能なエラーの例には、ファイルが見つからない、配列の範囲外アクセスなどがある
  • 回復不能なエラーにはpanic!マクロがある

panic!

  • 環境変数RUST_BACKTRACE=1を設定すると、呼び出された全関数の一覧を表示する
  • 標準では、パニックが発生すると、プログラムはスタックを遡り各関数のデータを片付ける(巻き戻し)

回復可能なエラー

  • 回復可能なエラーにはResult<T,E>値がある
  • Result型ではOkErrの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)
}
Kentaro AzumaKentaro Azuma

ジェネリック型

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
}
Kentaro AzumaKentaro Azuma

トレイト

トレイトはある特定の型が持っていて、他の型と共有できる機能を定義する。トレイトを使用することで、共有動作を抽象的な方法で定義することができる。また、トレイト境界(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
{
Kentaro AzumaKentaro Azuma

ライフタイム

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
    }
}