【问题标题】:Scala: implementing Map with concrete typesScala:使用具体类型实现 Map
【发布时间】:2011-08-10 00:18:36
【问题描述】:

我在 Scala 类型系统中遇到了某种怪癖,这让我有点难过。我正在尝试创建一个扩展 Map[String,String] 的类,但我无法弄清楚如何以编译器接受它的方式实现 + 方法。

这是我现在拥有的代码:

class ParamMap(val pairs:List[(String,String)] = Nil) extends Map[String,String] {

  lazy val keyLookup = Map() ++ pairs

  override def get(key: String): Option[String] = keyLookup.get(key)
  override def iterator: Iterator[(String, String)] = pairs.reverseIterator

  /**
   * Add a key/value pair to the map
   */
  override def + [B1 >: String](kv: (String, B1)) = new ParamMap(kv :: pairs)

  /**
   * Remove all values for the given key from the map
   */
  override def -(key: String): ParamMap  = new ParamMap(pairs.filterNot(_._1 == key))

  /**
   * Remove a specific pair from the map
   */
  def -(kv: (String, String)) : ParamMap = new ParamMap(pairs - kv)
}

Scala 告诉我:

type mismatch;  found: (String, B1)  required: (String, String)

我相信这是因为允许 B1 是 String 的子类型,但我的构造函数只需要一个 String (?)。我最初的尝试是:

override def +(kv: (String, String)) = new ParamMap(kv :: pairs)

但这抱怨是因为类型签名与特征不匹配:

class ParamMap needs to be abstract, since method + in trait Map of type [B1 >: String](kv: (String, B1))scala.collection.immutable.Map[String,B1] is not defined
method + overrides nothing

我是 Scala 的新手,我想我在类型系统的工作方式方面已经过头了。也许我会尝试弄乱选角,但我觉得可能有一种“更好的方法”,如果我知道的话,将来会为我省去很多麻烦。

有什么想法吗?

【问题讨论】:

    标签: scala types


    【解决方案1】:

    关于 Scala 类型系统的一些背景知识。

    • 语法B1 >: String 表示B1String超类型。所以B1 不太具体,不能转换为String。相反,B1 <: String 将是一个 子类型 关系。

    • Map trait 的定义是Map [A, +B],其中A 表示键的类型,B 表示值的类型。 +B 表示法表示Map 在键类型中是协变,这意味着T <: S 隐含Map[A, T] <: Map[A, S]

    • Map.+ 方法的完整类型是+ [B1 >: B] (kv: (A, B1)): Map[A, B1]B 的协方差会强制使用 B1 >: B。下面是它如何工作的示例:给定一个映射 m: Map[String, String],添加一个具有不太具体类型的键值对 kv : (String, Any) 将导致一个不太具体的映射 (m + kv): Map[String, Any]

    最后一点说明了ParamMap 定义的问题。根据Map 接口,应该能够将Any 类型的键添加到ParamMap <: Map[String, String] 类型的映射并返回Map[String, Any]。但是您尝试将ParamMap.+ 定义为始终返回ParamMap[String, String],这与Map.+ 不兼容。

    解决问题的一种方法是给ParamMap 一个显式类型参数,例如(警告未经测试),

    class ParamMap[B](val pairs:List[(String,String)] = Nil) extends Map[String, B] {
      ...
      override def + [B1 >: B](kv: (String, B1)) = new ParamMap[B1](kv :: pairs)
    }
    

    但这可能不是您想要的。我认为没有办法将值类型固定为String 并实现Map[String, String] 接口。


    鉴于以上所有,为什么您的答案中的代码会编译?您实际上已经发现了 Scala 模式匹配的一个限制(不健全),它可能导致运行时崩溃。这是一个简化的示例:

    def foo [B1 >: String](x: B1): Int = {
      val (s1: Int, s2: Int) = (x, x)
      s1
    }
    

    虽然它可以编译,但它并没有做任何有用的事情。事实上,它总是会以MatchError 崩溃:

    scala> foo("hello")
    scala.MatchError: (hello,hello) (of class scala.Tuple2)
        at .foo(<console>:9)
        at .<init>(<console>:10)
        at .<clinit>(<console>)
        ...
    

    在您的回答中,您基本上告诉编译器将B1 实例转换为String,如果转换不起作用,您将遇到运行时崩溃。这相当于一个不安全的演员表,

    (value: B1).asInstanceOf[String]
    

    【讨论】:

    • 显然几秒钟后,但答案比我的要完整得多。 +1
    • 我认为这里答案的关键部分是“但您试图将 ParamMap.+ 定义为始终返回与 Map.+ 不兼容的 ParamMap[String, String]。”我也喜欢 Schief 的回答,但如果他更喜欢你的回答,你可以打勾。谢谢!
    【解决方案2】:

    您的构造函数期望List[String, String] 类型的值是正确的,但问题不在于B1 可能是String 的子类,而在于它可能是超类 --这就是B1 :&gt; String 符号所表示的内容。

    乍一看,您可能想知道为什么父类Map 会以这种方式键入方法。实际上,您尝试覆盖的 + 方法的返回类型是 Map[String, B1]。但是,在一般地图的背景下,这是有道理的。假设您有以下代码:

    class Parent
    class Child extends Parent
    
    val childMap = Map[String, Child]("Key" -> new Child)
    
    val result = childMap + ("ParentKey" -> new Parent)
    

    result 的类型必须是 Map[String, Parent]。鉴于此,Map 中 + 方法的类型限制是有道理的,但您的固定类型映射无法实现该方法设计的功能。它的签名允许你传入一个值,例如键入(String, AnyRef),但使用您在后续答案中提供的方法定义,当它尝试执行对keyvalue 的分配时,您将得到一个MatchError

    这有意义吗?

    【讨论】:

      【解决方案3】:

      在尝试构建Bag[T](即Map[T,Int])时,我与一位同事遇到了同样的问题。我们找到了两种不同的解决方案:

      1. 使用适当的BuilderCanBuildFrom 实现Traversable 而不是Map,并添加有用的映射方法(get、+、-)。如果您需要将集合传递给以地图为参数的函数,则可以使用隐式转换。这是我们完整的 Bag 实现:https://gist.github.com/1136259

      2. 保持简单:

        object collection {
          type ParamMap = Map[String,String]
          object ParamMap {
            def apply( pairs: List[(String,String)] = Nil ) = Map( pairs:_* )
          }
        }
        

      【讨论】:

      • 嗯,这可能不是一个真正的“地图”,就像在 Scala 中通常的地图一样。在这种情况下,子类化地图可能不是一个好主意。
      【解决方案4】:

      编译器似乎确实接受了这个:

        override def + [B1 >: String](kv: (String, B1)) = {
          val (key:String, value:String) = kv
          new ParamMap((key,value) :: pairs)
        }
      

      但我不知道为什么它比原版更好。如果没有人有更好的解决方案,我想这是一个可以接受的解决方案。

      【讨论】:

      • 这相当于一个不安全的演员表。我的回答有更多细节。
      • 啊,我没想到这会像演员一样。这绝对澄清了事情,谢谢!
      猜你喜欢
      • 2011-05-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-15
      • 2019-03-19
      • 1970-01-01
      • 1970-01-01
      • 2017-07-14
      相关资源
      最近更新 更多