您的位置:首页 > 其它

scala中的协变和逆变

2012-07-29 00:00 411 查看

刚开始学面向对象的时候,记得有一个设计原则叫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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: