如果您希望能够在需要函数f2的地方使用函数f1,则f1必须是相同类型(输入参数和返回值)或f2的子类。里氏替换原则告诉我们,一个函数要成为另一个函数的子类,它需要更少(或相同)并提供更多(或相同)。
因此,如果您有一个方法作为参数采用 type 的函数(Fruit, Fruit) => Fruit
,则以下是一些可以传递给该方法的有效函数的类型:
- (水果,水果)=> 水果
- (水果,水果)=> 苹果
- (任何,任何)=> 水果
- (任何,任何)=> 苹果
这与协变/逆变规则有关;例如,Scala 中的每个单参数函数都是具有两个类型参数的 trait Function2[-S, +T]
,. 你可以看到它的参数类型是逆变的,而它的返回类型是协变的——需要S
或更少(“更少”,因为它更通用,所以我们丢失了信息)和提供T
或更多(“更多”,因为它更具体,所以我们获取更多信息)。
这给我们带来了您的问题。如果你有相反的东西,试图适应预期(Base, Base) => Int
的地方,那会奏效。(Derived, Derived) => Int
MethodmyMethod
显然希望为这个函数提供 typeDerived
的值,而接受 type 值的函数Base
会很乐意接受这些值;毕竟Derived
是一个Base
。基本上myMethod
说的是:“我需要一个可以处理Derived
s 的函数”,任何知道如何使用Base
s 的函数也可以采用它的任何子类,包括Derived
.
其他人指出,您可以将 functionf
的参数类型设置为 的子类型Base
,但在某些时候您可能希望将 v1 和 v2 与该函数一起使用,然后您需要通过模式匹配恢复为向下转换. 如果您对此感到满意,您也可以直接在函数上进行模式匹配,试图找出它的真实性质。myMethod
无论哪种方式,在这种情况下模式匹配都很糟糕,因为每次引入新类型时您都需要摆弄。
以下是使用类型类更优雅地解决它的方法:
trait Base[T] {
def f(t1: T, t2: T): Int
}
case class Shape()
case class Derived()
object Base {
implicit val BaseDerived = new Base[Derived] {
def f(s1: Derived, s2: Derived): Int = ??? // some calculation
}
implicit val BaseShape = new Base[Shape] {
def f(s1: Shape, s2: Shape): Int = ??? // some calculation
}
// implementations for other types
}
def myMethod[T: Base](v1: T, v2: T): Int = {
// some logic
// now let's use f(), without knowing what T is:
implicitly[Base[T]].f
// some other stuff
}
myMethod(Shape(), Shape())
这里发生的情况是myMethod
:“我需要两个 T 类型的值,并且我需要Base[T]
在范围内有一个隐式可用的值(这就是[T: Base]
部分,这是一种奇特的说法,你需要一个类型的隐式参数Base[T]
;这样你将通过其名称访问它,这样您就可以通过implicitly
) 访问它。然后我知道我将拥有f()
执行所需逻辑的可用"。而且由于逻辑可以根据类型有不同的实现,这是一种特殊多态性的情况,类型类是处理这种情况的好方法。
这里很酷的是,当引入了具有自己实现的新类型时f
,您只需将此实现Base
作为隐式值放入伴随对象中,以便它可以用于myMethod
. 方法myMethod
本身保持不变。