1 Rustの所有権規則について説明し、それがクローン(clone)、借用(borrow)、アプリケーション状態の共有といったバックエンドにおける実践的な選択にどう影響するかを述べてください。
Rustでは、各値に単一の所有者(owner)が存在します。`Copy` トレイトを実装していない値をムーブすると所有権が移動し、所有者がスコープを抜けると値はドロップ(破棄)されます。コードは、共有参照を通じて不変(イミュータブル)にデータを借用するか、排他的な可変参照を通じて可変(ミュータブル)に借用するかのいずれかを選択できます。バックエンドコードでは、この規則によって、関数が所有権を受け取るか、一時的に借用するか、独立した所有値を手に入れるためにクローンするか、あるいは変更の共有が必要な場合に同期機構などの並行性プリミティブを伴って `Arc` などのハンドル経由で長寿命な状態を共有するかといった設計判断が形作られます。
use std::sync::Arc;
struct AppState {
service_name: String,
}
async fn handler(state: Arc<AppState>, request_id: String) -> String {
// Borrow: no ownership transfer of the String inside state.
let name: &str = &state.service_name;
// Clone only if an owned independent value is needed.
let owned_name = name.to_owned();
format!("{owned_name}:{request_id}")
}
fn configure(state: Arc<AppState>) {
// Cloning Arc increments the reference count; it does not clone AppState.
let state_for_route = Arc::clone(&state);
let _ = state_for_route;
}
AI コーチを使ってこの質問に答えてみる
2 &T と &mut T による借用の違いは何ですか?また、それらの規則は API (Application Programming Interface) やハンドラのシグネチャにどのように影響しますか?
`&T` は共有参照(shared reference)です。読み取り専用のアクセスを許可し、同じ値に対する複数の共有参照が同時に存在できます。`&mut T` は排他的な可変参照(exclusive mutable reference)です。値の変更を許可しますが、これが有効である間は、同じ値に対する他の共有参照や可変参照を一切使用できません。
これらの規則は、読み取り専用の関数には `&T` や `&str` のようなより狭い借用型を受け取らせ、値を変更する関数には `&mut T` を受け取らせるか、所有権を移動(ムーブ)させるか、あるいは内部可変性(interior mutability)や同期機構を利用させるという形でAPIの設計に影響を与えます。バックエンドのハンドラでは通常、並行するリクエスト間で共有アプリケーション状態への単純な `&mut` アクセスを行うことは避け、共有ハンドルと同期機構の組み合わせやその他の所有権パターンを採用します。
struct User {
name: String,
login_count: u64,
}
fn display_name(user: &User) -> &str {
&user.name
}
fn record_login(user: &mut User) {
user.login_count += 1;
}
fn main() {
let mut user = User { name: "Ada".into(), login_count: 0 };
let name = display_name(&user); // shared borrow for reading
println!("{name}");
record_login(&mut user); // exclusive mutable borrow for mutation
}
AI コーチを使ってこの質問に答えてみる
3 Box はどのように機能し、Box を介したヒープ割り当てによってどのような問題が解決されますか?
`Box<T>` は所有権を持つスマートポインタであり、`Box` ハンドル自体はポインタと同等の固定サイズの値である一方、実体である `T` はヒープ上に格納されます。単一の所有権を持ち、スコープを抜ける際にヒープ割り当てを破棄(解放)します。`Box` は間接参照を提供するため、サイズの大きな値、コンパイル時に既知のサイズが必要な再帰型、動的サイズ型、および `Box<dyn Trait>` などのトレイトオブジェクトを扱う際に有用です。`Box` をムーブすると、ヒープに格納された値自体をムーブまたはコピーすることなく、メモリ割り当ての所有権のみが移譲されます。
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
AI コーチを使ってこの質問に答えてみる
4 Result型とOption型について説明し、値の不在や失敗する可能性のある操作を処理するための主要で慣用的な方法を述べてください。
`Option<T>` は `Some(T)` または `None` のいずれかを表し、値が存在しないことが想定される場合に使用されます。`Result<T, E>` は `Ok(T)` または `Err(E)` のいずれかを表し、エラー情報が重要となる失敗する可能性のある操作に使用されます。慣用的な処理方法としては、`match`、`if let`/`let else`、`map`、`and_then`、`ok_or`、`unwrap_or`、`unwrap_or_else` などのコンビネータ、および互換性のある `Option` や `Result` 型を返す関数から早期リターンを行うための `?` 演算子があります。`unwrap` や `expect` は、不変条件の表明、テスト、プロトタイプ作成、または回復不能な状況に留めるのが最適であり、バックエンドにおける通常の回復可能なエラー処理には使用すべきではありません。
fn parse_port(s: Option<&str>) -> u16 {
s.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(8080)
}
assert_eq!(parse_port(Some("3000")), 3000);
assert_eq!(parse_port(None), 8080);
AI コーチを使ってこの質問に答えてみる
5 ? 演算子はどのように動作し、異なるエラー型同士の変換は何によって可能になりますか?
`?` 演算子は、エラー伝播を簡潔に記述するための構文です。`Result` 型に対して `expr?` を適用すると、`Ok` の値を取り出して処理を継続するか、あるいは `Err` を呼び出し元関数へ早期リターンします。`Option` 型に対しては、`Some` の値を取り出すか、`None` をリターンします。このとき、外側の関数は互換性のある型を返す必要があります。`Result` では、発生したエラーが `From` / `Into` スタイルの変換を通じて関数の戻り値のエラー型へと変換されるため、異なるエラー型同士を合成できます。この変換は手動で実装されることもあれば、`thiserror` などのヘルパークレートを用いて自動導出されることもよくあります。
use std::{fs, io, num::ParseIntError};
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn read_number(path: &str) -> Result<i32, AppError> {
let s = fs::read_to_string(path)?; // io::Error -> AppError
let n = s.trim().parse::<i32>()?; // ParseIntError -> AppError
Ok(n)
}
AI コーチを使ってこの質問に答えてみる
6 enumに対するパターンマッチングについて説明し、網羅的なマッチング(exhaustive matching)がドメインやAPIの正当性をどのように向上させるかを述べてください。
enumは固定されたバリアント(variant)の集合のうち、いずれか1つの値を取り得るデータ構造をモデル化し、各バリアントはデータを保持できます。`match`式はバリアントごとに分岐し、内部のデータを分配束縛(デストラクト)できます。Rustでは、ワイルドカードや包括パターン(catch-all pattern)が明示的に使用されていない限り、enumのマッチングが網羅的であるかをコンパイラが検証します。これにより、新しい状態やレスポンスのバリアントが追加された際、それをマッチングしているコードに対して暗黙的に無視するのではなく、その新しいケースをどのように処理するかを明示的に決定することをコンパイラが強制するため、ドメインやAPIの正当性が向上します。
enum PaymentStatus {
Pending,
Paid { receipt_id: String },
Failed(String),
}
fn message(status: PaymentStatus) -> String {
match status {
PaymentStatus::Pending => "Payment is pending".to_string(),
PaymentStatus::Paid { receipt_id } => format!("Paid, receipt {receipt_id}"),
PaymentStatus::Failed(reason) => format!("Payment failed: {reason}"),
}
}
AI コーチを使ってこの質問に答えてみる
これらの回答を声に出して練習する準備はできていますか?
声で回答し、技術の深さ、構成、英語でのコミュニケーションについてフィードバックを受け取れます。
7 Newtypeパターン(newtype wrappers)とは何ですか。また、ID、金額、トークン、シークレット情報の型安全性をどのように向上させますか?
Newtypeラッパーとは、既存の内部表現をラップする独立したRustの型であり、多くの場合 `struct UserId(Uuid);` や `struct AccessToken(String);` のように単一フィールドのタプル構造体として定義されます。
これにより、コンパイラが基底のデータ型を共有する意味的に異なる値(例えば `UserId` と `OrderId`、セントとドル、未検証の文字列と検証済みトークンなど)を取り違えないようになるため、型安全性が向上します。また、Newtypeを利用することで、値の生成やバリデーション規則を集約できるほか、`Display`、`Debug`、`Serialize` などのトレイトの実装や、シークレット情報のマスキング(redaction)処理を個別に制御できます。
use uuid::Uuid;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct UserId(Uuid);
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct OrderId(Uuid);
struct SecretToken(String);
impl std::fmt::Debug for SecretToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SecretToken(**redacted**)")
}
}
fn load_user(id: UserId) {
// query by user id
}
fn example(user_id: UserId, order_id: OrderId) {
load_user(user_id);
// load_user(order_id); // does not compile: expected UserId, found OrderId
}
AI コーチを使ってこの質問に答えてみる