Rust 整理 1

公開日:2024-10-28


はじめに

  • Rustを学習していく中で、自身の中でまとめきれていない部分を整理していく。

メソッドと関連関数

  • 特定の型構造体やenumなどに紐づく形で定義できる関数をメソッドや関連関数と呼ぶ。
  • メソッド .. インスタンスに紐づく関数のため、第一引数に &self を取るもの。
  • 関連関数 .. インスタンスに紐づかない関数で、その型と関係が深い関数を定義する際に使用する。Rustの構造体にはコンストラクタがないので、慣習で構造体の生成を担う関連関数newを定義することが多い。
  • コード例)
    // 構造体
    pub struct User {
        name: String,
        age: u32,
    }
    
    // User構造体にメソッド、関連関数を実装
    impl User {
        // 関連関数
        pub fn new(name: String, age: u32) -> Self {
            Self { name, age }
        }
        // メソッド
        pub fn desc(&self) -> String {
            format!("{} {}", self.name, self.age)
        }
    }
    
    fn main() {
        // 関連関数を使ってUser構造体のインスタンスを生成
        let user = User::new("Taro".to_string(), 20);
        // メソッドを呼び出し
        println!("{}", user.desc());
    }
    

トレイト

  • 共通の振る舞いを抽象的に定義する機能。TypeScriptやGolangのインターフェースに近い。
  • トレイトを利用することで、異なる型に共通のインターフェースを提供し、コードの抽象化を促進することができる。たとえば、NewsArticle と Tweet は異なるデータ構造だが、Summary トレイトを通じて共通の機能である「要約」を提供している。これにより、型が異なるオブジェクトを同じように扱うことができ、柔軟なコード設計が可能になる。
  • トレイトにはデフォルト実装が可能。特定の方にトレイトを実装する際に、デフォルト実装を保持するか、オーバーライドするかを選択できる。
  • コード例)
    // トレイトの定義
    pub trait Summary {
        // メソッドの定義
        fn summarize(&self) -> String;
        // デフォルト実装
        fn default(&self) -> String {
            String::from("(Read more...)")
        }
    }
    
    // 構造体
    pub struct NewsArticle {
        pub headline: String,
        pub location: String,
        pub author: String,
        pub content: String,
    }
    
    // SummaryトレイトをNewsArticle構造体に実装
    impl Summary for NewsArticle {
        fn summarize(&self) -> String {
            format!("{}, by {} ({})", self.headline, self.author, self.location)
        }
    }
    
    // 構造体
    pub struct Tweet {
        pub username: String,
        pub content: String,
        pub reply: bool,
        pub retweet: bool,
    }
    
    // SummaryトレイトをTweet構造体に実装
    impl Summary for Tweet {
        fn summarize(&self) -> String {
            format!("{}: {}", self.username, self.content)
        }
    }
    
    fn main() {
        let newsarticle = NewsArticle {
            headline: String::from("hoge1"),
            location: String::from("hoge2"),
            author: String::from("hoge3"),
            content: String::from("hoge4"),
        };
        let tweet = Tweet {
            username: String::from("peco1"),
            content: String::from("peco2"),
            reply: false,
            retweet: false,
        };
        // 同じトレイトを実装しているため、同じメソッドを呼び出せる
        // また、トレイトではデフォルト実装もされているため、defaultメソッドが呼び出せる
        println!("{}{}", newsarticle.summarize(), newsarticle.default());
        println!("{}{}", tweet.summarize(), tweet.default());
    }
    

トレイト境界

  • ジェネリック型に対する制約のこと
  • 関数の引数で特定のトレイトを実装した何かを受け取ることが可能で、トレイト境界はジェネリック型パラメータに特定のトレイトが実装されていることを保証する。
  • コード例)
    • notify1notify2の糖衣構文で、シンプルに書くことができる。
    • 複数のトレイトを指定する場合は、notify3のように記載可能。
    • where句を利用することで、notify4のように記載することも可能。
    • 1つのジェネリック型パラメータに複数のトレイトを保証したい場合は、notify5のように記載することも可能。
    
    // Summaryトレイトを引数に取る関数
    // 下記の糖衣構文
    pub fn notify1(item: &impl Summary) {
        println!("Breaking news! {}", item.summarize());
    }
    
    // Summaryトレイトを引数に取る関数
    // Tに対してSummaryトレイトを制約しており、Summaryトレイトが実装された引数を関数にとることを保証
    pub fn notify2<T: Summary>(item: &T) {
        println!("Breaking news! {}", item.summarize());
    }
    
    // SummaryトレイトとSummary2トレイトを引数に取る関数
    // トレイト境界を複数指定すると長くなる
    pub fn notify3<T: Summary, U: Summary2>(item: &T, item2: &U) {
        println!(
            "Summary:{}\nSummary2:{}",
            item.summarize(),
            item2.summarize_author()
        );
    }
    
    // SummaryトレイトとSummary2トレイトを引数に取る関数
    // where句を利用すると読みやすくなる
    pub fn notify4<T, U>(item: &T, item2: &U)
    where
        T: Summary,
        U: Summary2,
    {
        println!(
            "Summary:{}\nSummary2:{}",
            item.summarize(),
            item2.summarize_author()
        );
    }
    
    // SummaryトレイトとSummary2トレイトを引数に取る関数
    // 複数のトレイトは+構文でつなぐことができる
    pub fn notify5<T>(item: &T)
    where
        T: Summary + Summary2,
    {
        println!(
            "Summary:{}\nSummary2:{}",
            item.summarize(),
            item.summarize_author()
        );
    }
    

derive属性

  • 構造体やenumに対して特定のトレイトの基本的な実装を自動生成する機能。

  • 例えば下記のようなコードを考える

    pub struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 10, y: 20 };
        println!("{:?}", p);
    }
    

    結果として、Point構造体に対してDebugトレイトが実装されていないため、コンパイルエラーが発生する。

    cargo run
    
    error[E0277]: `Point` doesn't implement `Debug`
    

    下記コードの場合だと実行可能

    #[derive(Debug)]
    pub struct Point {
        pub x: i32,
        pub y: i32,
    }
    
    fn main() {
        let p = Point { x: 10, y: 20 };
        println!("{:?}", p);
    }
    

    これは、Rustのprintln!("{:?}", ...)構文は、型がDebugトレイトを実装している場合にのみ使用できるため。
    Debugトレイトは、デバッグ出力用の表現{:?}を提供するトレイトで、Rustの標準ライブラリに含まれている。Debugをderiveしない限り、Rustはその型のフィールド情報を取得する方法がないため、println!("{:?}", p);のようなデバッグ出力ができない。
    ただし、println!("{:?}", p.x);のようにi32型のフィールドに対してはDebugトレイトが実装されているため、問題なく出力できる。

  • derive属性としては下記がある。

    トレイト 機能
    Clone クローン可能な型を生成する
    Copy コピー可能な型を生成する
    Eq 等価比較可能な型を生成する
    PartialEq 部分的な等価比較可能な型を生成する
    PartialOrd 部分的な順序比較可能な型を生成する
    Ord 順序比較可能な型を生成する
    Default デフォルト値を生成する
    Hash ハッシュ可能な型を生成する
    Debug デバッグ出力可能な型を生成する
    Serialize シリアライズ可能な型を生成する
    Deserialize デシリアライズ可能な型を生成する

静的/動的ディスパッチ

  • トレイトに対してどのように具象実装を紐づけるかについて2つ手段があり、それが静的ディスパッチと動的ディスパッチ。

  • 簡単にいうと、静的ディスパッチはコンパイル時に具象型を決定する方法で、動的ディスパッチは実行時に具象型を決定する方法。

    静的ディスパッチ 動的ディスパッチ
    コンパイル時に具象型を決定 実行時に具象型を決定
    ジェネリクスを利用 トレイトオブジェクトを利用
    コンパイル時に最適化されるため、生成されるバイナリサイズが大きくなる可能性あり(処理は軽くなる) 実行時にオーバーヘッドが発生する
  • 下記のように、静的ディスパッチの場合は、a: &impl TestTraitを引数に、動的ディスパッチの場合は、a: &dyn TestTraitを引数に指定することで実現できる。

    pub struct A {}
    
    pub trait TestTrait {
        fn test_method(&self) -> String;
    }
    
    impl TestTrait for A {
        fn test_method(&self) -> String {
            String::from("A")
        }
    }
    
    // 静的ディスパッチにて具象実装を紐づける
    pub fn impl_dispatch(a: &impl TestTrait) {
        println!("{}", a.test_method());
    }
    
    // 動的ディスパッチにて具象実装を紐づける
    pub fn dyn_dispatch(a: &dyn TestTrait) {
        println!("{}", a.test_method());
    }
    
    fn main() {
        let a = A {};
        impl_dispatch(&a);
        dyn_dispatch(&a);
    }
    
  • 上記の例だとあまり、静的ディスパッチと動的ディスパッチの違いがわかりにくいが、下記のような例だと違いがわかりやすい。

    • Structに複数のトレイトを持たせ、静的ディスパッチで具象実装を紐づける場合
    // 一つ目のトレイト
    trait RequestClient {
        fn get(&self);
    }
    
    // 二つ目のトレイト
    trait Logger {
        fn log(&self, message: &str);
    }
    
    // A構造体に2つのトレイトを持たせる
    struct A<T: RequestClient, L: Logger> {
        client: T,
        logger: L,
    }
    
    // A構造体にメソッドを実装
    impl<T: RequestClient, L: Logger> A<T, L> {
        pub fn perform_request(&self) {
            self.client.get();
            self.logger.log("Request performed");
        }
    }
    
    struct HttpClient;
    impl RequestClient for HttpClient {
        fn get(&self) {
            println!("HttpClient GET request");
        }
    }
    
    struct ConsoleLogger;
    impl Logger for ConsoleLogger {
        fn log(&self, message: &str) {
            println!("Log: {}", message);
        }
    }
    
    fn main() {
        let client = HttpClient;
        let logger = ConsoleLogger;
        let a = A { client, logger };
        a.perform_request();
    }
    
    • Structに複数のトレイトを持たせ、動的ディスパッチで具象実装を紐づける場合
    // 一つ目のトレイト
    trait RequestClient {
        fn get(&self);
    }
    
    // 二つ目のトレイト
    trait Logger {
        fn log(&self, message: &str);
    }
    
    // B構造体に2つのトレイトオブジェクトを持たせる
    struct B {
        client: Box<dyn RequestClient>,
        logger: Box<dyn Logger>,
    }
    
    // B構造体にメソッドを実装
    impl B {
        pub fn perform_request(&self) {
            self.client.get();
            self.logger.log("Request performed");
        }
    }
    
    struct HttpClient;
    impl RequestClient for HttpClient {
        fn get(&self) {
            println!("HttpClient GET request");
        }
    }
    
    struct ConsoleLogger;
    impl Logger for ConsoleLogger {
        fn log(&self, message: &str) {
            println!("Log: {}", message);
        }
    }
    
    fn main() {
        let client: Box<dyn RequestClient> = Box::new(HttpClient);
        let logger: Box<dyn Logger> = Box::new(ConsoleLogger);
        let b = B { client, logger };
        b.perform_request();
    }
    
    • 両者で読みやすさが異なり、動的ディスパッチの場合は複数のトレイトを持たせるなどコードの保守性が高まる。
    # 静的ディスパッチ
    - // A構造体に2つのトレイトを持たせる
    - struct A<T: RequestClient, L: Logger> {
    -     client: T,
    -     logger: L,
    - }
    - 
    - // A構造体にメソッドを実装
    - impl<T: RequestClient, L: Logger> A<T, L> {
    -     pub fn perform_request(&self) {
    -         self.client.get();
    -         self.logger.log("Request performed");
    -     }
    - }
    
    # 動的ディスパッチ
    + // B構造体に2つのトレイトオブジェクトを持たせる
    + struct B {
    +     client: Box<dyn RequestClient>,
    +     logger: Box<dyn Logger>,
    + }
    + 
    + // B構造体にメソッドを実装
    + impl B {
    +     pub fn perform_request(&self) {
    +         self.client.get();
    +         self.logger.log("Request performed");
    +     }
    + }
    
    # 静的ディスパッチ
    - fn main() {
    -     let client = HttpClient;
    -     let logger = ConsoleLogger;
    -     let a = A { client, logger };
    -     a.perform_request();
    - }
    
    # 動的ディスパッチ
    + fn main() {
    +     let client: Box<dyn RequestClient> = Box::new(HttpClient);
    +     let logger: Box<dyn Logger> = Box::new(ConsoleLogger);
    +     let b = B { client, logger };
    +     b.perform_request();
    + }
    

エラー伝播

  • ?unwrapなどのエラーを伝播させる方法についてまとめておく。

    エラー伝播 説明 ユースケース
    ? エラーが発生した場合に自動で呼び出し元にエラーを返す 関数の戻り値がResult型で、エラーを呼び出し元に伝播したいとき
    unwrap エラーが発生した場合にパニックを起こす サンプルコードやテストで使用
    expect エラーが発生した場合にパニックを起こすが、エラーメッセージを指定できる エラーが発生したときに詳細なメッセージを表示してデバッグしやすくする場合
    unwrap_or エラーが発生した場合に指定されたデフォルト値を返す デフォルト値が許容され、エラーが発生した場合にその値で処理を続行できる場合

Crates: anyhow

  • https://crates.io/crates/anyhow
  • Rustのエラーハンドリングを簡単にするためのクレート。
  • 下記はanyhowを用いて、ファイルの読み込み処理を行うサンプルコード。
    use anyhow::{Context, Result};
    use std::fs::File;
    use std::io::Read;
    
    fn read_file_content(file_path: &str) -> Result<String> {
        // with_contextで、エラーに対して追加情報を付加する
        // エラーが発生した際には、?で関数から早期リターンする
        let mut file =
            File::open(file_path).with_context(|| format!("Failed to open file: {}", file_path))?;
    
        let mut content = String::new();
        // エラーが発生した際には、?で関数から早期リターンする
        file.read_to_string(&mut content)
            .with_context(|| format!("Failed to read content from file: {}", file_path))?;
    
        // エラーが発生しない場合は、Okでラップして返す
        Ok(content)
    }
    
    fn main() -> Result<()> {
        let content = read_file_content("example.txt")?;
        print!("{}", content);
        Ok(())
    }
    

Crates: thiserror

  • https://crates.io/crates/thiserror
  • 独自型を準備した際に、追加のエラーメッセージをマクロで付与することができる。
  • 下記はanyhowthiserrorを用いてファイルの読み込みを行うサンプルコード
    use anyhow::{Context, Result};
    use std::fs::File;
    use std::io::Read;
    use thiserror::Error;
    
    // カスタムエラー型を定義
    #[derive(Debug, Error)]
    pub enum MyAppError {
        #[error("File operation failed: {0}")]
        FileError(#[from] std::io::Error), // ファイルエラーのラッピング
    
        #[error("Invalid file format: {0}")]
        FormatError(String), // 独自のエラーメッセージ付きエラー
    }
    
    fn read_file_content(file_path: &str) -> Result<String> {
        // with_contextで、エラーに対して追加情報を付加する
        // エラーが発生した際には、?で関数から早期リターンする
        let mut file = File::open(file_path)
            .map_err(MyAppError::FileError)
            .with_context(|| format!("Failed to open file: {}", file_path))?;
    
        let mut content = String::new();
        // エラーが発生した際には、?で関数から早期リターンする
        file.read_to_string(&mut content)
            .with_context(|| format!("Failed to read content from file: {}", file_path))?;
    
        if !content.starts_with("HEADER") {
            // MyAppError::FormatError("Missing header".to_string()).into()でMyAppErrorをanyhow::Errorに変換
            return Err(MyAppError::FormatError("Missing header".to_string()).into());
        }
    
        Ok(content)
    }
    
    fn main() -> Result<()> {
        match read_file_content("example.txt") {
            Ok(content) => print!("{}", content),
            Err(e) => {
                // エラーチェーン全体を出力
                println!("Error: {:?}", e);
                // エラーの詳細(原因)を確認
                for cause in e.chain() {
                    println!("Caused by: {}", cause);
                }
            }
        }
        Ok(())
    }
    

Crates: axum

  • https://crates.io/crates/axum
  • HTTPサーバを実装する際に使用できるクレート。
  • HTTPリクエストがどのパスに該当するかを識別するルーターと、割り当てられたリクエストを処理するハンドラを提供する。
  • 内部的に他の処理はtowerhypertokioといったクレートに依存している。

Crates: tokio

参考


😄 END 😀