scala 泛型之初解,定界,类型约束,逆变与协变

scala 的泛型应用如下:

一、初解

当构建一个类或者函数时,如果我们不知道(或者说不确定)传入的参数的具体数据类型,这时候可以泛型,例子如下:

1-1例子:

object test0 extends App{
   val str = "123"
   val intv =123
   val strTest = new Test[String](str)
   val intTest = new Test[Int](intv)
   strTest.check;
   intTest.check;
}
class Test[T](val v:T){
  def check = {
    if(v.isInstanceOf[String]){
      println("the param is String");
    }
    if(v.isInstanceOf[Int]){
      println("the param is Int");
    }
  }
}

1-2结果:

the param is String
the param is Int
结论:

很多时候我们都需要用到泛型,在web中最常见的就是在用java构建DAO层时,我们往往不知道传入的类型是什么,但是很多时候的操作都是一样的,如增删改查。用泛型+反射的模式可以写一个基于上述操作的通用的模板让所有的需要访问数据库的service层类去调用


二、界定(Bonds)
2-1例子:

class Pair[T](val first:T,val second:T){
  def smaller = if(first.compareTo(second) < 0) first else second //编译器报错,因为编译器不能确定T具有compareTo的方法
}
object test1 extends App{
  val a="3"
  val b = "2"
  val pair = new Pair(a,b);
  print(pair.smaller);
}
所以改进如下:

class Pair[T<:Compareable[T]](val first:T,val second:T){
  def smaller = if(first.compareTo(second) < 0) first else second 
}

修改后就可以运行了。原因是参数的界定是用来限制传入的参数的类型,代表该类型具有这样的一个特性。


2-2例子:

object test1 extends App{
  val a=3 // 改成整型
  val b =2 // 改成整型
  val pair = new Pair(a,b); // 这里将会报错,因为整型不是Compareable的子类,不具有compareto方法
  print(pair.smaller);
}
这时候把Pair的界定改成浏览界定<%,这个界定符可以隐式地转换类型

class Pair[T<%Comparable[T]](val first:T,val second:T){
  def smaller = if(first.compareTo(second) < 0) first else second
}


2-3例子:

class Pair[T : Ordering](val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]) =
if (ord.compare(first, second) < 0) first else second
}
上下文界定符 “:”意味着可以隐式地转换成Ordering[T]


三、类型约束(type contraints)
类型约束也可以给你用来限制类型,方式有三:
T =:= U
T <:< U
T <%< U
These constraints test whether T equals U, is a subtype of U, or is view-convertible to U.

3-1例子:

object test3 extends App{
  val first = 123
  val second =346
  val p = new Pair1[Int](first,second) //这里未调用smaller,不报错
  p.smaller;// 这里会报错,因为first,second被检测到不符合
}
class Pair1[T](val first: T, val second: T){
  // 下面的方法调用了类型约束,当被调用时才会检测约束
  def smaller(implicit ev: T <:< Comparable[T]) =
      if (first.compareTo( second) < 0) first else second
}

四、协变与逆变

这里主要参考文章:http://hongjiang.info/scala-covariance-and-contravariance/ 

原文如下:

对于一个带类型参数的类型,比如 List[T],如果对A及其子类型B,满足 List[B]也符合 List[A]的子类型,那么就称为covariance(协变),如果 List[A]是 List[B]的子类型,即与原来的父子关系正相反,则称为contravariance(逆变)

协变:

 _____               _____________ 
|     |             |             |
|  A  |             |  List[ A ]  |
|_____|             |_____________|
   ^                       ^ 
   |                       | 
 _____               _____________ 
|     |             |             |
|  B  |             |  List[ B ]  |
|_____|             |_____________|  

逆变:

 _____               _____________ 
|     |             |             |
|  A  |             |  List[ B ]  |
|_____|             |_____________|
   ^                       ^ 
   |                       | 
 _____               _____________ 
|     |             |             |
|  B  |             |  List[ A ]  |
|_____|             |_____________|  

如果一个类型支持协变或逆变,则称这个类型为variance(翻译为可变的或变型),否则称为invariant(不可变的)

在Java里,泛型类型都是invariant,比如 List<String> 并不是 List<Object> 的子类型。Java并不支持声明点变型(declaration-site variance,即在定义一个类型时声明它为可变型,也称definition-site),而scala支持,可以在定义类型时声明(用加号表示为协变,减号表示逆变),如:

trait List[+T] // 在类型定义时(declaration-site)声明为协变 

这样会把List[String]作为List[Any]的子类型。

不过Java支持使用点变型(use-site variance),所谓“使用点“,也就是在声明变量时:

List<? extends Object> list = new ArrayList<String>();

scala为了兼容java泛型通配符的形式,引入存在类型(existential type,后边再讲)时,也支持了使用点变型(use-site variance)

scala> val a : List[_ <: Any] = List[String]("A")
a: List[_] = List(A)    

要注意variance并不会被继承,父类声明为variance,子类如果想要保持,仍需要声明:

scala> trait A[+T]

scala> class C[T] extends A[T]  // C是invariant的

scala> class X; class Y extends X;

scala> val t:C[X] = new C[Y]
<console>:11: error: type mismatch; 
 found   : C[Y]
 required: C[X]
Note: Y <: X, but class C is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

必须也对C声明为协变的才行:

scala> class C[+T] extends A[T]

scala> val t:C[X] = new C[Y]
t: C[X] = C@6a079142

-----------------------------新增-----------------------------

扩展---实际应用中理解逆变和协变(-)

例子:

在scala中内置一个特征函数:

  1. trait Function1[-S, +T] {  
  2.   def apply(x: S): T  
  3. }  

这种函数接受一个参数,参数类型为泛型类型T,返回类型为泛型类型U。和其他支持泛型的语言一样,实际定义函数时T和U的类型会被确定下来,不过需要注意的是,这边的T之前有一个“-”,而U之前有一个“+”。

在这里引入关于这个符号的说明,在声明Scala的泛型类型时,“+”表示协变,而“-”表示逆变。

  • C[+T]:如果A是B的子类,那么C[A]是C[B]的子类。
  • C[-T]:如果A是B的子类,那么C[B]是C[A]的子类。
  • C[T]:无论A和B是什么关系,C[A]和C[B]没有从属关系。

根据Liskov替换原则,如果A是B的子类,那么能适用于B的所有操作,都适用于A。让我们看看这边Function1的定义,是否满足这样的条件。假设Bird是Animal的子类,那么看看下面两个函数之间是什么关系:

def f1(x: Bird): Animal // instance of Function1[Bird, Animal]
def f2(x: Animal): Bird // instance of Function1[Animal, Bird]

在这里f2的类型是f1的类型的子类。为什么?

我们先看一下参数类型,根据Liskov替换原则,f1能够接受的参数,f2也能接受。在这里f1接受的Bird类型,f2显然可以接受,因为Bird对象可以被当做其父类Animal的对象来使用。

再看返回类型,f1的返回值可以被当做Animal的实例使用,f2的返回值可以被当做Bird的实例使用,当然也可以被当做Animal的实例使用。

所以我们说,函数的参数类型是逆变的,而函数的返回类型是协变的。

(注意上面的f1,f2均是被当做变量传值时的状态)

如果上面的例子还不能明白,请看下面完整例子:


扩展---实际应用中理解逆变和协变(二)

<span style="font-family:SimSun;font-size:18px;">package fineqtbull.customer
//出版物类
class Publication(val title: String)
//书籍类
class Book(title: String) extends Publication(title)
//图书库类
object Library {
    //定义图书库内所有的书籍
    val books: Set[Book] =
        Set(
            new Book("Programming in Scala"),
            new Book("Walden")
        )
    //打印所有图书内容,使用外部传入的函数来实现
    def printBookList(info: Book => AnyRef) {
        //确认Scala中一个参数的函数实际上是Function1特征的实例
        assert(info.isInstanceOf[Function1[_, _]])
        //打印
        for (book <- books)
            println(info(book))
    }
    //打印所有图书内容,使用外部传入的GetInfoAction特征的实例来实现
    def printBokkListByTrait[P >: Book, R <: AnyRef](
            action : GetInfoAction[P, R]) {
        //打印
        for (book <- books)
            println(action(book))
    }

}
//取得图书内容特征,P类型参数的类型下界是Book,R类型参数的类型上界是AnyRef
trait GetInfoAction[P >: Book, R <: AnyRef] {
    //取得图书内容的文本描述,对应()操作符
   def apply(book : P) : R
}
//单例对象,文件的主程序
object Customer extends Application {
    //定义取得出版物标题的函数
    def getTitle(p: Publication): String = p.title
    //使用函数来打印
    Library.printBookList(getTitle)

    //使用特征GetInfoAction的实例来打印
    Library.printBokkListByTrait(new GetInfoAction[Publication, String] {
            def apply(p: Publication) : String = p.title })
}
</span>

printBookList的info参数是Function1类型,而 Function1的-S类型参数是逆变,+T参数是协变。printBookList方法的assert(info.isInstanceOf[Function1[_, _]])语句可以验证这一点。从printBookList方法的定义可以知道,info的S类型参数是Book,T类型参数是AnyRef。然而主函数中使用处则是Library.printBookList(getTitle),getTitle函数中对应的S是Publication,T是String。为什么可以与printBookList原来的定义不一致呢,这就是协变和逆变的威力了。由于-S是逆变,而Publication是Book的父类,所以Publication可以代替(泛化为)Book。由于+T是协变,而String是AnyRef的子类,所以String可以代替(泛化为)AnyRef。如此一来,主程序的语句也就完全正确了。

  为什么要遵循Function1这类的特性呢?你想想getTitle是真正实现函数体的变量,所以getTitle接受的参数p必须是printBookList参数中的info的父类。很多学java的程序猿分不清楚逆变的作用,就是没有搞懂在scala中函数可以作为参数传入方法以及哪个def才是实现内容主体的函数并被作为实参。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值