Idiomatic Scala way to deal with base vs derived c

2020-02-11 02:02发布

问题:

Consider the following base and derived classes in Scala:

    abstract class Base( val x : String )

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

Here, the identifier 'x' of the Derived class parameter overrides the field of the Base class, so invoking toString like this:

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

returns the Derived value and gives the result "string".

So a reference to the 'x' parameter prompts the compiler to automatically generate a field on Derived, which is served up in the call to toString. This is very convenient usually, but leads to a replication of the field (I'm now storing the field on both Base and Derived), which may be undesirable. To avoid this replication, I can rename the Derived class parameter from 'x' to something else, like '_x':

    abstract class Base( val x : String )

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

Now a call to toString returns "Base's string", which is what I want. Unfortunately, the code now looks somewhat ugly, and using named parameters to initialize the class also becomes less elegant:

    new Derived( _x = "string" ) 

There is also a risk of forgetting to give the derived classes' initialization parameters different names and inadvertently referring to the wrong field (undesirable since the Base class might actually hold a different value).

Is there a better way?

Edit 1: To clarify, I really only want the Base values; the Derived ones just appear to be necessary for initializing the fields of the base class. The example only references them to illustrate the ensuing issues.

Edit 2: Actually, the example would have been clearer if I had used vars instead of vals, since that highlights the problem with values getting changed later on in the base class:

    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

Edit 3: It might be nice to have a way to suppress automatic field generation if the derived class would otherwise end up hiding a base class field. It would appear that the Scala compiler could actually have been designed to do this for you, but of course this contradicts the more general rule of "nearer" identifiers (the Derived class' 'x') hiding more remote ones (the Base class' 'x'). It seems like a reasonably nice solution would be a modifier like 'noval', maybe like this:

    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

回答1:

The idiomatic way to avoid duplicating the field would be to write

abstract class Base { val x: String }

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

However, in your version it looks like you actually want a second field, since you have two distinct values. As you correctly point out, giving these fields the same name is likely to lead to confusion.

Since you don't actually need the constructor argument outside of the constructor, you could use this approach (a private constructor with a companion module that acts as a factory):

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)
}


回答2:

As the base class is abstract, it doesn't look as though you really want a val (or var) in the Base class, with the associated backing field. Instead, you're simply looking to make a guarantee that such a thing will be available in concrete subclasses.

In Java, you'd use an accessor method such as getX to achieve this.

In Scala, we can go one better, vals, vars and defs occupy the same namespace, so a val can be used to implement an abstract def (or to override a concrete def, if that's what floats your boat). More formally, this is known as the "Uniform Access Principle"

abstract class Base{ def x: String }

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

If you need for x to be settable via a reference to Base, you also need to declare the "setter" (which can then be implemented with a var):

abstract class Base {
  def x: String
  def x_=(s: String): Unit
}

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

(not that I would ever encourage mutability in any design; unless there was an especially compelling justification for it. There are too many good reasons for favouring immutability by default)

UPDATE

The benefits of this approach are:

  • x could be an entirely synthetic value, implemented entirely in terms of other values (i.e. the area of a circle for which you already know the radius)
  • x can be implemented at any arbitrary depth in the type hierarchy, and doesn't have to be explicitly passed through each intervening constructor (having a different name each time)
  • There's only a single backing field required, so no memory is wasted
  • As it now doesn't need a constructor, Base could be implemented as a trait; if you so desire


回答3:

You can try this:

abstract class Base( val x : String )

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

Then

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

prints exactly what you want



回答4:

you already provided the answer that works

abstract class Base( val x : String )

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

If the problem is that _x is not a nice name, then you should use a meaningful one. Alternatively, you could declare your classes as follows

abstract class Base( val _x : String )

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

And now you would have the "nice" syntax for initializing Derived instances.

If scala was to allow

a way to suppress automatic field generation if the derived class would otherwise end up hiding a base class field.

This to me seems a very low-level detail that you don't want to deal with in the code. If this can be done safely, the compiler should do it for you.



回答5:

As suggested by @jsuereth I created an enhancement ticked for Scala, just for the record, that I hope correctly summarizes the content of the discussions here. Thanks for all of your input! The ticket can be found here, the content below: https://issues.scala-lang.org/browse/SI-4762

Inadvertent shadowing of base class fields in derived classes, Warning desirable

Issue arises whenever (a) a class parameter in a derived class uses the same symbol as a field or function in a base class and (b) that base class symbol is subsequently accessed in the derived class. The derived class parameter causes the compiler to auto-generate a field of the same name that shadows the base class symbol. Any reference to that symbol in the Derived class, intended to refer to the base class field, then inadvertently (a) causes duplicate definition of the field and (b) unexpectedly (but correctly) refers to the auto-generated field in the derived class.

Code example:

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

Since the 'Base' class has 'val' to generate the field and the derived class does not, the developer clearly intends to use the 'Derived' 'x' only for pass-through to Base, and therefore expects the reference in 'toString' to yield the Base value. Instead, the reference in 'toString' causes the compiler to automatically generate a field 'Derived.x' that shadows the 'Base.x' field, resulting in an error. The compiler behaves correctly, but the result is a programming error.

Usage scenario (with var fields for clearer illustration of the risks):

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

Since this issue arises whenever anyone uses the default way to initialize base class fields from a derived class in Scala, the scenario would appear to be extremely common and result in lots of programming errors (easily fixed, but still) for newer users.

An easy work-around for this issue exists (use differing names for the derived class parameter, like '_x', 'theX', 'initialX', etc.), but this introduces unwanted extra symbols.

Solution A (Minimal): issue a warning whenever the compiler infers that a class parameter requires an auto-generated field in a derived class that would shadow a symbol already defined in a base class.

Solution B: the work-around, still required with Solution A, is to come up with a new symbol name each time one initializes a base class field. This scenario comes up all the time and polluting the namespace with workaround field names like '_x' and 'theX' seems undesirable. Instead, it might be nice to have a way to suppress automatic field generation if the developer determines that the derived class symbols would otherwise end up hiding a base class symbol (e.g., following the warning of Solution A). Maybe a useful addition to Scala would be a modifier like 'noval' (or 'passthrough' or 'temp', or whatever - in addition to 'val' and 'var'), like this:

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


回答6:

Ok, first of all I would like to point out that the answer from @Yuriy Zubarev is probably what you really want to have. Secondly, I think the problem might lie in your design. Check this out. This is the part of your code:

extends Base( "Base's " + _x )

So some value x comes into your derived class and gets modified with the information (in this case "Base's " + ...). Do you see the problem? Why does your derived type knows something that your base type actually should have known? Here is the solution I propose.

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
}

This might sound harsh, but if this solution helps you out it automatically means that you had bad design. If on the other hand it does not help, than I probably don't understand you problem correctly and therefore apologize right away.



回答7:

@Gregor Scheidt your code does not work if I move toString() down to Derived, as the following:

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,

A parameter such as class Foo(x : Int) is turned into a field if it is referenced in one or more methods

And Martin's reply confirms its truth:

That's all true, but it should be treated as an implementation technique. That's why the spec is silent about it.

since there is no way to prevent the compiler's action, my option is that a more robust way to reference base class field is to use a different name, for example using a underscore "_" as prefix, as the following:

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
  }
}