【问题标题】:Scala Map's get vs apply operation: "type mismatch"Scala Map 获取或应用操作:“类型不匹配”
【发布时间】:2019-12-14 06:20:48
【问题描述】:

我正在学习 Scala,发现如下:

List(('a', 1)).toMap get 'a'           // Option[Int] = Some(1)
(List(('a', 1)).toMap) apply 'a'       // Int = 1
(List(('a', 1)).toMap)('a')            // Error: type mismatch;
                                          found   : Char('a')
                                          required: <:<[(Char, Int),(?, ?)
                                          (List(('a', 1)).toMap)('a')

但随后将其分配给变量再次起作用。

val b = (List(('a', 1)).toMap)
b('a') // Int = 1

为什么会这样?

标准文档给出:

ms get k

映射 ms 中与键 k 关联的值作为选项,如果未找到则无。

ms(k)(或者,写出来,ms apply k

map ms 中键 k 关联的值,如果没有找到则异常。

为什么第三行不起作用?

【问题讨论】:

  • 这里发生了很多事情。首先,您的示例不正确。 get 如文档所述,返回一个 Optionapply 返回一个普通值。如果密钥不存在,get 将返回 None,而 apply 将抛出 Exception - 其次,问题在于,toMap 收到一些隐含的论点。这就是为什么最好将代码分成两行的原因,因为它会使您的apply 与传递这些参数混淆。
  • @LuisMiguelMejíaSuárez 已更正示例,感谢您指出这一点。你能解释一下“隐含的论点”吗?
  • @LieuZhengHong 这是一个很好的问题,而且这种行为确实是违反直觉的。 this duplicate 会回答你的问题吗?本质上,它试图将'a' 视为toMap 的隐含参数。这在这里有效:(List((1, 2)).toMap(implicitly[(Int, Int) &lt;:&lt; (Int, Int)]))(1)。这也有效:List((1, 2)).toMap.apply(1)。但由于有问题的原因,仅使用括号是行不通的。

标签: scala implicit syntactic-sugar


【解决方案1】:

signature 略有不同:

abstract def get(key: K): Option[V]

def apply(key: K): V

问题在于错误处理:当找不到元素时get 将返回Noneapply 将抛出异常:

scala> Map(1 -> 2).get(3)
res0: Option[Int] = None

scala> Map(1 -> 2).apply(3)
java.util.NoSuchElementException: key not found: 3
  at scala.collection.immutable.Map$Map1.apply(Map.scala:111)
  ... 36 elided

关于失败的行:toMap 有一个隐式参数ev: A &lt;:&lt; (K,V) 表示类型约束。当您调用 r.toMap('a') 时,您正在为隐式传递一个显式值,但它的类型错误。 Scala 2.13.0 有一个伴生对象&lt;:&lt;,它提供了一种反射性方法(使用给定类型本身而不是适当的子类型)。现在以下工作:

scala> List(('a', 1)).toMap(<:<.refl)('a')
res3: Int = 1

备注:我无法在 Scala 2.12.7 中调用 &lt;:&lt;.refl,添加的内容似乎是最近才添加的。

【讨论】:

  • 感谢您的回答。我已经把问题说得更具体了。我了解 Option 与抛出异常之间的区别。我想我的主要困惑是为什么分配给一个 val 有效,但把它全部放在一行上却没有。
  • 我刚刚意识到toMap 有一个隐含的参数:def toMap[K, V](implicit ev: &lt;:&lt;[A, (K, V)]): immutable.Map[K, V] 它为类型强制提供了一个见证对象。 Scala 尝试调用它而不是 apply。我还没有找到如何创建一个默认见证来通过。然后它应该通过调用List(('a',1)).toMap(ev)('a')来工作。
  • 添加说明
  • 感谢&lt;:&lt;.refl,不知道这个。
  • 根据 git 历史,它是在 2018 年 12 月添加的,它可能只是最近才在发布版本中浮出水面。
【解决方案2】:

它本质上只是隐式参数与apply-语法糖和奇怪的括号消除行为的特殊冲突。

作为explained here,括号中的

(List(('a', 1)).toMap)('a')

被丢弃有点太早了,所以你最终得到

List(('a', 1)).toMap('a')

以便编译器尝试将 'a' 解释为 (Char, Int) &lt;:&lt; (?, ?) 对于某些未知类型 ?? 的隐含证据。

这在这里有效(它没有用,它只是为了演示编译器通常在这个位置会期望什么):

(List(('a', 1)).toMap(implicitly[(Char, Int) <:< (Char, Int)]))('a')

List(...).toMap 分配给变量也可以:

({val l = List((1, 2)).toMap; l})(1)

或者,您可以强制 toMap 停止接受参数,方法是将其提供给不执行任何操作的 identity 函数:

identity(List((1, 2)).toMap)(1)

但消除隐含参数和apply-syntactic sugar 的歧义的最简单和最清晰的方法是直接写出.apply

List((1, 2)).toMap.apply(1)

我认为在这一点上应该很明显为什么.get 的行为不同,所以我不会详细说明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多