【问题标题】:Limiting classes that can extend a scala trait限制可以扩展 Scala 特征的类
【发布时间】:2018-09-19 11:46:05
【问题描述】:

似乎有三种(或更多)方法可以限制哪些类可以混入给定的 scala 特征:

  1. 使用共同祖先 [特征]
  2. 使用抽象声明
  3. 在特质中使用自我类型

共同祖先方法需要额外的限制,而且似乎不是最理想的。同时,自键入和抽象声明似乎是相同的。有人愿意解释区别和用例(尤其是在 2 和 3 之间)吗?

我的例子是:

val exampleMap = Map("one" -> 1, "two" -> 2)
class PropsBox (val properties : Map[String, Any])    

// Using Common Ancestor
trait HasProperties {
  val properties : Map[String, Any]
}
trait KeysAsSupertype extends HasProperties {
  def keys : Iterable[String] = properties.keys
}
class SubProp(val properties : Map[String, Any]) extends HasProperties
val inCommonAncestor = new SubProp(exampleMap) with KeysAsSupertype
println(inCommonAncestor.keys)
// prints: Set(one, two)


// Using Abstract Declaration
trait KeysAsAbstract {
  def properties : Map[String, Any]
  def keys : Iterable[String] = properties.keys
}
val inAbstract = new PropsBox(exampleMap) with KeysAsAbstract
println(inSelfType.keys)
// prints: Set(one, two)    


// Using Self-type
trait KeysAsSelfType {
  this : PropsBox => 
  def keys : Iterable[String] = properties.keys
}
val inSelfType = new PropsBox(exampleMap) with KeysAsSelfType
println(inSelfType.keys)
// prints: Set(one, two)    

【问题讨论】:

    标签: scala inheritance traits self-type


    【解决方案1】:

    在您的示例中,PropsBox 没有对properties 施加任何有趣的约束——它只是有一个成员properties: Map[String, Any]。因此,没有办法检测从PropsBox 继承和只需要def properties: Map[String, Any] 之间的区别。

    考虑以下示例,实际存在差异。假设我们有两个类GoodBoxBadBox

    1. GoodBoxproperties,所有键都是短字符串,只包含数字
    2. BadBox 只是有properties,对键的结构不做任何保证

    在代码中:

    /** Has `properties: Map[String, Any]`, 
      * and also guarantees that all the strings are
      * actually decimal representations of numbers 
      * between 0 and 99.
      */
    class GoodBox(val properties: Map[String, Any]) {
      require(properties.keys.forall {
        s => s.forall(_.isDigit) && s.size < 3
      })
    }
    
    
    /** Has `properties: Map[String, Any]`, but 
      * guarantees nothing about the keys.
      */
    class BadBox(val properties: Map[String, Any])
    

    现在假设我们出于某种原因想要将Map[String, Any] 转换为稀疏的Array[Any],并使用键作为数组索引。同样,这里有两种方法:一种使用self-type 声明,另一种使用抽象def properties 成员声明:

    trait AsArrayMapSelfType {
      self: GoodBox =>
      def asArrayMap: Array[Any] = {
        val n = 100
        val a = Array.ofDim[Any](n)
        for ((k, v) <- properties) {
          a(k.toInt) = v
        }
        a
      }
    }
    
    trait AsArrayMapAbstract {
      def properties: Map[String, Any]
      def asArrayMap: Array[Any] = {
        val n = 100
        val a = Array.ofDim[Any](n)
        for ((k, v) <- properties) {
          a(k.toInt) = v
        }
        a
      }
    }
    

    现在试试吧:

    val goodBox_1 = 
      new GoodBox(Map("1" -> "one", "42" -> "fourtyTwo"))
      with AsArrayMapSelfType
    
    val goodBox_2 = 
      new GoodBox(Map("1" -> "one", "42" -> "fourtyTwo"))
      with AsArrayMapAbstract
    
    /* error: illegal inheritance
    val badBox_1 = 
      new BadBox(Map("Not a number" -> "mbxkxb"))
      with AsArrayMapSelfType
    */
    
    val badBox_2 = 
      new BadBox(Map("Not a number" -> "mbxkxb"))
      with AsArrayMapAbstract
    
    goodBox_1.asArrayMap
    goodBox_2.asArrayMap
    // badBox_1.asArrayMap - not allowed, good!
    badBox_2.asArrayMap // Crashes with NumberFormatException, bad
    

    使用goodBox,两种方法都可以工作并产生相同的结果。但是,使用badBox,self-type 与 abstract-def 的行为不同:

    1. 自类型版本不允许代码编译(编译时捕获错误)
    2. abstract-def 版本在运行时崩溃并显示 NumberFormatException(运行时发生错误)

    这就是区别。

    【讨论】:

    • 这是有道理的。因此,如果我理解正确,对于我简单的“松散”示例,它们是相同的。但如果我真的需要施加约束,则首选自键入。不过快速跟进:require 语句是否可以作为 trait(而不是类)中的约束来实现?如果是这样,这样做有意义吗?
    • @mjalajel 您可以在可以使用任何其他语句的任何地方使用require,因此您也可以在特征的初始化程序中使用它(即在特征的主体中,在开始的块中在特征名称之后)。我想,在特征的初始化程序中使用它会比在构造函数中使用它少一些,但只要它有助于通过早期失败来防止奇怪的未定义行为 - 为什么不呢。
    猜你喜欢
    • 2015-06-03
    • 1970-01-01
    • 2019-05-11
    • 1970-01-01
    • 2016-09-13
    • 1970-01-01
    • 2012-12-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多