💬

[T]とかstrの使い方

2024/07/20に公開

前口上

Rustのスライス型には汎用の&[T]と、文字列の&strがある。

割と以前から、[T]strという&無しの定義に使い道はあるのかしらん?と疑問に思っていたんだけど、今般こー言うときに使うのねということが少し解ったのでまとめておこうかなって。

仕込み

毎度ながら結構作為的な事例になるのでその辺ご了承の程。

ConsoleにDumpを実行するtraitを定義するとしたら概ね以下のようになるかなって

pub trait Dump {
	fn dump(&self);
}

このtraitをSliceに適用しようとした場合、dump(&self)の通り、Associate先の参照を得る形となる。此処で、たとえば以下のように実装したとする

impl<T: Debug> Dump for &[T] {
	fn dump(&self) {
		for elem in self.iter() {
			println!("{elem:?}")
		}
	}
}

fn dump(item: &impl Dump) {
	item.dump()
}

と、ここまでは良いんだけど、実際呼び出そうとしたとき

fn main() {
	let vec = vec![1, 2, 3, 4, 5, 6];
	dump(&vec.as_slice())
}

と、dump(&vec.as_slice())とスライスの参照即ちここでは&&[i32]をこさえることになるので割るか無いけどなんとなく上長打よねってことになる。

[T]を使う

ということで、実装を少し変えてみよう

impl<T: Debug> Dump for [T] {
	fn dump(&self) {
		for elem in self.iter() {
			println!("{elem:?}")
		}
	}
}

fn dump(item: &(impl Dump + ?Sized)) {
	item.dump()
}

となる。

dump関数の引数に&[T:Debug]を使おうとした場合、当然のことながらスライスは?Sizedなので、?Sized境界を付与しておかなければならない。逆に先の場合はスライスへの参照なのでこの境界は不要となっている。

但し、呼び出しの方が

fn main() {
	let vec = vec![1, 2, 3, 4, 5, 6];
	dump(vec.as_slice())
}

このように余計な参照が不要になってすっきりする

切口上

&strに対する実装も以下のようになる

impl Dump for str {
	fn dump(&self) {
		println!("{self}")
	}
}

impl Dump for &str {
	fn dump(&self) {
		println!("{self}")
	}
}

Sliceに対して何かしらのtraitをimpl仕様としたとき概ね&selfを取るメソッドが内包されていることが殆どじゃないかなと思う。このとき、&[T]&strに対する実装とした場合、先に観察したとおりスライスの参照に対する実装となり、スライスそのものへの実装とすることが出来ない。

スライスそのものへのAssociateとしたい場合に[T]strを使うと言うことが理解できた。

追記 ~やっぱり両方実装した方が幸せかなって~

これまでの実装をまとめて,以下のようにしてみた

use std::fmt::Debug;

pub trait Dump {
	fn dump(&self);
}

impl<T: Debug> Dump for &[T] {
	fn dump(&self) {
		println!("&[T] implements");
		for elem in self.iter() {
			println!("{elem:?}")
		}
	}
}

impl<T: Debug> Dump for [T] {
	fn dump(&self) {
		println!("[T] implements");
		for elem in self.iter() {
			println!("{elem:?}")
		}
	}
}

fn dump(item: &(impl Dump + ?Sized)) {
	item.dump()
}

fn val_dump(item: impl Dump) {
	item.dump()
}

fn main() {
	let vec = vec![1, 2, 3, 4, 5];
	let slice = vec.as_slice();

	println!("slice.dump();");
	slice.dump();
	println!("----------\n");

	println!("dump(slice);");
	dump(slice);
	println!("----------\n");

	println!("val_dump(slice)");
	val_dump(slice);
	println!("----------\n");
}

これの出力結果が以下

cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target\debug\iterable.exe`
slice.dump();
[T] implements
1
2
3
4
5
----------

dump(slice);
[T] implements
1
2
3
4
5
----------

val_dump(slice)
&[T] implements
1
2
3
4
5
----------

ここから解るように、slice.dump();fn dump(item: &(impl Dump + ?Sized))への呼び出しはimpl<T: Debug> Dump for [T] こっちの実装が使われている一方、val_dump(slice);の呼び出しは、impl<T: Debug> Dump for &[T] こちらの方が使われる結果となっている。

このことから、可能であればどちらも実装しておいた方が自由度が高くなるんじゃないかなと考えた

fn val_dump(item: impl Dump)は、見れば解るとおり、値をMoveさせる形で引数を取っている。結果、impl<T: Debug> Dump for [T]の実装だけ提供している場合は、引数として、[T]を要求されることになり、そんなもんは存在できない*(と僕は現時点で考えている)*のでコンパイルが通せなくなる。

以上のことから、スライスに対してTraitをAssociateしようとした場合、[T]またはstrだけで良いのか、それとも&[T]&strのみで良いのかそれとも両方の実装が必要とされるのか使われ方を想定しながら考慮する必要があるかなって

Discussion