Rethink Scala 之十:Scala里的泛型
很多其他语言都有泛型支持,Scala也不例外,我们今天就从头开始看一下我们为什么需要泛型,以及Scala是如何支持泛型的。
我们为什么需要泛型?
你为项目组实现了一个类,它可也放入一个Int值,并自动记录放入的时间。可以根据时间来得到之前放入的Int值。(不考虑同一时间放入多个值的情况。)
大家觉得你实现的这个类太棒了,但是又想让你实现一个可以存放String类型并同时可以记录时间的类。你想想这也不难,再加一个类:
很好,又满足需求了,这时又有很多人提出他们要Boolean,Float,Double类型的。还有人提出要支持他们自定义的所有类。这时你可能想到这么一种解决办法:
好了,我这个类可以满足你们所有需求!它能放进去所有东西!可是过了一会,你就得到了很多抱怨:
因为他们发现他们之前这样的语句现在不能通过编译了:
项目组为了使用你最新设计的通用代码不得不加上了令人生厌的类型检查语句:
有没有一种既能支持各种类型,又是类型安全(不用做类型转换,类型检查)的方法呢?那就是泛型。它的优点就是你可以编写针对通用类型,但又是类型安全的代码。我们来改一下代码:
val map=scala.collection.mutable.HashMap[Date,T]() // 这里可以直接用类型变量T来定义Map。
def add(e:T) ={
map += (new Date() -> e)
}
def show()=map.foreach( println(_) )
def get(date:Date):T =map(date) //返回值类型也是T
}
val record = new AnyRecord[Int] //我们必须通过具体类型来实例化对象,一旦实例化,这个对象内部所有的T,都会被替换为Int。
record add 3
record add "abc" //这样的语句不能通过编译。
val value:Int = record.get(new Date())//因为T被替换成了Int,所以这里get函数返回的就是Int。
通过定义泛型类,我们成功的解决了类型通用并同时类型安全的问题。
泛型类的局限
因为在我们定义泛型类的时候,并没有具体类型,如果不知道操作的变量是什么类型,那么对它能进行的操作就非常有限了。只能把它当做一个集合成员进行管理,操作基本都是在集合上,而不是元素上。这是因为我们不知道元素是什么类型,所以无法进行操作。除了Any类型里定义的那些操作。
我要修一个澡堂,别人说最少要有两个房间,因为要一个男澡堂,一个女澡堂。我表示理解并开始定义我的类。
val sex:String
}
class Man extends Customer{
override val sex="Man" //重写sex属性为Man
}
class Woman extends Customer{
override val sex="Woman" //重写sex属性为Woman
}
class BathRoom{
val customers = ListBuffer[Customer]()
def in(c:Customer): List={
println("One "+c.sex+" come.") //进来一个人,打印他/她的性别
customers+=c
customers.toList //返回不可变的所有客人的List
}
}
我们定义了一个customer的trait,里边定义了一个属性 sex。对于澡堂来说,区别客人性别是非常重要的。然后我们分别实现了男客户和女客户。他们都混入了Customer这个trait,分别给自己的sex属性赋值。最后我们实现了一个澡堂类BathRoom。它设计的时候是基于Cutomer设计的,这样男澡堂可以用,女澡堂也可以用。
接着我们实例化一个男澡堂,开始营业了。
非常不幸,一个女生进入了男澡堂,编译器竟然没有发现类型错误。编译器说你是用Customer定义的,女客人也是客人啊。当然可以进。可见类型安全我们没有做好。那么这里可以用泛型来帮我们吗?我们来试一下:
我们把BathRoom加上类型参数T,我们都是基于T来构造,可是问题出现了。我们无法调用类型T的sex属性,因为T是个通用类型,sex属性可不是所有类型都有的属性。这时候我们来看一下上界
上界
我们只改动了一处,就是把BathRoom的类型参数从T 改成了T<:Customer.意思是说T只能被Customer以及它的子类来实例化。同时我们在BathRoom的定义里可以访问sex属性了。因为Customer以及它的子类都是有sex属性的。然后我们再来看看上次进错澡堂的问题能解决吗:
利用静态语言的类型安全特性,我们完美解决了问题。
下界
有上界,就要有下界。我们先定义一个新的类
正如你所想,下界就是用符号>:表示的.那我们试着把BathRoom的类型参数改成T>:Man试试
这个报错也合情合理,因为我们这个BathRoom可以用任意类型来实例化,只要它是Boy的父类,包括用AnyRef类型。AnyRef可没有sex属性。我们注释掉出错的这一句。
然后我们看看实例化BathRoom时,对类型参数有什么要求:
目前下界只能防止你用某个类的子类去实例化一个类型参数,好像作用不大,下界和我们将要讲的协变在一起可以有更大的作用。
协变
我们再来定义一个类:Boy
Boy继承自Man,所以下边这个语句是没有问题的,一个男孩就是一个男顾客。
那么BathRoom[Man] 和BathRoom[Boy] 之间有没有关系呢?一个男孩澡堂是不是一个男澡堂呢?
不幸的是,在默认情况下这句话不能通过编译,男孩澡堂和男澡堂是两种不同的类型,并不能赋值。除非,你定义BathRoom的类型参数是协变的。协变的意思就是如果类型A是类型B的父类,那么通过类型A参数化的模板类也是通过类型B参数化的模板类的父类,定义方式就是在模板类的类型参数前加一个"+".那我们试一下
val customers = ListBuffer[T]() //编译错误:covariant type T occurs in invariant position
def in(c:T): List[T] ={ //编译错误:covariant type T occurs in contravariant position in type T of value c
customers+=c
customers.toList
}
}
val boyBathRoom:BathRoom[Man] = new BathRoom[Boy]
理论上,在类型T前边加了一个+,这样类型BathRoom[Man]就应该是BathRoom[Boy]的父类了。但是,实际情况并没有那么简单。编译器马上找出了两个可能的错误。这两个错误都是类似的,如果编译器没有报错,那么就可能出现下边的情况:
上边代码的第二行只是为了你理解,其实如果你声明了你的类是协变的,那么你的类内部就需要能处理任意类型。并返回正确的类型。比如我们声明了一个BathRoom[Boy],它的in(c:T)方法需要能接受Man,Customer,AnyRef,Any。既然支持到了Any,那就是可以放任意值进来。我们的BathRoom类如果要支持协变,有几个问题:
- 首先我们定义的成员属性ListBuffer[T]不具备这种能力,一旦用具体的类型A,比如Man。实例化ListBuffer[T]后,这个list是不能再加入像123这样的数字的。
- 第二我们的in方法在用具体类型A实例化后,不能输入A的父类的参数,以及返回A的父类的列表。
我们对代码进行如下的改动:
通过上边的修改, 我们的BathRoom类可以处理任意类型了。我们试一下
boyBathRoom.in(new Boy)//返回一个List[Boy]
val manBathRoom:BathRoom[Man] = boyBathRoom//因为协变,BathRoom[Boy]是BathRoom[Man]的子类了。
manBathRoom.in(new Boy)//返回一个List[Man]
manBathRoom.in(new Woman)//返回一个List[Customer]
manBathRoom.in(123)//返回一个List[Any]
协变我们是想让BathRoom[Boy]是BathRoom[Man]的子类,从而可以让男孩澡堂赋值给男人澡堂。但是有时候我们可能需要的是逆变。
逆变
比如我们有一个冰箱,它有几个模式,水果模式,冻肉模式,如果调成了水果模式,那么我只放苹果肯定是可以的。逆变就是在模板类的类型前加上一个减号