Jade Dungeon

抽象成员

抽象成员

Scala中不仅可以指定方法为抽象,还可以声明字段甚至抽象类型为类和特质的成员。

在特质中分别对类型(T)、方法(transform)、val(initial)、var(current)的 抽象声明做出了一个例子:

抽象成员的快速浏览

  trait Abstract {
    type T
    def transform(x: T): T
    val initial: T
    var current: T
  }

实现:

  class Concrete extends Abstract {
    type T = String
    def transform(x: String) = x + x
    val initial = "hi"
    var current = initial
  }

类型成员

(略)

抽象val

val是不可变的,抽象的val指定了类型与变量名,不指定值:

  val initial: String

实现时指定值:

  val initial = "hi"

如果不知道类中定义的确切内容,但是确定对于每个实例来说值都是不可变的。在这样的 情况下可以使用抽象的val声明。

可以注意到,抽象val的格式非常类似于下面的抽象无参数方法声明:

  def initial: String

客户代码将使用统一的obj.initial方法引用val及方法。如果initial是抽象val,那么 客户就可以保证每次引用都将得到同样的值。如果initial是抽象方法那就无法保证, 因为在不同的实现中initial可以被实现为每次调用都返回不同的值。

换句话说抽象的val限制了合法实现的方式:任何实现都必须是val类型的定义不可以是 var。另一方面,抽象方法声明可以被实现为具体的方法定义或具体的val定义。

所以在下面的代码中,Apple是合法的子类而BadApple不是:

  abstract class Fruit {
    val v: String // `v' for value
    def m: String // `m' for method
  }

  abstract class Apple extends Fruit {
    val v: String
    val m: String // OK to override a `def' with a `val'
  }

  abstract class BadApple extends Fruit {
    def v: String // ERROR: cannot override a `val' with a `def'
    def m: String
  }

抽象var

在特质里使用,只声明类型与名称,没有初始值:

  trait AbstractTime {
    var hour: Int
    var minute: Int
  }

也会有自动扩展的gettersetter方法,上面的代码相当于:

  trait AbstractTime {
    def hour: Int          // getter for `hour'
    def hour_=(x: Int)     // setter for `hour'
    def minute: Int        // getter for `minute'
    def minute_=(x: Int)   // setter for `minute'
  }

初始化抽象val

结合特质来使用,抽象val可以让子类扩展提供父类没有的参数与细节。因为特质缺省能 用来传递参数的构造器。

拿前面的实数类来作例子,以下特质:

  trait RationalTrait { 
    val numerArg: Int 
    val denomArg: Int 
  } 

为了实例化这个特质,先要实现val。在这里我们要用到新的new语法结构:

  new RationalTrait {
    val numerArg = 1
    val denomArg = 2
  }

上面的代码会产混入了特质的匿名类实例,类似于new Rational(1, 2)。当然区别还是 有的:

  new Rational(expr1, expr2)

上面的两个表达式会在类初始化前计算,而相反的:

  new RationalTrait {
    val numerArg = expr1
    val denomArg = expr2
  }

上面的两个表达式会作为匿名类初始化的一部分计算。而匿名类初始化在RationalTrait 之后执行,所以numerArgdenomArg的值在RationalTrait初始化期间还没有准备好 ,都是Int类型的默认值0

所以对下面的代码来说,这会成为一个问题,因为其中定义了经过约分后的分子与分母:

  trait RationalTrait { 
    val numerArg: Int 
    val denomArg: Int 
    require(denomArg != 0)
    private val g = gcd(numerArg, denomArg)
    val numer = numerArg / g
    val denom = denomArg / g
    private def gcd(a: Int, b: Int): Int = 
      if (b == 0) a else gcd(b, a % b)
    override def toString = numer +"/"+ denom
  }

如果尝试使用某种分子和分母的表达式而不是简单的字面量实例化这个特质,会引起以下 错误:

  scala> val x = 2
  x: Int = 2

  scala> new RationalTrait {
       |   val numerArg = 1 * x
       |   val denomArg = 2 * x
       | }
  java.lang.IllegalArgumentException: requirement failed
          at scala.Predef$.require(Predef.scala:107)
          at RationalTrait$class.$init$(<console>:7)
          at $anon$1.<init>(<console>:7)
          ....

解决方案有两个,分别是预初始化字段和懒加载val。

预初始化字段

用于匿名类实例

给字段定义加上花括号,放在超类的构造器之前:

  scala> new { 
       |   val numerArg = 1 * x
       |   val denomArg = 2 * x 
       | } with RationalTrait
  res15: java.lang.Object with RationalTrait = 1/2
用于类

不仅匿名类可以用预加载,有名称的类和单例对象也可以。

注意要放在关键字extends后面:

  object twoThirds extends {
    val numerArg = 2
    val denomArg = 3
  } with RationalTrait

由于预初始化的字段的超类构造器调用前被初始化,所以不能引用正在被构造的对象。所以 对于this实际指向的是正被构造的类或对象的对象,而来是被构造的对象本身:

  scala> new {
     |   val numerArg = 1
     |   val denomArg = this.numerArg * 2
     | } with RationalTrait
  <console>:8: error: value numerArg is not a
       member of object $iw
           val denomArg = this.numerArg * 2
                               ^

因为实例还没有构建完成,所以会报错。$iw是合成对象,解释器把用户输出语句放在 这个对象中。

  class RationalClass(n: Int, d: Int) extends {
    val numerArg = n
    val denomArg = d
  } with RationalTrait {
    def + (that: RationalClass) = new RationalClass(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
  }

懒加载val

懒加载让表达式在val第一次被使用的时候才计算机。格式为把lazy修饰加在val上。

普通情况下初始化与类初始化一起的:

  scala> object Demo {
       |   val x = { println("initializing x"); "done" }
       | }
  defined module Demo


  scala> Demo
  initializing x
  res19: Demo.type = Demo$@97d1ff

  scala> Demo.x
  res20: java.lang.String = done

使用了懒加载以后,val的初始化延迟到第一次使用时:

  scala> object Demo {
       |   lazy val x = { println("initializing x"); "done" }
       | }
  defined module Demo

  scala> Demo
  res21: Demo.type = Demo$@d81341

  scala> Demo.x
  initializing x
  res22: java.lang.String = done

上面的情况有点像是用defx定义为一个无参的方法,不同于def的是计算只进行一次。

通过上面两个例子可以看出,单例对象的初始化也很像懒加载。它们在第一次被使用时进行 初始化。

通过懒加载重新实现RationalTrait,与前一版本的主要变化是require子句从特质的 方法体移动到了计算numerArgdenomArg最大公约数的私有字段g的初始化器中。 所以这个版本中LazyRationalTrait初始化器已经用不干啥事儿了:

  trait LazyRationalTrait { 
    val numerArg: Int 
    val denomArg: Int 
    lazy val numer = numerArg / g
    lazy val denom = denomArg / g
    override def toString = numer +"/"+ denom
    private lazy val g = {
      require(denomArg != 0)
      gcd(numerArg, denomArg)
    }
    private def gcd(a: Int, b: Int): Int = 
      if (b == 0) a else gcd(b, a % b)
  }


  scala> val x = 2
  x: Int = 2

  scala> new LazyRationalTrait {
       |   val numerArg = 1 * x
       |   val denomArg = 2 * x
       | }
  res1: java.lang.Object with LazyRationalTrait = 1/2

在特质中的两个懒加载对象numberdenom是在toString方法调用时才初始化。计算 它们的表达式要用到同样是懒加载的g

应用懒加载还是要注意副作用,在有副作用的情况下跟踪加载顺序是很重要的事情。 无副作用的纯函数式应用配合懒加载是相当合适的。

懒加载并不是没有额外的开销,对于每次访问都会有一个方法被调用。这个方法会以线程 安全的方式检查该值是否已经被初始化。

抽象类型

抽象类型声明type T应用在尚不可知的类型上,不同的子类可以提供不同的T实现。

以一个动物食性的例子来解释应用环境,动物吃食物:

  class Food

  abstract class Animal {
    def eat(food: Food)
  }

会在想让它们的子类牛吃草时遇到麻烦。eat方法不能重写,因为参数不能从Food转为 子类Grass

  class Grass extends Food

  class Cow extends Animal {
    override def eat(food: Grass) {} // This won't compile
  }

  BuggyAnimals.scala:7: error: class Cow needs to be
  abstract, since method eat in class Animal of type
      (Food)Unit is not defined
  class Cow extends Animal {
        ^
  BuggyAnimals.scala:8: error: method eat overrides nothing
    override def eat(food: Grass) {}
                 ^

这样看来类型检查太严格了,应但是如果允许子类的话又会失去类型检验保障。比如说喂牛 吃鱼:

  class Food
  class Grass extends Food
  class Fish extends Food

  abstract class Animal {
    def eat(food: Food)
  }

  class Cow extends Animal {
    override def eat(food: Grass) {} // This won't compile,
  }                                  // but if it did,...

  val bessy: Animal = new Cow
  bessy eat (new Fish)               // ...you could feed fish to cows.

抽象类型也可以加上界与下界

更加精确的方式是能按不同的动物决定食物的种类,父类中指定动物只能吃食物:

  class Food

  abstract class Animal {
    type SuitableFood <: Food
    def eat(food: SuitableFood)
  }

SuitableFood被定义为抽象类,而且有上界Food。以后动物类中指定具体的食物子类:

  class Grass extends Food
  class Cow extends Animal {
    type SuitableFood = Grass
    override def eat(food: Grass) {}
  }

现在得到了比较合适的类型检查:

  scala> class Fish extends Food
  defined class Fish

  scala> val bessy: Animal = new Cow
  bessy: Animal = Cow@674bf6

  scala> bessy eat (new Fish)
  <console>:10: error: type mismatch;
   found   : Fish
   required: bessy.SuitableFood
         bessy eat (new Fish)
                    ^

注意路径依赖

看一下前面例子的最后一条错误信息。它说明需要的类型是bessy.SuitableFood,这里的 SuitableFoodbessy引用的对象的成员。

这样的类型被称为路径依赖类型(参见对象一章的路径部分),路径指的是对象的引用。 不同路径将产不同的类型:

  class DogFood extends Food
  class Dog extends Animal {
    type SuitableFood = DogFood
    override def eat(food: DogFood) {}
  }

  scala> val bessy = new Cow
  bessy: Cow = Cow@10cd6d

  scala> val lassie = new Dog
  bootsie: Dog = Dog@d11fa6

  scala> lassie eat (new bessy.SuitableFood)
  <console>:13: error: type mismatch;
   found   : Grass
   required: DogFood
         lassie eat (new bessy.SuitableFood)
                     ^

bessy.SuitableFood不能匹配lassie.SuitableFood,但如果同样是Dog的话,情况又 不同。因为DogSuitableFood被定义为DogFood类的别名,所以实际上是一样的:

  scala> val bootsie = new Dog
  bootsie: Dog = Dog@54ca71

  scala> lassie eat (new bootsie.SuitableFood)

同时指定上界与下界

格式:T >: Lower <: Upper

对于类型层级:

class Food
class Grass extends Food
class Meat extends Food
class Mutton extends Meat
class Beef extends Meat
class DogFood extends Meat
class BadDogFood extends DogFood

指定上界与下界就是:SuitableFood >: DogFood <: Meat。用图表示:

图

抽象类型表示:

abstract class Animal {
  type SuitableFood >: DogFood <: Meat
  def eat(food: SuitableFood)
}

子类中只能用指定的类型范围:

class Dog extends Animal {
  type SuitableFood = Food                 // ERROR
  override def eat(food: Food) {}
}

class Dog extends Animal {
  type SuitableFood = Meat                 // OK
  override def eat(food: Meat) {}
}

class Dog extends Animal {
  type SuitableFood = DogFood              // OK
  override def eat(food: DogFood) {}
}

class Dog extends Animal {
  type SuitableFood = BadDogFood           // ERROR
  override def eat(food: BadDogFood) {}
}

对于子类中确定的类型范围来说:

class Dog extends Animal {
  type SuitableFood = Meat
  override def eat(food: Meat) {}
}

只要是确定类型的子类就可以:

val teo = new Dog

teo.eat(new Food)        // ERROR
teo.eat(new Grass)       // ERROR
teo.eat(new Meat)        // OK
teo.eat(new Mutton)      // OK
teo.eat(new Beef)        // OK
teo.eat(new DogFood)     // OK
teo.eat(new BadDogFood)  // OK

抽象类型与泛型

抽象类型与泛型可实现相同的功能。例:

trait Reader {
  type Contents
  def read(fileName: String): Contents
}

子类中指定Contents类型为String

import scala.io._

class StringReader extends Reader {
  type Contents = String
  def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}

import java.awt.image._
import java.io._
import javax.imageio._

class ImageReader extends Reader {
  type Contents = BufferedImage
  def read(fileName: String) = ImageIO.read(new File(fileName))
}

用泛型也可以实现同样的功能:

trait Reader[C] {
  def read(fileName: String): C
}

import scala.io._

class StringReader extends Reader[String] {
  def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}

import java.awt.image._
import java.io._
import javax.imageio._

class ImageReader extends Reader[BufferedImage] {
  def read(fileName: String) = ImageIO.read(new File(fileName))
}

用哪种好呢?一般经验是:

  • 如果类型是在创建实例时确定的,用泛型,如:HashMap[String, Int]
  • 如果是在子类中确定的,用抽象类型。如这里的Reader例子。

虽然在子类中确定类型时也可以用泛型,但如果有多个类型参数会显得比较啰嗦,如: Reader[File, BufferedImabe, .... ]

而抽象类型不用带这么多参数:

trait Reader {
  type In
  type Contents
  def read(in: In)
}

import scala.io._

class StringReader extends Reader {
  type In = String
  type Contents = String
  def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}

import java.awt.image._
import java.io._
import javax.imageio._

class ImageReader extends Reader {
  type In = File
  type Contents = BufferedImage
  def read(file: In) = ImageIO.read(file)
}

依赖注入

Scala中可以通过特质、抽象成员与自身类型实现简单的依赖注入效果。

  • 特质定义接口与部分实现。
  • 自身类型指定特质只能混入特定的类。
  • 抽象成员实现在实例与子类中确定成员。

例子,带日志的登录功能:

先是日志组件,有控制台与文件两个实现版本:

trait LoggerComponent {

  trait Logger { def log(msg: String) }

  val logger: Logger

  class ConsoleLogger extends Logger {
    def log(msg: String) { println(msg); }
  }

  class FileLogger(file: String) extends Logger { 
    val out = new PrintWriter(file) 
    def log(msg: String) { out.println(msg); out.flush() }
  }
 
}

注意val logger: Logger用一个抽象的日志工具,具体实现类用哪一个版本的以后再注入 的时候再决定。

再来看登录组件,它的内部只定义一个用来测试的MockAuth实现:

trait AuthComponent {

  this: LoggerComponent => // Gives access to logger    

  trait Auth { 
    def login(id: String, password: String) = {
      if (check(id, password)) true
      else {
        logger.log("login failure for " + id)
        false
      }
    }
    def check(id: String, password: String): Boolean 
  }

  val auth: Auth

  class MockAuth(file: String) extends Auth { 
    val props = new java.util.Properties()
    props.load(new FileReader(file))
    def check(id: String, password: String) = props.getProperty(id) == password
  }
}

这里除了val auth: Auth是抽象的成员以外,还用自身类型this: LoggerComponent => 说明了要依赖一个日志组件。

在把这两个组件组成一个单对象时指定它们中抽象成员的具体类型:

object AppComponents extends LoggerComponent with AuthComponent {
  val logger = new FileLogger("test.log")
  val auth = new MockAuth("users.txt")
}

建立一个文件users.txt来存用户名密码模拟数据库:

cay=secret

运行测试一下:

scala> import AppComponents._
import AppComponents._

scala> if (auth.login("cay", "secret"))
     |   logger.log("cay is logged in")

scala> println("Look inside test.log")
Look inside test.log

日志内容输出在:test.log文件中。

案例研究:货币

设计一个货币类能处理不同的货币。定义抽象类可以扩展为具体不同的货币。当然第一个 版本肯定是不完善的:

  // A first (faulty) design of the Currency class
  abstract class Currency {
    val amount: Long
    def designation: String 
    override def toString = amount +" "+ designation
    def + (that: Currency): Currency = ...
    def * (x: Double): Currency = ...
  }

amountdesignation分别代表金额和表示金额的符号。其他方法还有加法和乘法操作 。这个版本的问题是,在语法上两个不同的子类可以相加:

  abstract class Dollar extends Currency {
    def designation = "USD"
  }
  abstract class Euro extends Currency {
    def designation = "Euro"
  }

这样不同货币的相加是有问题的。所以下一个改进版本用抽象类型来标明末知的类型:

  abstract class AbstractCurrency {
    type Currency <: AbstractCurrency
    val amount: Long
    def designation: String 
    override def toString = amount +" "+ designation
    def + (that: Currency): Currency = ...
    def * (x: Double): Currency = ...
  }

每个子类都要把Currency指定为这个类自身,扩展实现是类似于这样:

  abstract class Dollar extends AbstractCurrency {
    type Currency = Dollar
    def designation = "USD"
  }

这个版本的问题在于加法与乘法的定义。首先想到的把金额转为正确类型的货币的方法可能 是这样的:

  def + (that: Currency): Currency = new Currency {
    val amount = this.amount + that.amount
  }

但这通不过编译,因为Scala不能用抽象类型new出实例来,即使是作为其他类型的父类:

  error: class type required
    def + (that: Currency): Currency = new Currency {

使用工厂方法是个解决方案:用声明抽象方法代替直接创建抽象类的实例:

  abstract class AbstractCurrency {
    type Currency <: AbstractCurrency // abstract type
    def make(amount: Long): Currency  // factory method
    ...                               // rest of class
  }

但这样有别的问题,因为这样不得不把工厂方法放到AbstractCurrency类中,所有的实例 都可以调用make方法,也就都有了创建货币的能力:

  myDollar.make(100)  // here are a hundred more!

所以把工厂方法移到一个新的类中,新的类叫CurrencyZone。把AbstractCurrencyCurrency也作为它的内部类:

  abstract class CurrencyZone {
    type Currency <: AbstractCurrency
    def make(x: Long): Currency
    abstract class AbstractCurrency {
      val amount: Long
      def designation: String 
      override def toString = amount +" "+ designation
      def + (that: Currency): Currency = 
        make(this.amount + that.amount)
      def * (x: Double): Currency = 
        make((this.amount * x).toLong)
    }
  }

这样按不同货币来扩展:

  object US extends CurrencyZone {
    abstract class Dollar extends AbstractCurrency {
      def designation = "USD"
    }
    type Currency = Dollar
    def make(x: Long) = new Dollar { val amount = x }
  }

US中定义了类Dollar。它的类型是US.Dollar

继续改进设计:关于单位,单位不仅是美元,还有美分。所以让amount以美分为单位比较 合适。所以用多一个字段CurrencyUnit记录单位:

  class CurrencyZone { 
    ... 
    val CurrencyUnit: Currency 
  } 

子类里再加上两个方法直接把1美元代表100美分的逻辑描述出来:

  object US extends CurrencyZone {
    abstract class Dollar extends AbstractCurrency {
      def designation = "USD"
    }
    type Currency = Dollar
    def make(cents: Long) = new Dollar {
      val amount = cents
    }
    val Cent = make(1)
    val Dollar = make(100)
    val CurrencyUnit = Dollar
  }

还有显示问题,用多数内部类型上都带的format方法格式化美元与美分的小数显示,如 10.23 USD

    ((amount.toDouble / CurrencyUnit.amount.toDouble)
     formatted ("%."+ decimals(CurrencyUnit.amount) +"f")
     +" "+ designation)

输出字符的长度是通过decimals方法得出的。decimals方法返回十进制数字所要占用的 字符长度。如对于decimals(10)代表0到9,会占用一个字符,而decimals(100)是0到99 会占用两个字符。decimals方法通过简单递归实现:

  private def decimals(n: Long): Int = 
    if (n == 1) 0 else 1 + decimals(n / 10)

相对的看一下欧元的实现:

  object Europe extends CurrencyZone {
    abstract class Euro extends AbstractCurrency {
      def designation = "EUR"
    }
    type Currency = Euro
    def make(cents: Long) = new Euro {
      val amount = cents
    }
    val Cent = make(1)
    val Euro = make(100)
    val CurrencyUnit = Euro
  }

  object Japan extends CurrencyZone {
    abstract class Yen extends AbstractCurrency {
      def designation = "JPY"
    }
    type Currency = Yen
    def make(yen: Long) = new Yen {
      val amount = yen
    }
    val Yen = make(1)
    val CurrencyUnit = Yen
  }

再改进一下,增加汇率的功能。先用一个新的对象来记录汇率:

  object Converter {
    var exchangeRate = Map(
      "USD" -> Map("USD" -> 1.0   , "EUR" -> 0.7596, 
                   "JPY" -> 1.211 , "CHF" -> 1.223),
      "EUR" -> Map("USD" -> 1.316 , "EUR" -> 1.0   , 
                   "JPY" -> 1.594 , "CHF" -> 1.623),
      "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, 
                   "JPY" -> 1.0   , "CHF" -> 1.018),
      "CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160, 
                   "JPY" -> 0.982 , "CHF" -> 1.0  )
    )
  }

在货币中增加根据汇率来转换的功能。接收一个外币类型,把自己的金额转成这个外币的 金额:

  def from(other: CurrencyZone#AbstractCurrency): Currency = 
    make(Math.round(
      other.amount.toDouble * Converter.exchangeRate
        (other.designation)(this.designation)))

参数是末知的CurrencyZone#AbstractCurrency,所以能处理任意外币类型。

全部的货币代码,假设都放在org.stairwaybook.currencies包中:

  abstract class CurrencyZone {

    type Currency <: AbstractCurrency
    def make(x: Long): Currency

    abstract class AbstractCurrency {

      val amount: Long
      def designation: String 

      def + (that: Currency): Currency = 
        make(this.amount + that.amount)
      def * (x: Double): Currency = 
        make((this.amount * x).toLong)
      def - (that: Currency): Currency = 
        make(this.amount - that.amount)
      def / (that: Double) = 
        make((this.amount / that).toLong)
      def / (that: Currency) = 
        this.amount.toDouble / that.amount

      def from(other: CurrencyZone#AbstractCurrency): Currency = 
        make(Math.round(
          other.amount.toDouble * Converter.exchangeRate
            (other.designation)(this.designation)))

      private def decimals(n: Long): Int = 
        if (n == 1) 0 else 1 + decimals(n / 10)

      override def toString = 
        ((amount.toDouble / CurrencyUnit.amount.toDouble)
         formatted ("%."+ decimals(CurrencyUnit.amount) +"f")
         +" "+ designation)
    }

    val CurrencyUnit: Currency
  }

调用的例子:

  scala> import org.stairwaybook.currencies._

  scala> Japan.Yen from US.Dollar * 100
  res16: Japan.Currency = 12110 JPY

  scala> Europe.Euro from res16
  res17: Europe.Currency = 75.95 EUR

  scala> US.Dollar from res17
  res18: US.Currency = 99.95 USD

相同类型的货币可以相加,不同类型的不可以相加:

  scala> US.Dollar * 100 + res18
  res19: currencies.US.Currency = 199.95


  scala> US.Dollar + Europe.Euro
  <console>:7: error: type mismatch;
   found   : currencies.Europe.Euro
   required: currencies.US.Currency
         US.Dollar + Europe.Euro
                            ^

类型抽象实现了不同货币不能相加的功能。像是1999年9月23日,混用英制单位和公制单位 引起的火星航天器坠毁事件不会再重演了。