习惯用Scala处理基类vs派生类字段名称的方法?

时间:2011-06-29 04:57:57

标签: scala field idiomatic

在Scala中考虑以下基类和派生类:

    abstract class Base( val x : String )

    final class Derived( x : String ) extends Base( "Base's " + x )
    {
        override def toString = x
    }

这里,Derived类参数的标识符'x'会覆盖Base类的字段,所以像这样调用toString:

    println( new Derived( "string" ).toString )

返回Derived值并给出结果“string”。

因此,对'x'参数的引用会提示编译器自动在Derived上生成一个字段,该字段在调用toString时提供。这通常非常方便,但会导致字段的复制(我现在将字段存储在Base和Derived上),这可能是不合需要的。为了避免这种复制,我可以将Derived类参数从'x'重命名为其他内容,例如'_x':

    abstract class Base( val x : String )

    final class Derived( _x : String ) extends Base( "Base's " + _x )
    {
        override def toString = x
    }

现在调用toString会返回“Base的字符串”,这就是我想要的。不幸的是,代码现在看起来有点难看,使用命名参数来初始化类也变得不那么优雅了:

    new Derived( _x = "string" ) 

还存在忘记为派生类的初始化参数赋予不同名称并无意中引用错误字段的风险(因为Base类实际上可能包含不同的值而不合适)。

有更好的方法吗?

编辑1 :为了澄清,我真的只想要Base值;导出的只是初始化基类字段所必需的。该示例仅引用它们来说明随后出现的问题。

编辑2 :实际上,如果我使用vars而不是vals,那么示例会更清楚,因为这突出了稍后在基类中更改值的问题:

    class Base( var x : Int ) { def increment() { x = x + 1 } }
    class Derived( x : Int ) extends Base( x ) { override def toString = x.toString }

    val derived = new Derived( 1 )
    println( derived.toString )     // yields '1', as expected
    derived.increment()
    println( derived.toString )     // still '1', probably unexpected

编辑3 :如果派生类最终会隐藏基类字段,那么有一种方法可以抑制自动字段生成。似乎Scala编译器实际上可以设计为你做这件事,但当然这与“更近”标识符(Derived class''x')隐藏更多远程标识符(Base类')的更一般规则相矛盾。 'X')。似乎一个相当不错的解决方案是像'noval'这样的修饰符,可能是这样的:

    class Base( var x : Int ) { def increment() { x = x + 1 } }
    class Derived( noval x : Int ) extends Base( x ) { override def toString = x.toString }

    val derived = new Derived( 1 )
    println( derived.toString )     // yields '1', as expected
    derived.increment()
    println( derived.toString )     // still '2', as expected

7 个答案:

答案 0 :(得分:6)

避免重复字段的惯用方法是编写

abstract class Base { val x: String }

final class Derived(val x: String) extends Base {
   def toString = x
}

但是,在您的版本中,您看起来确实需要第二个字段,因为您有两个不同的值。正如您正确指出的那样,给这些字段赋予相同的名称可能会导致混淆。

由于您实际上不需要构造函数之外的构造函数参数,您可以使用此方法(具有作为工厂的伴随模块的私有构造函数):

abstract class Base { val x: String }

final class Derived private (val x: String) extends Base {
   def toString = x
}
object Derived {
   def apply(x: String) = new Derived("Base " + x)
}

答案 1 :(得分:6)

由于基类是抽象的,看起来好像确实想要val类中的var(或Base),与相关的支持领域。相反,您只是想要保证在具体的子类中可以使用这样的东西。

在Java中,您可以使用getX之类的访问器方法来实现此目的。

在Scala中,我们可以更好地使用val,vars和defs占用相同的命名空间,因此可以使用val来实现抽象def(或覆盖具体的{{1}如果这就是浮在你船上的东西)。更正式地说,这被称为“统一访问原则”

def

如果您需要通过对abstract class Base{ def x: String } class Derived(val x: String) extends Base { override def toString = x } 的引用来设置x,则还需要声明“setter”(然后可以使用Base实现):< / p>

var

(并不是说我会鼓励任何设计中的可变性;除非有一个特别令人信服的理由。默认情况下有太多理由支持不变性)

<强>更新

这种方法的好处是:

  • abstract class Base { def x: String def x_=(s: String): Unit } class Derived(var x: String) extends Base { override def toString = x } 可以是完全合成的值,完全根据其他值实现 (即您已知道半径的圆的区域)
  • x可以在类型层次结构中的任意深度实现,而不必 明确地通过每个介入构造函数 (每次都有不同的名字)
  • 只需要一个支持字段,因此不会浪费任何内存
  • 由于现在不需要构造函数,x可以作为特征实现; 如果你愿意的话

答案 2 :(得分:1)

你可以试试这个:

abstract class Base( val x : String )

final class Derived( _x : String ) extends Base( _x ) {
  override val x  = "Base's " + _x
  override def toString = x
}

然后

println(new Derived("string").toString)

准确打印您想要的内容

答案 3 :(得分:1)

你已经提供了有效的答案

abstract class Base( val x : String )

final class Derived( _x : String ) extends Base( "Base's " + _x )
{
    override def toString = x
}

如果问题是_x不是一个好名字,那么你应该使用一个有意义的名字。 或者,您可以按如下方式声明您的类

abstract class Base( val _x : String )

final class Derived( x : String ) extends Base( "Base's " + x )
{
    override def toString = _x
}

现在,您将拥有初始化Derived实例的“漂亮”语法。

如果scala允许

  

如果派生类最终会隐藏基类字段,则禁止自动字段生成的方法。

这对我来说似乎是一个非常低级的细节,你不想在代码中处理。如果可以安全地完成,编译器应该为您完成。

答案 4 :(得分:1)

正如@jsuereth所建议的,我创建了一个针对Scala的增强功能,仅供记录,我希望能够正确地总结这里讨论的内容。感谢您的所有投入!可在此处找到该票证,内容如下:https://issues.scala-lang.org/browse/SI-4762

派生类中基类字段的无意遮蔽,警告可取

只要(a)派生类中的类参数使用相同的参数,就会出现问题 符号作为基类中的字段或函数,以及(b)基类符号 随后在派生类中访问。派生类参数 导致编译器自动生成与阴影同名的字段 基类符号。 Derived类中对该符号的任何引用, 意图引用基类字段,然后无意中(a)原因 该字段的重复定义和(b)意外地(但正确地)引用 到派生类中的自动生成字段。

代码示例:

class Base( val x : String )
class Derived( x : String ) extends Base( x ) { override def toString = x }

由于'Base'类具有'val'来生成字段和派生类 不,开发人员显然打算只使用'衍生''x' 传递给Base,因此期望'toString'中的引用 产生基值。相反,'toString'中的引用会导致编译器 自动生成一个阴影'Base.x'字段的字段'Derived.x', 导致错误。编译器行为正确,但结果是a 编程错误。

使用场景(使用var字段可以更清楚地说明风险):

class Base( var x : Int ) { def increment() { x = x + 1 } }
class Derived( x : Int ) extends Base( x ) { override def toString = x.toString }

val derived = new Derived( 1 )
println( derived.toString )     // yields '1', as expected
derived.increment()
println( derived.toString )     // still '1', probably unexpected

由于每当有人使用默认方式进行初始化时都会出现此问题 来自Scala中派生类的基类字段,该场景将出现 非常常见,导致很多编程错误(很容易修复, 但仍然)为新用户。

存在这个问题的简单解决方法(使用不同的派生名称 类参数,如'_x','theX','initialX'等),但这引入了 不需要的额外符号。

解决方案A(最小):每当编译器推断出a时都会发出警告 class参数需要派生类中的自动生成字段 会遮蔽已经在基类中定义的符号。

解决方案B:解决方案A仍然需要解决方法 每次初始化基类字段时使用新的符号名称。这个 场景一直出现并通过解决方法污染命名空间 像'_x'和'theX'这样的字段名称似乎不合需要。相反,它可能会很好 如果开发人员有办法抑制自动字段生成 确定派生类符号最终会隐藏基数 类符号(例如,遵循解决方案A的警告)。也许是有用的 除了Scala之外,还有一个像'noval'(或者'passthrough'或者 'temp',或者其他什么 - 除了'val'和'var'),像这样:

class Base( var x : Int ) { def increment() { x = x + 1 } }
class Derived( noval x : Int ) extends Base( x ) { override def toString = x.toString }

val derived = new Derived( 1 )
println( derived.toString )     // yields '1', as expected
derived.increment()
println( derived.toString )     // still '2', as expected

答案 5 :(得分:0)

好的,首先我要指出@Yuriy Zubarev的回答可能就是你真正想要的。其次,我认为问题可能在于你的设计。看一下这个。这是您的代码的一部分:

extends Base( "Base's " + _x )

因此,某些值x会进入您的派生类,并会被信息修改(在本例中为"Base's " + ...)。你看到了问题吗?为什么你的派生类型知道你的基类型实际应该知道的东西?这是我建议的解决方案。

abstract class Base {
    // this works especially well if you have a var
    // which is what you wanna have as you pointed out later.
    var x: String
    x = "Base's " + x
}

final class Derived(override var x: String ) extends Base{
   override def toString = x
}

这可能听起来很苛刻,但如果这个解决方案自动帮助你,那就意味着你的设计很糟糕。如果另一方面它没有帮助,我可能没有正确理解你的问题,因此立即道歉。

答案 6 :(得分:0)

@Gregor Scheidt 如果我将toString()向下移动到Derived,则代码不起作用,如下所示:

object Test {
  abstract class Base ( val x: String)

  final class Derived(x: String) extends Base(x + " base") {
    override def toString() = x
  }

  def main(args: Array[String]): Unit = {
    val d  = new Derived( "hello")
    println( d) // hello
  }
}

A post from the official site said

  

如果是,则将类Foo(x:Int)等参数转换为字段   在一个或多个方法中引用

马丁的回复证实了其真相:

  

这一切都是正确的,但它应该被视为一种实现   技术。这就是规范对此保持沉默的原因。

因为没有办法阻止编译器的操作,我的选择是引用基类字段的更健壮的方法是使用不同的名称,例如使用下划线“_”作为前缀,如下所示: / p>

object Test {
  abstract class Base ( val x: String)

  final class Derived(_x: String) extends Base(_x + " base") {
    override def toString() = x
  }

  def main(args: Array[String]): Unit = {
    val d  = new Derived( "hello")
    println( d) // hello
  }
}