Chapter 20 《Abstract Members》

作者: liqing151 | 来源:发表于2018-07-06 19:24 被阅读2次
    • 如果类或者特质的某个成员在当前类中没有完成的定义,则这个成员就是抽象的。抽象成员的本意是强制子类进行实现。Scala相对Java泛化了抽象字段的意义,存在四种抽象成员,valvar,方法和类型。

    抽象成员概述

    trait Abstract {
    type T
    def transform(x: T): T
    val initial: T
    var current: T
    }
    
      1. 声明了四种抽象成员,抽象类型,抽象方法,抽象val和抽象var
      1. Scala中,抽象的类和特质不叫抽象类型,抽象类型永远是类或者特质的一个成员。
      1. 使用关键字type可以为真名冗长或者含义不明显的类型定义一个别名;另一个主要用于是声明子类必须定义的抽象类型。
      1. 抽象val限制了它的具体实现,因为def实现的方法可能每次的返回值都不一样,因此抽象val的实现只能是val
      1. 抽象var,抽象var在类中或者特质中定义的时候也会自动生成对应的def namedef name_=方法

    初始化抽象val

      1. 抽象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)expr1expr2会在类Rational初始化之前被求值,这样expr1expr2对于Rational类的初始化过程是可见的。对于特质而言,

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

    expr1expr2这两个表达式是作为匿名类初始化过程中的一部分被求值的,但是这个匿名类是在RationalTrait特质之后被初始化的。因此,在RationalTrait的初始化过程中,expr1expr2都为0,不可用。说明了类参数和抽象字段初始化顺序的差异。解决这个问题有两种方式:预初始化字段和懒加载val字段。

      1. 预初始化字段是指在超类被调用之前初始化子类的字段,例如:
    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
    ^```
    
      1. 另外一种解决方法是使用懒加载的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已经被初始化为2g完成初始化,numer初始化完成,toString中继续触发denom

      1. 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类型是不对的。


    路径依赖类型

      1. bessy.SuitableFood这样的type称为路径依赖类型,路径指的是对对象的引用。
      1. 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.ValueWeekday.Value是不一样的类型。常用的方法有values来获取名称,也可以使用id来获取名称。在Enumeration中定义了一个HashMapInt -> Value类型,Value中有两个成员,一个是Int用来表示id,一个是String表示的Name,如果出现Value("readableName")Name中就会保存readableName


    货币实例

    • 对于不能创建抽象类型以及抽象类实例的情况,可以采用工厂方法绕过这一限制。
    type Currency <: AbstractCurrencydef make(amount: Long): Currency ...
    

    相关文章

      网友评论

        本文标题:Chapter 20 《Abstract Members》

        本文链接:https://www.haomeiwen.com/subject/mndyuftx.html