- 如果类或者特质的某个成员在当前类中没有完成的定义,则这个成员就是抽象的。抽象成员的本意是强制子类进行实现。
Scala
相对Java
泛化了抽象字段的意义,存在四种抽象成员,val
,var
,方法和类型。
抽象成员概述
trait Abstract { type T def transform(x: T): T val initial: T var current: T }
- 声明了四种抽象成员,抽象类型,抽象方法,抽象
val
和抽象var
。
- 声明了四种抽象成员,抽象类型,抽象方法,抽象
- 在
Scala
中,抽象的类和特质不叫抽象类型,抽象类型永远是类或者特质的一个成员。
- 在
- 使用关键字
type
可以为真名冗长或者含义不明显的类型定义一个别名;另一个主要用于是声明子类必须定义的抽象类型。
- 使用关键字
- 抽象
val
限制了它的具体实现,因为def
实现的方法可能每次的返回值都不一样,因此抽象val
的实现只能是val
。
- 抽象
- 抽象
var
,抽象var
在类中或者特质中定义的时候也会自动生成对应的def name
和def name_=
方法
- 抽象
初始化抽象val
- 抽象
val
有时会承担超类参数的职能,允许在子类中提供那些在超类中缺失的细节。对于特质来说是很重要的,因为在特质中并没有类参数,因此通常来说特质的参数化是通过子类中实现抽象val
实现的。
- 抽象
trait RationalTrait { val numerArg: Int val denomArg: Int }
- 2.一种实例化方法:
new RationalTrait {val numerArg = 1;val denomArg = 2}
new
出现在特质名称RationalTrait
之前,然后是用花括号括起来的定义体。这个表达式交出的是一个混入了特质并由定义体定义的匿名类的实例。表达式初始化的顺序有一些细微的差异。new Rational(expr1, expr2)
中expr1
和expr2
会在类Rational
初始化之前被求值,这样expr1
和expr2
对于Rational
类的初始化过程是可见的。对于特质而言,
new RationalTrait {val numerArg = 1;val denomArg = 2}
expr1
和expr2
这两个表达式是作为匿名类初始化过程中的一部分被求值的,但是这个匿名类是在RationalTrait
特质之后被初始化的。因此,在RationalTrait
的初始化过程中,expr1
和expr2
都为0
,不可用。说明了类参数和抽象字段初始化顺序的差异。解决这个问题有两种方式:预初始化字段和懒加载val字段。
- 预初始化字段是指在超类被调用之前初始化子类的字段,例如:
object twoThirds extends { val numerArg = 2 val denomArg = 3 } with RationalTrait
还有一种更为通用的写法
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 ) }
由于初始化字段在超类的构造方法之被调用,因此使用this
的时候,this
指向的不是{}
本身,而是new{}
这个对象。初始化字段的行为类似于类参数,相当于class Test(a: Int)
这样的形式,a
是类参数,但不是类中的字段。
scala> new { val numerArg = 1 val denomArg = this.numerArg * 2 } with RationalTrait <console>:11: error: value numerArg is not a member of object $iw val denomArg = this.numerArg * 2 ^```
- 另外一种解决方法是使用懒加载的
val
,使用预初始化字段可以精确模拟类构造方法的入参初始化行为,如果希望系统自己能搞定应有的初始化顺序时,将val
定义为惰性的即可,在val
上加上lazy
,右侧的初始化表达式只会在val
第一次被使用时求值。将接口中涉及到子类初始化的字段全部设置成为lazy
,得到以下接口:
- 另外一种解决方法是使用懒加载的
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) }
使用
new LazyRationalTrait { val numerArg = 1 val denomArg = 2 } res7: LazyRationalTrait = 1/2
这样的代码完全没问题,因为在特质中涉及到需要子类覆盖的字段都是lazy
的,只有在第一次被访问时才进行初始化。
初始化过程。1.LazyRationalTrait
初始化;2.需要new
对象的匿名类初始化;3.解释器调用对象的toString
方法进行打印:触发numer
初始化,numerArg
已经被初始化为1
,触发g
初始化,denomArg
已经被初始化为2
,g
完成初始化,numer
初始化完成,toString
中继续触发denom
。
-
lazy val
的初始化顺序和其在代码中的定义顺序并没有任何关系,因为其值会按需初始化。lazy val
可以避免程序员一直组织val
的初始化顺序保证在使用时已经正确定义。但这种优势只有在val
的初始化是纯函数式的,没有副作用,对函数式对象而言初始化顺序并不重要,最后只要初始化完成即可。但是指令式的代码中,lazy val
的初始化顺序变得难以跟踪,所以是函数式对象的完美补充。
-
抽象类型
-
type T
是在类继承关系下游中被定义的类型,在Scala
中,对override
中的类型检查严格,主要还是继承树上C-F-C1
的问题,如果override
中参数可以是F
的,则具体的子类可以传不配套的C1
类型,导致出现牛吃鱼的问题。override
中参数类型必须是严格匹配的,不允许使用子类覆盖父类这种写法。
class Food abstract class Animal { def eat(food: Food) } class Grass extends Food class Cow extends Animal { override def eat(food: Grass) = {} // This won't compile } class Fish extends Food 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) }
至于具体的Animal
该吃什么食物,在Animal
这个层次并不能确定。定义抽象类型用于子类各自实现。
class Grass extends Food class Cow extends Animal { type SuitableFood = Grass override def eat(food: Grass) = {} }
这时候使用bessy eat (new Fish)
会编译出错,SuitableFood
类型是不对的。
路径依赖类型
-
bessy.SuitableFood
这样的type
称为路径依赖类型,路径指的是对对象的引用。
-
- 在
Scala
中也支持内部类,内部类的寻址是通过Outer#Inner
这样的语法,内部类的类型和外部类对象有关,new Outer.Inner
的类型和new Outer.Inner
的类型是不一样的,但都是Outer#Inner
的子类。和Java
一样,Scala
内部类中会保留一个到外部类实例的引用。允许内部类访问外部类的成员,因此在实例化内部类的时候必须以某种方式给出外部类的实例。注意,直接new Outer#Inner
是不行的,因为没有Outer
的实例。
- 在
改良类型
- 结构子类型,如果某个类型除了成员之外没有更多的信息可以使用结构子类型。例如,如果想定义一个食草动物的列表,一种选择是定义
AnimalThatEatGrass
特质,并在对应的类上混入,另一种方式是使用改良类型,使用基类Animal
,再加上一系列使用花括号括起来的成员即可。花括号中的成员进一步指定(改良)了基类中的成员类型。
Animal { type SuitableFood = Grass } val animals = List[Animal { type SuitableFood = Grass }](new Cow)
枚举
- 在
Scala
中的枚举是使用scala.Enumeration
来表示的。
object Color extends Enumeration { val Red = Value val Green = Value val Blue = Value }
Red,Green,Blue
就是普通的对象,不过是字母大写了而已,使用Color.Red
照样进行访问,每一个都是一个Value
类型的对象,Color.Value
和Weekday.Value
是不一样的类型。常用的方法有values
来获取名称,也可以使用id
来获取名称。在Enumeration
中定义了一个HashMap
,Int -> Value
类型,Value
中有两个成员,一个是Int
用来表示id
,一个是String
表示的Name
,如果出现Value("readableName")
,Name
中就会保存readableName
。
货币实例
- 对于不能创建抽象类型以及抽象类实例的情况,可以采用工厂方法绕过这一限制。
type Currency <: AbstractCurrencydef make(amount: Long): Currency ...
网友评论