getter/setter要らない論は、すでにいろいろなところで話されているのですが(その話の是非は置いておいて)、Rustにおいては、getterのせいで参照ルールに引っかかってコンパイルが通らなくなることが多々あるため、わかりやすく害があるよというお話です。
※rustバージョンは、1.60.0 (stable)です。
Rustの参照の規則
RustのBookでは、参照の規則は以下のように説明されています。
- 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
- 参照は常に有効でなければならない。
このルールで特にきついのが他に参照がある場合に、可変参照は存在できない。という点です。Rustと上手に付き合えるかは、このルールに沿った設計ができるかにかかっていると思っています。というのも「他に参照がある場合に」の部分はライフタイム管理の話なのですが、Rustはメソッドを通したライフタイムを完璧に追えるほどには賢くありません。そのため、参照が存在しないにも関わらず、可変参照を作れないということが起きがちなのです。
構造体のフィールドアクセス
構造体のフィールドへのアクセスは、構造体への参照がない限り、フィールドごと独立して行われます。別の言い方をすると、フィールドごとに別々に所有権があるように扱われます。つまり、以下のコードは、コンパイルが通ります。
struct MyStruct {
a: String,
b: String,
c: String,
}
let mut x = MyStruct { a: "a".to_string(), b: "b".to_string(), c: "c".to_string() };
// それぞれのフィールドは独立して所有権を持っている(cのように所有権のムーブも可能)
let (a, b, c) = (&x.a, &mut x.b, x.c);
構造体への参照がある場合、構造体のフィールドも全て借用されている扱いとなります。つまり、以下のコードは、コンパイルが通りません。
struct MyStruct {
a: String,
b: String,
c: String,
}
let mut x = MyStruct { a: "a".to_string(), b: "b".to_string(), c: "c".to_string() };
// aへのアクセスに&xが使われているため、bとcへはアクセス禁止
let (a, b, c) = (&(&x).a, &mut x.b, x.c);
getterの弊害
以上を踏まえて、getterを使用したときはどうなるでしょうか。
struct MyStruct {
a: String,
b: String,
}
impl MyStruct {
fn a(&self) -> &str {
self.a.as_ref()
}
fn a_mut(&mut self) -> &mut String {
&mut self.a
}
fn b(&self) -> &str {
self.b.as_ref()
}
fn b_mut(&mut self) -> &mut String {
&mut self.b
}
}
getterをこのように定義します。この定義からわかるように、getterを使用したフィールドへのアクセスは、構造体全体を借用して(&self
や&mut self
)行います。また、戻り値のライフタイムは、&self
のライフタイムとなるので、getterの戻り値が有効な間は、構造体全体も借用されている扱いとなります。
構造体全体の借用を要求しているということは、構造体全体もしくは一部への可変参照が存在する間は、getterは使えなくなります。
let mut x = MyStruct { a: "a".to_string(), b: "b".to_string() };
// x.a()で&selfを使っているため、x.b_mut()は使えない
let (a, b) = (x.a(), x.b_mut());
let mut x = MyStruct { a: "a".to_string(), b: "b".to_string() };
// x.a()で&selfを使っているため、&mut x.bは使えない
let (a, b) = (x.a(), &mut x.b);
let mut x = MyStruct { a: "a".to_string(), b: "b".to_string() };
// x.b_mut()で&mut selfを使っているため、&x.aは使えない
let (a, b) = (&x.a, x.b_mut());
このように、各フィールド単位では参照の規則に沿っているのに、getterというメソッドを通したせいで参照の規則に引っかかるのです。フィールドの変更時に他のフィールドを参照しないなら問題ないのですが、将来、他のフィールドを参照するよう変更しないとも限りません。なので、可変な構造体についてはgetterを使わない方向で設計したほうがうまくいくと思います。
更に言うと、構造体が可変なタイミングと、不変なタイミングははっきりと分けたほうが扱いやすくなります。getterに限らず、参照を返すメソッドは当たり前に存在します。なので、常に可変な状態だと、変更したいときに不変参照が残っている可能性があり、面倒です。RustでBuilderパターン等がよく使われるのは、おそらくこういう理由だと思います。
getter(とsetter)を使わない
フィールドの変更処理は、1つのトランザクションとして自分のメソッドにまとめる
getterを使わないということは、フィールドが直接見える場所で変更を行う必要があります。なので、変更処理は極力外部の関数・メソッドに任せず、自分のメソッドで実装しましょう。変更処理を全て自分のメソッドで記述すると、setterも不要になります。
また、変更処理を行うときに自己のフィールドを参照する場合、必ず、変更メソッド内でフィールドの参照を取得しましょう。引数として参照を受け取る可能性があると、Self
が借用される関係で渡せなくなりなります。
/// 変更メソッド
pub fn update(&mut self, name: &str) {
// フィールドの参照は変更メソッド内部で取得する。その際、Selfの参照はつくらないこと。
let a = &self.a;
self.b = format!("hello! {} & {}", a, name).to_string();
}
外部に変更を任せる場合は、素直にフィールドをpublicにする
とはいえ、自分のメソッド内で変更するのは都合が悪い場合もあります。変更する箇所が外部モジュールになる場合は、素直にフィールドをpublicにして、その場所から見えるようにしましょう。getterを作る目的として、フィールドを直接見せないというのが一番にあると思いますが、私としては、フィールドをクレート内部に公開するくらいなら何も問題ないと思います(この辺は宗教的ですが)。
mod a {
pub struct MyStructA {
a: String,
b: String,
}
impl MyStructA {
pub fn update(&mut self, b: &mut super::b::MyStructB) {
self.a = b.a.clone();
// 他のモジュールからフィールドを直接参照できる
b.b = self.b.clone();
}
}
}
mod b {
pub struct MyStructB {
// フィールドをクレート内部に公開する
pub(crate) a: String,
pub(crate) b: String,
}
}
どうしても直接見せたくない場合は、参照専用の構造体を作る
どうしてもフィールドを直接見せたくない場合は、参照専用の構造体を作るといいかもしれません。
pub struct MyStruct {
a: String,
b: String,
}
pub struct MyStructMut<'a> {
pub a: &'a String,
pub b: &'a mut String,
}
impl MyStruct {
pub fn as_mut(&mut self) -> MyStructMut {
MyStructMut {
a: &self.a,
b: &mut self.b,
}
}
}
そこまでする必要があるかはわかりませんが、選択肢として頭にあったほうがいいかと思います。
getterを使ってもいい場合
getterで渡す参照が、実質構造体全体を表す場合
ここまでの主張はすべて、他のフィールドにアクセスできない点を問題にしています。なので、他にアクセスするフィールドがない場合は、&mut
を返すgetterがあっても弊害はありません。ただし、この場合は専用のgetterではなくDerefMut
やBorrowMut
、AsMut
等の実装を考えるべきでしょう。
また、構造体がコレクションの場合は、その一部要素の&mut
を渡すメソッドがあっても良いかと思います。
不変な構造体のgetterはあってもいい
ここまでの主張はすべて、可変参照に関する問題点です。可変参照が存在することが無いなら、不変参照は複数存在できます。なので、getterメソッドを通すことでSelf
が借用されていても、弊害はありません。
そもそもgetterが必要かどうかは…なんとも言えないところですが、そのへんの話はここでは置いておきます。