抽象成员
抽象成员
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 }
也会有自动扩展的getter
与setter
方法,上面的代码相当于:
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
之后执行,所以numerArg
和denomArg
的值在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
上面的情况有点像是用def
把x
定义为一个无参的方法,不同于def
的是计算只进行一次。
通过上面两个例子可以看出,单例对象的初始化也很像懒加载。它们在第一次被使用时进行 初始化。
通过懒加载重新实现RationalTrait
,与前一版本的主要变化是require
子句从特质的
方法体移动到了计算numerArg
和denomArg
最大公约数的私有字段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
在特质中的两个懒加载对象number
和denom
是在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
,这里的
SuitableFood
是bessy
引用的对象的成员。
这样的类型被称为路径依赖类型(参见对象一章的路径部分),路径指的是对象的引用。 不同路径将产不同的类型:
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
的话,情况又
不同。因为Dog
的SuitableFood
被定义为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 = ... }
amount
和designation
分别代表金额和表示金额的符号。其他方法还有加法和乘法操作
。这个版本的问题是,在语法上两个不同的子类可以相加:
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
。把AbstractCurrency
和
Currency
也作为它的内部类:
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日,混用英制单位和公制单位 引起的火星航天器坠毁事件不会再重演了。