设
T1, T2, T3
为三种类型。我们还定义了 Conversion
类的两个 给定实例,这样编译器就可以从
T1
到 T2
以及从 T2
到 T3
.
以下代码可以正常编译:
type T1
type T2
type T3
given Conversion[T1, T2] with
override def apply(x: T1): T2 = ???
given Conversion[T2, T3] with
override def apply(x: T2): T3 = ???
val t1: T1 = ???
val t2: T2 = t1
val t3: T3 = t2
但是当我们尝试从
T1
到 T3
时会发生什么?编译器不会让我们:
val t3: T3 = t1
^^
Found: (t1: T1)
Required: T3
我的问题:是否有编译器无法本地(参见解决方法)链转换的具体原因?
我的解决方法:事实证明,我们可以隐式地告诉编译器如何通过定义从
A
到 C
的通用转换来链接转换,因为我们知道如何将 A
转换为 B
和 B
到 C
:
given [A, B, C](using conv1: Conversion[A, B])(using conv2: Conversion[B, C]): Conversion[A, C] with
override def apply(x: A): C = conv2.apply(conv1.apply(x))
编译器现在可以链式转换:
val t3: T3 = t1 //OK
奖励点:这个新的泛型转换甚至可以递归调用自身,这意味着我们可以链接无限转换。
不是“不能”的问题,而是“不会”的问题。限制是有意识地选择的。
Odersky、Spoon 和 Venners Scala 中的编程(我引用第三版,使用 Scala 2 术语,但模数术语差异仍然适用):
一次一个规则:只插入一个隐式[转换]。编译器永远不会将
重写为x + y
。这样做会导致错误代码的编译时间急剧增加,并且会增加程序员编写的内容与程序实际执行的内容之间的差异。为了理智起见,编译器在尝试另一个隐式转换时不会插入进一步的隐式转换。但是,可以通过使隐式采用隐式参数来规避此限制。convert1(convert2(x)) + y
详细说明这个论点,除了关于程序明显的明确含义与应用转换后的实际含义之间的差异的更主观的论点之外(人们可以很容易地争论(和(隐含地......)几乎所有其他语言设计者通过不进行 Scala 风格的转换来做到这一点)尝试任何隐式转换都会过度增加所写内容与程序实际执行的内容之间的差异),请考虑对转换深度没有限制的后果(回想一下格言:“零,一, 或无限制”)
如果我有一个没有方法
foo()
的类型,像x.foo()
这样的表达式不能被编译器拒绝,直到它从x
的类型计算类型转换的传递闭包并且发现不一个有一个foo()
方法。在 IDE 中,这将意味着相当长的延迟,直到出现“红色波浪形”(或者 IDE 可能假设在一定程度的转换之后,它可能值得一个波浪形,而不是 Scala 中有任何广泛使用的具有历史的 IDE不同意实际编译器接受/拒绝的内容......)。
即使对于可接受的程序,也有一种情况应该尝试通过转换空间的所有可能路径:编译器作为一般规则(除非有一个转换比其他转换“更具体”)不会应用转换当有多个候选者可以申请并且编译器无法在不尝试每条路径的情况下知道有多少候选者时(一旦发现第二个转换它可能会停止,但在只有一个的典型情况下,它会找到一个接受之前的传递闭包)。通过限制为一次转换,关于如何处理这个问题的争论就被取消了。