こんにちは、Rustプログラマの皆さん。今日はRustのwhereキーワードを使って、Trait境界を定義する方法について説明したいと思います。whereキーワードは、構造体や関数のジェネリック型パラメータが満たすべき制約を明示的に示すために使用されます。これにより、コードの可読性が向上し、型システムがコンパイル時に適切な制約を確認できるようになります。
Trait境界は、ジェネリック型や関数に対して特定のトレイトを実装することを要求するために使用されます。これにより、型安全性が向上し、コンパイル時に制約が満たされていることが保証されます。Trait境界を使用する主なシーンは以下のとおりです。
- ジェネリック型の制約 : ジェネリック型を定義する際、型パラメータに特定のトレイトを実装させることで、その型が持つべき振る舞いや性質を制限できます。これにより、ジェネリック型を使用する際に適切な型が渡されることが保証されます。
例:
pub struct MyStruct<T: Display> {
value: T,
}
- ジェネリック関数の制約 : ジェネリック関数を定義する際に、型パラメータに特定のトレイトを実装させることで、関数内で呼び出されるメソッドやプロパティが確実に存在することを保証できます。これにより、関数の引数に適切な型が渡されることが確認できます。
例:
fn print_value<T: Display>(value: T) {
println!("{}", value);
}
- 条件付き実装 : あるトレイトが別のトレイトを実装している場合に限り、特定の機能を提供するように実装を制限できます。これにより、特定の条件下でのみ実装が有効になり、コードの柔軟性が向上します。
例:
impl<T: Display> MyTrait for MyStruct<T> {
// ...
}
- 複数のトレイト境界 : 型パラメータが複数のトレイトを実装することを要求する場合にも、Trait境界を使用できます。これにより、型が複数の振る舞いや性質を持つことが保証されます。
例:
fn process_data<T: Read + Write>(data: T) {
// ...
}
これらのシーンにおいて、Trait境界はコードの安全性と可読性を向上させ、コンパイル時に適切な制約が満たされていることを確認できるようになります。
Trait境界を使ったより具体的な例
以下のコード例を見てみましょう。
pub struct RealOnboardingApi<
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
> {
usecase: CreateAppthrustOwner<A, B, C>,
}
ここでは、RealOnboardingApi
という構造体が定義されています。この構造体は、3つのジェネリック型パラメータA
、B
、C
を持っています。各型パラメータには、それぞれKeycloak + Sync + Send
、InstallationTokenVerificationApi + Sync + Send
、KeycloakAdminPasswordProvider + Sync + Send
といったTrait境界が指定されています。これにより、この構造体をインスタンス化する際に、型パラメータがこれらの制約を満たすことが確実になります。
次に、impl
ブロックを見てみましょう。
impl<A, B, C> RealOnboardingApi<A, B, C>
where
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
{
//...
}
impl
ブロックでも、where
キーワードを使ってTrait境界を明示しています。これにより、実装されるメソッドでこれらの制約が適用されることが保証されます。
最後に、async_trait
マクロを使ってOnboardingApi
トレイトを実装する部分を見てみましょう。
#[async_trait::async_trait]
impl<
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
> OnboardingApi for RealOnboardingApi<A, B, C>
where
Self: 'static,
{
//...
}
ここでも、where
キーワードを使ってTrait境界を指定しています。また、Self: 'static
という制約を追加しています。これは、RealOnboardingApi
の実装がスタティックライフタイム('static
)であることを要求しています。これは、RealOnboardingApi
のインスタンスがプログラムの実行中にドロップされず、永続的に存在することを示しています。
where
キーワードを使用することで、コードの可読性が向上し、型パラメータに対する制約が明確になります。また、コンパイル時に型チェックが行われるため、実行時エラーを防ぐことができます。
まとめると、Rustのwhere
キーワードを使用してTrait境界を定義する方法は以下のとおりです。
- 構造体や関数のジェネリック型パラメータに対して、
where
キーワードを使ってTrait境界を指定します。 -
impl
ブロックで、同様にwhere
キーワードを使ってTrait境界を指定し、実装されるメソッドに適用される制約を明示します。 - 必要に応じて、
where
キーワードを使って追加の制約(例えばSelf: 'static
)を指定します。
これらの手順に従って、Rustのwhere
キーワードを使用して、コードの可読性と安全性を向上させることができます。
なぜ Where 'staticが必要になるのか?
Self: 'static
が必要になる理由は、RealOnboardingApi
の実装が非同期メソッドを含むためです。async_trait
マクロを使用すると、トレイトの非同期メソッドはボックス化された動的ディスパッチに変換されます。この変換の結果、トレイトの実装に寿命に関する制約が追加される場合があります。
RealOnboardingApi
の例では、OnboardingApi
トレイトに非同期メソッドが含まれており、async_trait
マクロを使用しています。このため、RealOnboardingApi
の実装に寿命に関する制約が追加されます。
Self: 'static
という制約は、RealOnboardingApi
の実装がスタティックライフタイム('static
)であることを要求しています。これは、RealOnboardingApi
のインスタンスがプログラムの実行中にドロップされず、永続的に存在することを示しています。この制約により、非同期メソッドがボックス化された動的ディスパッチで安全に実行できることが保証されます。
したがって、Self: 'static
制約は、非同期メソッドを含むトレイトの実装が安全に動作することを確保するために必要になります。
実装例全文
pub struct RealOnboardingApi<
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
> {
usecase: CreateAppthrustOwner<A, B, C>,
}
impl<A, B, C> RealOnboardingApi<A, B, C>
where
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
{
pub fn new(usecase: CreateAppthrustOwner<A, B, C>) -> Self {
Self { usecase }
}
}
#[async_trait::async_trait]
impl<
A: Keycloak + Sync + Send,
B: InstallationTokenVerificationApi + Sync + Send,
C: KeycloakAdminPasswordProvider + Sync + Send,
> OnboardingApi for RealOnboardingApi<A, B, C>
where
Self: 'static,
{
async fn create_appthrust_owner(
&self,
request: tonic::Request<CreateAppthrustOwnerRequest>,
) -> Result<tonic::Response<Empty>, tonic::Status> {
let message = request.into_inner();
let result = self.usecase.create(Input {
installation_token: message.installation_token,
initial_owner_email: message.email,
initial_owner_password: message.password,
});
if result.is_err() {
println!("{:?}", result);
return Err(tonic::Status::internal("something wrong"));
}
Ok(tonic::Response::new(Empty {}))
}
}