🍢

【Rust】俺(self)とDerefとオブジェクト

2023/12/28に公開

Rustにおけるメソッドの扱い

Rustのオブジェクト機能は他のオブジェクト指向言語と少し違うところがあり、struct内の関数の定義で第一引数にselfを指定すると、その関数はメソッドのように扱われるようになります。

C++やJavaではclass内で定義した関数は明示的にstaticをしなければメソッドとなり、メソッド内では暗黙的にthisが自身を指し示すオブジェクトなります。

が、Rustではメソッドの定義の際に明示的にselfを指定します。

これはつまり、

struct Hoge{
}

impl Hoge{
	pub fn hoge(&self){
	}
}

というstructと関数が存在する場合に、

let hoge=Hoge{};
hoge.hoge();

とメソッドとして呼び出す事も

let hoge=Hoge{};
Hoge::hoge(&hoge);

このように普通の関数として引数にオブジェクトの参照を渡す事もできるという事です。

これはつまり、普通の関数とメソッドとの違いが殆ど無いという事を意味していると思われ、この事から、どちらかというとRustはオブジェクト指向プログラミング言語ではなく、あくまでベースは手続き型言語でありつつオブジェクト指向プログラミングのような事もできる言語である、と考えられます。

https://doc.rust-jp.rs/book-ja/ch17-01-what-is-oo.html

Deref

RustにはDerefというtraitがあります。
まずは公式ドキュメントを見てみましょう。

https://doc.rust-jp.rs/book-ja/ch15-02-deref.html

Derefトレイトを実装することで、参照外し演算子の*(掛け算やグロブ演算子とは違います)の振る舞いをカスタマイズできます。

という事が書かれています。
私は初見では何のことかよくわかりませんでした。
が、これは要するに透過的に型変換を行う仕組みのようです。

例えば、

struct Hoge {
	num: u32,
}
impl Deref for Hoge {
	type Target = u32;

	fn deref(&self) -> &Self::Target {
	    &self.num
	}
}
let hoge = Hoge { num: 10 };
println!("{}", *hoge);

このプログラムを実行すると数字の10が出力されます。
参照外し演算子の*をhogeにつけることでderefで実装された型変換が行われます。

このような例の場合ではイマイチ使い道は見えませんが、例えばC++で出来るように何らかのClass(struct)を継承したい場合だとどうでしょう。

rustには継承という概念がありませんがDerefを使うとそれに近い事が可能です。

///何らかのstructを内包するstruct
struct Hoge {
	inner: XYZ	///XYZにはfn xyz_methodがimplされているとする
}
impl Deref for Hoge {
	type Target = XYZ;

	fn deref(&self) -> &Self::Target {
	    &self.inner
	}
}
let hoge = Hoge { inner: XYZ::new() };
let r=hoge.xyz_method();

この例の場合、derefを使う事で、

hoge.inner.xyz_method()

ではなく、

hoge.xyz_method()

と、XYZのメソッドをHogeから直接呼び出すことができます。
これは、derefによって型変換が暗黙的に行われた結果です。
また、hoge.innerはpubになっていないため、外から直接参照する事が出来ませんが、Derefを使う事で要素を隠蔽しつつ、透過的に外部から参照させる事も出来ています。

また、暗黙的に型が変換されるわけですから、下記のようにXYZの参照を受け取るような関数にHoge型を渡すという事も出来ます。

fn pass_xyz(xyz:&XYZ){}

pass_xyz(&hoge);

という事で、継承ではなく内包というアプローチを取り、概念をよりシンプルにし、さらに他の用途にも使えるようにしたものがDerefである、と個人的には認識しています。

AsRef

Rustでは同一のtraitを同一の型に複数回実装出来ないという制約があります。
ですので、型Hogeに対して複数の型をDerefする、というような事が出来ません。

そのような事をしたい場合には、AsRef<T>が使えます。
AsRef<T>はジェネリックなtraitですので、Tの数だけ実装する事が出来ます。

struct Hoge {
	a: u32,
	b: i64,
}

impl AsRef<u32> for Hoge {
	fn as_ref(&self) -> &u32 {
	    &self.a
	}
}

impl AsRef<i64> for Hoge {
	fn as_ref(&self) -> &i64 {
	    &self.b
	}
}
let hoge = Hoge { a: 1, b: 2 };
let a:&u32=hoge.as_ref();
let b:&i64=hoge.as_ref();

上記は実用的なコードではないですが、要求される型に自動的に変換する事が出来ています。

&selfと&mut selfとselfとmut self

メソッドを定義する際は第一引数に

fn func(&self){
}

fn func(&mut self){
}

として参照を渡す事が殆どだと思います。

ところが、使う機会は少ないと思いますが、

fn func(self){
}

として実体を渡す事もできます。

この場合、どのような挙動になるかというと、関数内に値がムーブします。

struct Hoge{}
impl Hoge{
	fn func(self){
	}
}

fn main(){
	let hoge=Hoge{};

	hoge.func();	
	hoge.func();
}

hoge.func();
を二回呼び出していますが、一回目の呼び出しで値がムーブしているので、二回目の呼び出しはコンパイルエラーになります。
メソッドの呼び出しで値がムーブするというのはちょっと混乱しそうですが、メソッドの定義の代わりに、

struct Hoge{}
impl Hoge{
	fn func(hoge:Hoge){
	}
}

fn main(){
	let hoge=Hoge{};

	Hoge::func(hoge);	
	Hoge::func(hoge);
}

と置き換えてみれば、普通に引数に値を渡しているのと同じことが起こっているのだと素直に理解できると思います。

この仕組みがどのようなケースで使われるのかというと、今のところ私の経験上ではメソッドチェーンを作る場合にこの仕組みが役に立ちました。

コードにするとこのようになります。

struct Hoge {}
impl Hoge {
	fn new() -> Self {
		Hoge {}
	}
	fn func(mut self) -> Self {
		//何らかの処理
		self
	}
	fn func2(mut self) -> Self {
		//何らかの処理
		self
	}
}
let hoge = Hoge::new().func().func2();

このように、引数で渡ってきたselfを戻り値でそのまま返す事で、呼び出し元に再度ムーブされ、別のメソッドを呼び出すことができます。
なお、selfを引数として受け取る場合でも、Rustの通常の挙動と同じくデフォルトではイミュータブルなので、selfをミュータブルにしたい場合はmut selfにする必要があります。

Discussion