💊

Rustのメソッドの第一引数は&selfとselfどちらを使用すべきか

2024/11/02に公開
2

こんにちは。
今回はRustを勉強中の開発者が一度は考えるであろう、メソッドの第一引数は&selfselfどちらを使用すべきか、についてまとめておきます。

結論

  • 基本的には&selfを使って実装する。
  • インスタンスの所有権を奪って、新しい何かに変換して返却するような場合にはselfを活用する。

https://doc.rust-jp.rs/book-ja/ch05-03-method-syntax.html#メソッドを定義する

そもそもRustにおけるメソッドとは

  • メソッドは、構造体(enumやトレイトも)のインスタンスに紐づけて定義される関数と言える。
  • Rustのメソッドでは、第一引数で必ずself(self: Self(自身の型)の糖衣構文)として定義する必要があり、関数同様にselfを借用したり、所有権を奪ったり、可変で受けることも可能である。
struct Score {
    v: i32,
}

// 関数
fn compare_score(a: &Score, b: &Score) -> std::cmp::Ordering {
    a.v.cmp(&b.v)
}

// メソッド
impl Score {
    fn compare(&self, other: &Score) -> std::cmp::Ordering {
        self.v.cmp(&other.v)
    }

    fn add(&mut self, other: &Score) {
        self.v += other.v;
    }
}

https://doc.rust-jp.rs/rust-by-example-ja/fn/methods.html

所有権を奪ってselfを使用してメソッドを定義する場合

先述の通り、インスタンスの所有権を奪って、新しい何かに変換して返却するような場合にはselfを利用します。
これによく遭遇する場面としては、私はDTOが絡んだ変換処理が思い浮かびます。

例えば、DDDでWebAPIを開発している場合、Repositoryで利用されるDTOから、あるドメインモデルに変換する場合です。下記は、Repositoryから何らかの情報(今回は顧客情報)を取得する場合を想定し、そのDTOからCustomerドメインに変換する実装です。

schema.rs
// 顧客情報のDTO
#[derive(Debug, Deserialize)]
pub struct CustomerNode {
    pub id: String,
    pub default_address: Option<AddressNode>,
    pub display_name: String,
    pub email: Option<String>,
    pub first_name: Option<String>,
    pub last_name: Option<String>,
    pub state: String,
    // ...
}

impl CustomerNode {
    // DTOからドメインに変換する
    pub fn to_domain(self) -> Result<Customer, DomainError> {
        let status = match self.state.as_str() {
            "ENABLED" => Ok(CustomerStatus::Active),
            "DISABLED" => Ok(CustomerStatus::Inactive),
            _ => Err(DomainError::ConversionError),
        }?;

        Customer::new(
            self.id,
            self.default_address
                .map(|address| address.to_domain())
                .transpose()?,
            self.display_name,
            self.email.map(|email| Email::new(email)).transpose()?,
            self.first_name,
            self.last_name,
            status,
            // ...
        )
    }
}
repository.rs
#[async_trait]
impl CustomerRepository for CustomerRepositoryImpl {
    async fn find_customer_by_email(&self, email: &Email) -> Result<Customer, DomainError> {
        // 
        // GQLによる顧客情報の取得処理
        //
        let graphql_response: GraphQLResponse<CustomerData> = self.client.query(&query).await?;
        if let Some(errors) = graphql_response.errors {
            log::error!("Error returned in GraphQL response. Response: {:?}", errors);
            return Err(DomainError::QueryError);
        }

        // レスポンスから顧客DTOの取り出し
        let node: CustomerNode = graphql_response
            .data
            .customer

        // 所有権を奪って、ドメインに変換する
        node.to_domain()
    }
}

上記の実装では、CustomerNode::to_domain()の第一引数をselfとし、所有権を奪った上でDTOをドメインに変換しています。このように実装することで、repository.rsの中でドメインに変換した後にCustomerNodeは利用できません。これによって、実装ミスでドメインに変換した後にCustomerNodeを更新しちゃって顧客情報の操作がちぐはぐになる、なんてことはできなくなります。

おわりに

基本的には、&selfを使って借用を使って効率よく実装していきたいですが、selfを使って所有権を奪うべき場面も度々ありそうです。

Discussion

kanaruskanarus

そもそもRustにおけるメソッドとは

  • メソッドは、構造体(enumやトレイトも)に紐づけて定義される関数と言える。

    「構造体(enumやトレイトも)に紐づけて定義される関数」だと関連関数 ( associated function ) の説明にしか見えないです…

    例えば、「構造体(enumやtraitも)のインスタンスに対して呼び出せる関数」みたいな表現でどうでしょうか?

  • 第一引数で必ずself(構造体名の糖衣構文)として定義する必要があり、

    self 自体はメソッドの呼び出し対象のインスタンスを指す keyword なので、この説明だと誤解を招きかねないように思います。

    例えば、「第一引数に self をとって定義する必要があり、」として

    • 「自身の型」を Self で利用できる
    • self: Selfselfself: &/&mut Self&/&mut self という糖衣構文がある


    といった説明を足すのはどうでしょう?

こじろうこじろう

コメントいただきありがとうございます!
こちらおっしゃる通りですね。コメントいただいたように記載を修正しました🙏

例えば、「構造体(enumやtraitも)のインスタンスに対して呼び出せる関数」みたいな表現でどうでしょうか?

インスタンスに対して、が伝わるように文言を追加しました。

self 自体はメソッドの呼び出し対象のインスタンスを指す keyword なので、この説明だと誤解を招きかねないように思います。

こちらは説明を簡略化しつつ、具体的にどんな記述の糖衣構文なのかがわかるように修正しました。