意味上の混同

関数が同じ型の引数を複数受け取る場合、呼び出し箇所からは意図が分かりにくくなります:

// 著作権 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

struct LoginError;
fn login(username: &str, password: &str) -> Result<(), LoginError> {
    // [...]
    Ok(())
}

fn main() {
    let password = "password";
    let username = "username";

    // コードベースの別の箇所で、誤って引数を入れ替えてしまいます。
    // バグ(最善の場合)、セキュリティ脆弱性(最悪の場合)
    login(password, username);
}

newtype パターンを使うと、この種のエラーをコンパイル時に防げます:

// 著作権 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

struct Username(String);
struct Password(String);
struct LoginError;

fn login(username: &Username, password: &Password) -> Result<(), LoginError> {
    // [...]
    Ok(())
}

fn main() {
    let password = Password("password".into());
    let username = Username("username".into());
    login(password, username); // 🛠️❌
}
  • 両方の例を実行して、元の例は正常にコンパイルされ、変更後の例ではコンパイラエラーが返されることを受講者に示してください。

  • _意味上の_観点を強調してください。newtype パターンは、異なる概念には異なる型を使うために活用すべきであり、これによってこの種のエラーを完全に排除できます。

  • ただし、関数が同じ型の引数を複数受け取ることが正当なシナリオもある点には注意してください。そのようなシナリオで正しさが最重要であるなら、入力として名前付きフィールドを持つ struct の使用を検討してください:

    #![allow(unused)]
    fn main() {
    // 著作権 2025 Google LLC
    // SPDX-License-Identifier: Apache-2.0
    
    pub struct LoginArguments<'a> {
        pub username: &'a str,
        pub password: &'a str,
    }
    fn login(i: LoginArguments) {}
    let password = "password";
    let username = "username";
    
    // 問題を見つけるために `login` 関数の定義を確認する必要はありません。
    login(LoginArguments {
        username: password,
        password: username,
    })
    }

    ユーザーは呼び出し箇所で各フィールドに値を割り当てることを強制されるため、バグに気づける可能性が高まります。