関連型 (Associated types) はトレイトのアイテムとして定義される。そして、トレイトを実装する型から、その関連型を通して、別の型を得られるようにする。つまり、トレイトの関連型 A は「トレイトの実装型 X から、別の型 A(X) への写像」となる。
トレイトの側では、type Assoc; のように定義する。
トレイトの実装側では、type Assoc = SomeType; のように型を指定する。
以下では、トレイト Sequence からの生成値のために、関連型 Value を定義している。
そして、Sequence を実装する型 IntCounter にて、Value は i32 に指定されている。
fn main() {
let seq = &mut IntCounter(0);
let seq = seq as &mut dyn Sequence<Value = i32>;
assert_eq!(seq.next(), 1);
}
trait Sequence {
type Value;
fn next(&mut self) -> Self::Value;
}
struct IntCounter(i32);
impl Sequence for IntCounter {
type Value = i32;
fn next(&mut self) -> Self::Value {
self.0 += 1;
self.0
}
}
関連型に境界を指定する方法について。
type Assoc: Bounds のように定義すると、境界が付加される。
これにより、実装側で Assoc として指定できる型に Bounds の制約がつく。
以下のコードは、直前の例と似たような構成だが、関連型 Value に境界 Ord がある。
そして、型 IntCounter における Value である i32 はもちろん Ord である。
fn main() {
let seq = &mut IntCounter(0);
skip_to(seq, 3);
assert_eq!(seq.next(), 4);
}
trait Sequence {
type Value: Ord;
fn next(&mut self) -> Self::Value;
}
struct IntCounter(i32);
impl Sequence for IntCounter {
type Value = i32;
fn next(&mut self) -> Self::Value {
self.0 += 1;
self.0
}
}
fn skip_to<T>(target: &mut T, max: T::Value)
where
T: Sequence
{
loop {
let val = target.next();
if val >= max {
return
}
}
}
定義時に指定した境界の他に、関連型の使用時にも境界を追加できる。
これには、型パラメタ T が関連型 Assoc を持つ場合、T::Assoc: Bounds のようにする。
以下のコードは、直前の例と同じ動作をする。ただし、関連型 Value の境界 Ord は、関連型の定義時でなく、外部の関数 skip_to での関連型の使用時に指定されている。
fn main() {
let seq = &mut IntCounter(0);
skip_to(seq, 3);
assert_eq!(seq.next(), 4);
}
trait Sequence {
type Value;
fn next(&mut self) -> Self::Value;
}
struct IntCounter(i32);
impl Sequence for IntCounter {
type Value = i32;
fn next(&mut self) -> Self::Value {
self.0 += 1;
self.0
}
}
fn skip_to<T>(target: &mut T, max: T::Value)
where
T: Sequence,
T::Value: Ord,
{
loop {
let val = target.next();
if val >= max {
return
}
}
}
関連型はジェネリクスパラメタを付加する事で、パラメトリックにできる。
この機能は GAT (Generic Associated Types) と呼ばれている。
where による境界の指定
GAT への境界の指定はコロンによる方式の他に、where による方式にも対応している。
これにより、GAT のパラメタへの境界の指定もできるようになっている。
関連型のジェネリクスパラメタに型パラメタを使うパターン。
以下では、トレイト CollectionBuilder が生成するコレクションのために、ジェネリックな関連型 Output<T> が定義されている (T はコレクションの要素型)。
fn main() {
let vec_builder = VecBuilder();
let vec = vec_builder.build(42, 3);
assert_eq!(vec, vec![42, 42, 42]);
}
trait CollectionBuilder {
type Output<T>: Default + Collection<T>;
fn build<T: Clone>(&self, v: T, n: usize) -> Self::Output<T> {
let mut result = <Self::Output<T>>::default();
for _ in 0..n {
result.add(v.clone());
}
result
}
}
trait Collection<T> {
fn add(&mut self, x: T);
}
struct VecBuilder();
impl CollectionBuilder for VecBuilder {
type Output<T> = Vec<T>;
}
impl<T> Collection<T> for Vec<T> {
fn add(&mut self, x: T) {
self.push(x);
}
}
関連型のジェネリクスパラメタにライフタイム指定子を使うパターン。
以下では、トレイト IntCollection が生成するイテレータのために、ジェネリックな関連型 IntStream<'a> が定義されている。ここで、'a はイテレータのアイテムが所属するライフタイムで、IntCollection は 'a から参照できなければならない (Self: 'a の境界のため)。
use std::slice::Iter;
fn main() {
let vec = vec![1, 2, 3];
let stream = vec.int_stream();
assert_eq!(stream.sum::<i32>(), 6);
}
trait IntCollection {
type IntStream<'a>: Iterator<Item = &'a i32> where Self: 'a;
fn int_stream(&self) -> Self::IntStream<'_>;
}
impl IntCollection for Vec<i32> {
type IntStream<'a> = Iter<'a, i32> where Self: 'a;
fn int_stream(&self) -> Self::IntStream<'_> {
self.iter()
}
}
、GAT にはまだ制限が多い。この事は公式ブログでも紹介されている。それによると、これは導入 (Rust 1.65: 2022/11/03) を急いだ結果らしく、将来的には改善される見込みらしい。