Scala编程(第19章:类型参数化)

1.函数式队列:纯函数式队列还跟列表有一些相似,它们都被称作完全持久化的数据结构,在经过扩展或修改后,老版本将继续使用:

trait Queue[+T] {
  def head: T
  def tail: Queue[T]
  def enqueue[U>:T](x: U): Queue[U]
}

object Queue {
  def apply[T](xs: T*): Queue[T] = new QueueImpl(xs.toList, Nil)

  private class QueueImpl[T](
    private[this] var leading: List[T],
    private[this] var trailing: List[T]
  ) extends Queue[T] {
    private def mirror() =
      if (leading.isEmpty) {
        while (!trailing.isEmpty) {
          leading = trailing.head :: leading
          trailing = trailing.tail
        }
      }

    def head: T = {
      mirror()
      leading.head
    }

    def tail: QueueImpl[T] = {
      mirror()
      new QueueImpl(leading.tail, trailing)
    }

    def enqueue[U>:T](x: U):Queue[U]= new QueueImpl(leading, x :: trailing)
  }

}

可以通过在参数列表前加上private修饰符来隐藏主构造方法。添加一个工厂方法来从这样一组初始元素来构建队列。

 

2.型变注解:Queue是一个特质,而不是一个类型。Queue不是类型,因为它接收一个类型参数。所以,Queue是一个特质,而Queue[String]是一个类型。Queue也被称作类型构造方法

在类型形参前面加上+表示子类型关系在这个参数上是协变(灵活)的。通过这个字符,我们告诉Scala我们要的效果是,Queue[String]是Queue[AnyRef]的子类型

trait Queue[+T]{...}

除了+,还有-可以作为前缀,表示逆变的子类型关系。如果T是类型S的子类型,则表示Queue[S]是Queue[T]的子类型

trait Queue[-T]{...}

类型参数是协变的、逆变的还是不变的,被称作类型参数的型变。 可以放在类型参数旁边的+和-被称作型变注解

 

3.检查型变注解:为了验证型变注解的正确性,Scala编译器会对类或特质定义中的所有能出现类型参数的点归类为协变的逆变的不变的。所谓的“点”指的是类或特质中任何一个可以用类型参数的地方。

编译器会检查类的类型参数的每一次使用。用+注解的类型参数只能用在协变点,而用-注解的类型参数只能用在逆变点。而没有型变注解的类型参数可以用在任何能出现类型参数的点,因此这也是唯一的一种能用在不变点的类型参数。

简单理解来说:方法的类型参数和值参数位于逆变点上,要么注解-,要么不用注解。方法的返回值位于协变点,要么注解+,要么不用注解。(可能不够正确,提供一个方向,虽然可能是错误的)

 

4.下界:回到Queue类,enqueue方法的参数类型是一个逆变点,而T是协变的。有一个办法可以解决:可以通过多态让enqueue泛化(即给enqueue方法本身一个类型参数)并对其类型参数使用下界:

def enqueue[U>:T](x: U):Queue[U]= new QueueImpl(leading, x :: trailing)

新定义的类型参数U没有型变注解,可以用在任何点上。用“U>:T”这样的语法定义了U的下界为T。这样一来,U必须是T的超类型。现在enqueue的参数类型为U而不是T,方法的返回值是Queue[U]而不是Queue[T]。

举例来说,假定有一个Fruit类和两个子类Apple和Orange。按照Queue类的定义,可以对Queue[Apple]追加一个Orange,其结果是一个Queue[Fruit]。

 

5.逆变:在有的场景下逆变是自然的。输出通道特质:

trait OutputChannel[-T]{
  def write(x:T)
}

这里的OutputChannel被定义为以T逆变。因此,一个AnyRef的输出通道就是一个String的输出通道的子类。虽然看上去有违直觉,实际上是讲得通的。我们能对一个OutputChannel[String]做什么?唯一支持的操作是向它写一个String。同样的操作,一个OutputChannel[AnyRef]也能够完成。因此可以安全地用一个OutputChannel[AnyRef]来替换OutputChannel[String]。与之相对应,在需要OutputChannel[AnyRef]的地方用OutputChannel[String]替换则是不安全的。毕竟,可以向OutputChannel[AnyRef]传递任何对象,而OutputChannel[String]要求所有被写的值都是字符串。

上述推理指向类型系统设计的一个通用原则:如果在任何需要类型U的值的地方,都能用类型T的值替换,那么就可以安全地假定类型T是类型U的子类型。这被称作李氏替换原则

有时候,协变和逆变会同时出现在同一个类型中。一个显著的例子是Scala的函数特质。举例来说,当我们写下函数类型            A => B,Scala会将它展开成Function[A,B]。标准类库中的Function1同时使用了协变和逆变:

trait Function1[ -T1, +R]{
  def apply(v1: T1): R
}

举个例子:

class Food(val f:String)
class Grass(f:String) extends Food(f)

class Cow{
  def eat(food: Grass=>AnyRef): Unit ={}
  def info(x:Food)=x.f
  //传入一个 Food=>String 没有报错
  eat(info)
}

 

6.对象私有数据

private class QueueImpl[T](
    private[this] var leading: List[T],
    private[this] var trailing: List[T]
  ) extends Queue[T] {

你可能会怀疑这段代码是否能通过Scala的类型检查。毕竟队列现在包含了两个协变的参数类型T的可被重新赋值的字段。这不是违背了型变规则吗?的确有这个嫌疑,不过leading和trailing带上了一个private[this]的修饰符,因而是对象私有的,只能从定义它们的对象内部访问。而从定义变量的同一个对象访问这些变量并不会造成型变的问题。直观地理解,如果我们要构建一个型变会引发类型错误的场景,需要引用一个从静态类型上比定义该对象更弱的对象,而对于访问对象私有值的情况,这是不可能出现的。

 

7.上界:语法为:“T <: Ordered[T]”,T为Ordered[T]或子类。适用于需要T含有某个类特定的方法。如需要排序相关的方法,可以传入继承了Ordered[T]的类。

你可能感兴趣的:(scala)