![]()
刚开始学面向对象的时候,记得有一个设计原则叫LSP原则,简单来说就是设有类型P和P的子类S,假如一个方法接受类型P作为参数,那这个方法也应该能接受S。
这个概念不难理解,java和scala都能接受子类。那么当问题扩展到集合的时候会怎样呢?也就是如果函数接受P实例的集合为参数,它是否能接受P的子类S的集合呢?
如果对这个问题的答案是可以,则称为协变。这个术语其实是来自范畴论的。是个跟集合相关的问题。有兴趣的可以去查查数学方面的本源。
我们研究一下java的集合,从5.0开始java就支持了泛型,我们做下实验
Object s = "abc";
List<Object> objects = new java.util.ArrayList<String>();
第一行OK,第二行编译不过,报:ArrayList<String>无法转换为List<Object>,
说明java是非协变的
那么scala里会怎样呢?val s:AnyRef = "abc"
var objects:List[AnyRef] = List[String]("abc","123")
全部编译通过。
那么这是否说明scala语言中集合是默认协变的呢?并非如此!
我们在scala里用java集合类做下实验
val ss:java.lang.Object = new java.lang.String("abc")
var jObjects:java.util.List[Object] =
new java.util.ArrayList[java.lang.String]()
结果是编译不过,而且scala明确给出了提示
<console>:34: error: type mismatch;
found : java.util.ArrayList[java.lang.String]
required: java.util.List[java.lang.Object]
Note: java.lang.String <: java.lang.Object, but Java-defined
trait List is invariant in type E.
明确告知invariant,也就是不协变。
就是说在语言层面上来说scala里是默认非协变的, 查看scala.List的定义可见 class List [+A]
scala提供了更细的语法来控制协变行为,这里是通过[+A]来明确声明List为协变,因此我们的控制粒度细了很多。
协变的可变(mutable)集合其实是不安全的,比如java里历史悠久的数组类型是协变的。
String[] a = new String[] { "abc" };
Object[] b = a;
b[0] = 1;
System.out.println(a[0]);
这些语句能编译通过,但是运行时出错:Exception in thread "main"java.lang.ArrayStoreException: java.lang.Integer at Lsp.main(Lsp.java:24)
既然存在这种不安全问题,那么为什么scala里的List定义为[+A]呢?原因是scala的List是不可变的,对不变集合的修改会产生新的实例而不影响旧实例。我们可以看到scala的可变集合的定义:classMutableList [A]
这个是不可协变的。
除了协变外,scala还可以定义[-A],称为逆变,还有上界和下界,但是在继续前让我设计个稍微有意思点的例子一边帮助思考。
假设有Fruit类和其子类Apple,然后我们有个参数化类型Package[T]可以把东西放进去。现在我们定义一个cut函数,此函数接受Package[Fruit]类型的参数,那么请问它能接受Package[Apple]类型的参数吗?
如果前面看懂了, 那答案很清楚,不能,因为scala是不协变的。看一下代码
class Fruit {
def selfIntro() = "Fruit"
}
class Apple extends Fruit{
override def selfIntro() = "Apple"
}
class Package[T] (val contents:Seq[T]) {
def this() = this(List[T]())
def putIn[T](xs:T*) = {new Package(for(x <- xs) yield x) }
def getAll() = contents
}
def cut(p:Package[Fruit]) {p.getAll foreach (x => Console.println(x.selfIntro + " cutted"))}
val fruitPackage = new Package[Fruit].putIn(new Fruit, new Fruit, new Fruit)
val applePackage = new Package[Apple].putIn(new Apple, new Apple, new Apple)
很这个cut只能切fruitPackage,不能切applePackage,尽管Apple是Fruit的子类,而且编译器会很清楚的告诉你原因是因为不协变,并建议你改用+T
修改成协变却不是那么容易的,由于scala实现上的细节,必须利用“下界”来做声明,见代码
class Package[+T] (val contents:Seq[T]) {
def this() = this(List[T]())
def putIn[U >: T](xs:U*) = {new Package(for(x <- xs) yield x) }
def getAll() = contents
}
使用这个修改过的Package,cut就可以操作applePackage了,注意U >: T表示U为T和T
的超类,这个东东叫做下界。
那么逆变呢?Package[-T]与协变相反,就是只能接受Fruit及其超类而不是子类。逆变在集合上
没有什么实际价值,乍听上去也很难理解,但是在设计库的时候逆变也是一个重要的工具。
仍然基于水果的背景,我们考虑如下场景:如果有RedApple extends Apple extends Fruit
有一把切水果刀,就好象料理机那样,装上水果刀头就能切所有的水果,但是比较粗糙,装上
苹果刀就能切苹果,装好红苹果刀就能把红苹果切成艺术品。一个厨师右手拿水果,左手去拿刀,如果
手里拿的是苹果,显然切苹果的刀和切水果的刀都可以用,而切子类红苹果的刀反而不能用了,对吗?
class Fruit {
def selfIntro() = "Fruit"
}
class Apple extends Fruit{
override def selfIntro() = "Apple"
}
class RedApple extends Apple{
override def selfIntro() = "RedApple"
}
class Cutter[-] {
def cut[U <: T](x:U) = x match {
case (f:Fruit) => Console.println("cut a " ++ f.selfIntro)
case _ => Console.println("don't know what to do")
}
}
class Chef[T] {
def cutIt(c:Cutter[T], p:T) = c.cut(p)
}
///测试结果,切红苹果的刀是不能切苹果的,而且水果的刀是可以的,这正是我们需要的
scala> new Chef[Apple].cutIt(new Cutter[RedApple], new Apple)
<console>:16: error: type mismatch;
found : Cutter[RedApple]
required: Cutter[Apple]
new Chef[Apple].cutIt(new Cutter[RedApple], new Apple)
^
scala> new Chef[Apple].cutIt(new Cutter[Apple], new Apple)
cut a Apple
scala> new Chef[Apple].cutIt(new Cutter[Fruit], new Apple)
cut a Apple
可见这正是我们需要的结果,通过精确的类型定义,我们使得编译器能够检查出用户对库的不正确
使用,提高了库的强壮性。
scala的类型系统很复杂,但是对库设计者来说非常强大,还有很多需要学的东西,写这篇博客花了不少时间,
希望对大家有用。
标签 : haskell, scala, 函数式编程, 编程, 软件开发
本文转自大魔头博客: http://www.kaopua.com/blog/2012/01/02/1325438040000.html
http://blog.csdn.net/lord_is_layuping/article/details/7797976