独立した借用のための構造体の分解
説明
大きな構造体は、借用チェッカーに関する問題を引き起こすことがあります。フィールドは独立して借用できますが、構造体全体が一度に使用されることになり、他の使用を妨げてしまう場合があります。解決策として、その構造体をいくつかの小さな構造体に分解することが考えられます。その後、それらを組み合わせて元の構造体を構成します。すると、それぞれの構造体を個別に借用でき、より柔軟な振る舞いが可能になります。
これは、多くの場合、他の面でもより良い設計につながります。このデザインパターンを適用すると、より小さな機能単位が明らかになることがよくあります。
例
借用チェッカーによって、構造体を使用する計画が妨げられる作為的な例を次に示します。
struct Database {
connection_string: String,
timeout: u32,
pool_size: u32,
}
fn print_database(database: &Database) {
println!("Connection string: {}", database.connection_string);
println!("Timeout: {}", database.timeout);
println!("Pool size: {}", database.pool_size);
}
fn main() {
let mut db = Database {
connection_string: "initial string".to_string(),
timeout: 30,
pool_size: 100,
};
let connection_string = &mut db.connection_string;
print_database(&db);
*connection_string = "new string".to_string();
}
コンパイラは次のエラーを出力します。
let connection_string = &mut db.connection_string;
------------------------- mutable borrow occurs here
print_database(&db);
^^^ immutable borrow occurs here
*connection_string = "new string".to_string();
------------------ mutable borrow later used here
このデザインパターンを適用して、Database を 3 つの小さな構造体にリファクタリングすることで、借用チェックの問題を解決できます。
// Database は現在、ConnectionString、Timeout、PoolSize の 3 つの構造体で構成されています。
// これを小さな構造体に分解しましょう
#[derive(Debug, Clone)]
struct ConnectionString(String);
#[derive(Debug, Clone, Copy)]
struct Timeout(u32);
#[derive(Debug, Clone, Copy)]
struct PoolSize(u32);
// その後、これらの小さな構造体を再び `Database` に組み合わせます
struct Database {
connection_string: ConnectionString,
timeout: Timeout,
pool_size: PoolSize,
}
// print_database は、代わりに ConnectionString、Timeout、Poolsize 構造体を受け取れるようになります
fn print_database(connection_str: ConnectionString, timeout: Timeout, pool_size: PoolSize) {
println!("Connection string: {connection_str:?}");
println!("Timeout: {timeout:?}");
println!("Pool size: {pool_size:?}");
}
fn main() {
// 3 つの構造体で Database を初期化します
let mut db = Database {
connection_string: ConnectionString("localhost".to_string()),
timeout: Timeout(30),
pool_size: PoolSize(100),
};
let connection_string = &mut db.connection_string;
print_database(connection_string.clone(), db.timeout, db.pool_size);
*connection_string = ConnectionString("new string".to_string());
}
動機
このパターンは、独立して借用したいフィールドを多数持つようになってしまった構造体がある場合に最も有用です。その結果、最終的により柔軟な振る舞いが得られます。
利点
構造体を分解することで、借用チェッカーの制限を回避できます。また、多くの場合、より良い設計になります。
欠点
より冗長なコードにつながる可能性があります。また、小さな構造体が適切な抽象化にならない場合もあり、その結果、より悪い設計になってしまうことがあります。それはおそらく「コードスメル」であり、プログラムを何らかの形でリファクタリングすべきであることを示しています。
議論
このパターンは、借用チェッカーを持たない言語では必要ないため、その意味では Rust に固有です。しかし、より小さな機能単位を作ることは、多くの場合、よりクリーンなコードにつながります。これは、言語に依存しない、ソフトウェア工学で広く認められている原則です。
このパターンは、Rust の借用チェッカーがフィールドを互いに独立して借用できることに依存しています。この例では、借用チェッカーは a.b と a.c が別々のものであり、独立して借用できることを認識しています。a 全体を借用しようとはしません。もしそうであれば、このパターンは役に立たなくなってしまいます。