💊
Rustのメソッドの第一引数は&selfとselfどちらを使用すべきか
こんにちは。
今回はRustを勉強中の開発者が一度は考えるであろう、メソッドの第一引数は&self
とself
どちらを使用すべきか、についてまとめておきます。
結論
- 基本的には
&self
を使って実装する。 - インスタンスの所有権を奪って、新しい何かに変換して返却するような場合には
self
を活用する。
そもそも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;
}
}
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
「構造体(enumやトレイトも)に紐づけて定義される関数」だと関連関数 ( associated function ) の説明にしか見えないです…
例えば、「構造体(enumやtraitも)のインスタンスに対して呼び出せる関数」みたいな表現でどうでしょうか?
self
自体はメソッドの呼び出し対象のインスタンスを指す keyword なので、この説明だと誤解を招きかねないように思います。例えば、「第一引数に
self
をとって定義する必要があり、」としてSelf
で利用できるself: Self
→self
、self: &/&mut Self
→&/&mut self
という糖衣構文があるといった説明を足すのはどうでしょう?
コメントいただきありがとうございます!
こちらおっしゃる通りですね。コメントいただいたように記載を修正しました🙏
インスタンスに対して、が伝わるように文言を追加しました。
こちらは説明を簡略化しつつ、具体的にどんな記述の糖衣構文なのかがわかるように修正しました。