Generic Parameters v.s. Associated Types

在学习和使用Rust的过程中会碰到两个概念 泛型参数(Generic Parameters)和 关联类型(Associated Types),有不少人(包括我自己)有时会混淆这两种类型。比如 有人会问,为什么描述闭包的Fn/FnMut/FnOnce trait,其返回值用关联类型来描述,而不是泛型。与其它语言有所不同,比如scala的Function1[-T1, +R]。

关于这两种类型的选择,这里写下自己的一些理解。

稍微限定一下前提,这里的泛型参数指的是trait上的泛型参数:

 

与关联类型相对应的概念在Haskell中称为type families(type class中的)。规则上大概是这样,泛型和Self 与 关联类型 是 n对1的关系:

relation

换句话说就是当泛型和Self确定下来的时候,其关联类型也要确定下来,通过这种方式来消除一些trait的歧义。比如已经给Range<i32>实现了Iterator,其Itemi32,我们就不能再实现另一个Iterator<Item=u32>了。总之,关联类型在trait里充当一个类型的“占位符”,表示“存在一个类型”,它在定义(impl)时,便要确定具体的类型了(而泛型是在调用时才决定具体类型)。

 

就这样的定义,似乎有点抽象。但使用上,其实有个比较简单的法则,去帮助你思考你的trait应该使用泛型还是关联类型:

  1. 具体类型由caller来决定的,使用泛型参数。

    一般使用在method的参数处(也不全是,比如Into trait)

    而函数的返回值处一般拿不到泛化的类型(不通过参数),如果将trait泛型放置于方法返回值处,大多数情况就要单独实现不同的trait了(就像Into 一样,但是Into可由From trait自动实现)。

  1. 具体类型由callee来决定的,使用关联类型。

    一般用在方法返回值处中(Into trait也是个特例):

    或者回调的参数中:

    因为callee中的类型,是在实现时就确定下来的。对于确定的泛型和Self,它就是唯一的。所以它并没有多态性,如果放在method参数上,要放不同的类型的参数,就要写多个类似的结构,所以一般不会用在method的参数上。这样也解释了为什么FnOnce trait为什么要将返回类型设计为关联类型,当一个闭包写下来的时候,其返回类型是确定的,而且还有一个重要的原因是,rust没有scala那样的子类型关系,所以无法进行类似的推导。

 

 

不过在关联类型处有时候也有“多态”的需求:

  1. 回调不同类型的参数:

  1. 返回不同类型的结果:

    嘛……这种”多态“也是伪多态,其实是确定的几种类型,解决方法也很简单,用enum包一下,或是用trait object,这里就不用展开了。而对于是引生命周期不一致的,还可以用高阶生命周期(higher-ranked trait bound):

     

 

 

最后再解决一个trait泛型中的一个常见问题,“未限形参”(unconstrained type parameter):

如果泛型参数没有出现在:

  1. Self类型中(比如上面的ParserWrapper<Lhs>
  2. trait的实现中(比如上面的BitOr<Rhs>和method签名中,不包括关联类型)
  3. trait bound的关联类型处(比如impl<A, F: Fn() -> A> 这样是可以的,因为AF的关联类型中,AF约束了)

就会出现这种编译错误。

 

我们来看一下具体的场景,要为Parser trait重载BitOr运算符:

这段代码就会出现刚刚的编译错误,因为这里泛型参数S没被约束(Lhs: Parser<S>,这里并没有限制,S还是任意的)。如果这段代码成立就会造成,对于一个ParserWrapper<Lhs>和一个BitOr<Rhs>多种实现,无法进行区分。

修复这段代码,其实也很简单,可以在ParserWrapper中多加一个泛型参数,make rustc happy:

这样S就被约束到Self中了。其实在类型上Index一个类型参数还有其它好处,它给编译器提供了更多的类型信息,有时候可以帮助类型推导,也可以利用这个类型参数表示“同型”但不同“性”的数据,这里就不展开说了。

 

总结

这里提到的法则,也不是通用的,也有不少反例,比如Into/From trait等等,所以仅供参考。。。只要被抽象的东西满足了(Generic, Self) n..1 Associated的关系时,就可以了。

另外Self type其实也可以当作一个泛型参数:

Self may be used in a type annotation to refer to an instance of the type implementing this trait passed as a parameter.

这也解释了,为什么Self和泛型参数用法这么相近了。