zoukankan      html  css  js  c++  java
  • 13.Scala-类型参数

    第13章 类型参数

    13.1 泛型类

    类和特质都可以带类型参数,用方括号来定义类型参数,可以用类型参
    数来定义变量、方法参数和返回值。带有一个或多个类型参数的类是泛型的。如下 p1,
    如果实例化时没有指定泛型类型,则 Scala 会自动根据构造参数的类型自动推断泛型的具体类型。
    class Pair[T, S](val first: T, val second: S) {
      override def toString = "(" + first + "," + second + ")"
    }

    //从构造参数推断类型 val p = new Pair(42, "String")
    //设置类型 val p2 = new Pair[Any, Any](42, "String")

    13.2 泛型函数

    函数和方法也可以有类型(泛型)参数:
    def getMiddle[T](a: Array[T]) = a(a.length / 2)
    
    // 从参数类型来推断类型
    println(getMiddle(Array("Mary", "had", "a", "little", "lamb")).getClass.getTypeName)
    
    // 指定类型,并保存为具体的函数。
    val f = getMiddle[String] _
    println(f(Array("Mary", "had", "a", "little", "lamb")))

    13.3 类型变量限

    在 Java 泛型里表示某个类型是 Test 类型的子类型,使用 extends 关键
    字:
     <T extends Test>
    //或用通配符的形式:
     <? extends Test>
    这种形式也叫为泛型的 上限上界,同样的意思在 scala 的写法为:
    [T <: Test]
    需要用<:来表示,叫做上界。上界:类型参数应该是某某的子类型。
    //或用通配符:
    [ _ <: Test ]
    class Pair1[T <: Comparable[T]](val first: T, val second: T) {
      def smaller = if (first.compareTo(second) < 0) first else second
    }
    val p3 = new Pair1("Fred", "Brooks")
    println(p3.smaller)
     
    在 Java 泛型里表示某个类型是 Test 类型的父类型,使用 super 关键字:
     <T super Test>
    //或用通配符的形式:
     <? super Test>
     
    这种形式也叫下限或下界,同样的意思在 scala 的写法为:
    [T >: Test]
    //或用通配符:
    [_ >: Test]
    class Pair2[T](val first: T, val second: T) {
      def replaceFirst[R >: T](newFirst: R) = new Pair2[R](newFirst, second)
      override def toString = "(" + first + "," + second + ")"
    }
    Java 里,T 同时是 A 和 B 的子类型,称为 multiple bounds
     
    Scala 里对上界和下界不能有多个,不过变通的做法是使用复合类型
    (compund type):
    [T <: A with B]
     
    而对于下界,在 java 里则不支持 multiple bounds 的形式:
    //java 不支持
     
    Scala 里对复合类型同样可以使用下界
    [T >: A with B]
    因为 Scala 里对于 A with B 相当于 (A with B),仍看成一个类型,

    13.4 视图界定

    还来看 Comparable 子类的问题,如果输入如 Pair(4,2)则无法比较,
    Scala 的 Int 类型没有实现 Comparable 接口,而 RichInt 确实现了,在这里我
    们需要一个隐式转换,让 Int 可以转换为 RichInt。
    我们可以通过视图界定来解决,用 <% 来表示。 T 可以被隐式转换成
    Comparable[T]
    [T <% Comparable[T]]
    class PairSec04[T <% Comparable[T]](val first: T, val second: T) {
      def smaller = if (first.compareTo(second) < 0) first else second
      override def toString = "(" + first + "," + second + ")"
    }
    
    val p4 = new PairSec04(4, 2) // Works
    println(p4.smaller)

    13.5 上下文界定

    视图界定 T <% V 要求必须存在一个从 T 到 V 的隐式转换。上下文界定
    的形式为 T:M,其中 M 是另一个泛型类,它要求必须存在一个类型为 M[T]的
    隐式值。
    class Pair3[T : Ordering](val first: T, val second: T) {
      def smaller(implicit ord: Ordering[T]) ={
        println(ord)
        if (ord.compare(first, second) < 0) first else second
      }
      override def toString = "(" + first + "," + second + ")"
    }
     
    上述类定义要求必须存在一个类型为 Ordering[T]的隐式值,当你使用了
    一个使用了隐式值得方法时,传入该隐式参数。

    13.6 Manifest 上下文界定

    Manifest 是 scala2.8 引入的一个特质,用于编译器在运行时也能获取泛
    型类型的信息。在 JVM 上,泛型参数类型 T 在运行时是被“擦拭”掉的,编译
    器把 T 当作 Object 来对待,所以 T 的具体信息是无法得到的;为了使得在运
    行时得到 T 的信息,scala 需要额外通过 Manifest 来存储 T 的信息,并作为参
    数用在方法的运行时上下文。
     
      def test[T] (x:T, m:Manifest[T]) { ... }
      有了 Manifest[T]这个记录 T 类型信息的参数 m,在运行时就可以根据 m
    来更准确的判断 T 了。但如果每个方法都这么写,让方法的调用者要额外传
    入 m 参数,非常不友好,且对方法的设计是一道伤疤。好在 scala 中有隐式转
    换、隐式参数的功能,在这个地方可以用隐式参数来减轻调用者的麻烦。
    def foo[T](x: List[T])(implicit m: Manifest[T]) = {
      println(m)
      if (m <:< manifest[String])
        println("Hey, this list is full of strings")
      else
        println("Non-stringy list")
    }
    foo(List("one", "two")) // Hey, this list is full of strings
    foo(List(1, 2)) // Non-stringy list
    foo(List("one", 2)) // Non-stringy list
     
     
      隐式参数 m 是由编译器根据上下文自动传入的,比如上面是编译器根
    据 "one","two" 推断出 T 的类型是 String,从而隐式的传入了一个
    Manifest[String]类型的对象参数,使得运行时可以根据这个参数做更多的事
    情。
      不过上面的 foo 方法定义使用隐式参数的方式,仍显得啰嗦,于是 scala
    里又引入了“上下文绑定”,
      def foo[T](x: List[T]) (implicit m: Manifest[T])
      可以简化为:
      def foo[T:Manifest] (x: List[T])
      在引入 Manifest 的时候,还引入了一个更弱一点的 ClassManifest,所谓
    的弱是指类型信息不如 Manifest 那么完整,主要针对高阶类型的情况
    scala 在 2.10 里却用 TypeTag 替代了 Manifest,用 ClassTag 替代了
    ClassManifest,原因是在路径依赖类型中,Manifest 存在问题:
    scala> class Foo{class Bar}
    defined class Foo
    
    scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
    m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]
    
    scala> val f1 = new Foo;val b1 = new f1.Bar
    f1: Foo = Foo@67d18ed7
    b1: f1.Bar = Foo$Bar@2c78d320
    
    scala> val f2 = new Foo;val b2 = new f2.Bar
    f2: Foo = Foo@4b41dd5c
    b2: f2.Bar = Foo$Bar@3b96c42e
    
    scala> val ev1 = m(f1)(b1)
    ev1: Manifest[f1.Bar] = Foo@67d18ed7.type#Foo$Bar
    
    scala> val ev2 = m(f2)(b2)
    ev2: Manifest[f2.Bar] = Foo@4b41dd5c.type#Foo$Bar
    
    scala> ev1 == ev2 // they should be different, thus the result is wrong
    res0: Boolean = true

    了解之后,我们总结一下,TypeTag到底有啥用呢?看下面的例子: 
    请留意: 
    =:=,意思为:type equality 
    <:< ,意思为:subtype relation 
    类型判断不要用 == 或 !=

    class Animal{}
    class Dog extends Animal{}
    
    object MainFoo extends App{
      override def main(args: Array[String]): Unit = {
        val list1 = List(1, 2, 3)
        val list2 = List("1", "2", "3")
        val list3 = List("1", "2", 3)
    
        def test1(x: List[Any]) = {
          x match {
            case list: List[Int] => "Int list"
            case list: List[String] => "String list"
            case list: List[Any] => "Any list"
          }
        }
        println(test1(list1)) //Int list
        println(test1(list2)) //Int list
        println(test1(list3)) //Int list
    
        import scala.reflect.runtime.universe._
        def test2[A : TypeTag](x: List[A]) = typeOf[A] match {
          case t if t =:= typeOf[String] => "String List"
          case t if t <:< typeOf[Animal] => "Dog List"
          case t if t =:= typeOf[Int] => "Int List"
        }
    
        println(test2(List("string"))) //String List
        println(test2(List(new Dog))) //Dog List
        println(test2(List(1, 2))) //Int List
      }
    }

    13.7 多重界定

    不能同时有多个上界或下界,变通的方式是使用复合类型
    T <: A with B
    T >: A with B
    可以同时有上界和下界,如
    T >: A <: B
    这种情况下界必须写在前边,上界写在后边,位置不能反。同时A要符合B的子类型,A与B不能是两个无关的类型。
    可以同时有多个view bounds
    T <% A <% B
    这种情况要求必须同时存在 T=>A的隐式转换,和T=>B的隐式转换。

    class A{}
    class B{}
    implicit def string2A(s:String) = new A
    implicit def string2B(s:String) = new B
    def foo2[ T <% A <% B](x:T)  = println("foo2 OK")
    foo2("test")

     可以同时有多个上下文界定 
    T : A : B 
    这种情况要求必须同时存在C[T]类型的隐式值,和D[T]类型的隐式值。

    class C[T];
    class D[T];
    implicit val c = new C[Int]
    implicit val d = new D[Int]
    def foo3[ T : C : D ](i:T) = println("foo3 OK")
    foo3(2)

    13.8 类型约束

     类型约束,提供了限定类型的另一种方式,一共有3中关系声明: 
    T =:= U意思为:T类型是否等于U类型 
    T <:< U意思为:T类型是否为U或U的子类型 
    T <%< U 意思为:T 类型是否被隐式(视图)转化为 U

    如果想使用上面的约束,需要添加“隐式类型证明参数” 比如:

    class Pair5[T] (val first: T, val second: T)(implicit ev: T <:< Comparable[T]){}

    使用举例:

    import java.io.File
    
    class Pair6[T](val first: T, val second: T) {
      def smaller(implicit ev: T <:< Ordered[T]) = {
        if(first < second) first else second
      }
    }
    
    object Main6 extends App{
      override def main(args: Array[String]): Unit = {
        //构造Pair6[File]时,注意此时是不会报错的
        val p6 = new Pair6[File](new File(""), new File(""))
        //这就报错了
        p6.smaller
      }
    }

    13.9 型变

    术语:

    英文 中文 示例
    Variance 型变 Function[-T, +R]
    Nonvariant 不变 Array[A]
    Covariant 协变 Supplier[+A]
    Contravariant 逆变 Consumer[-A]
    Immutable 不可变 String
    Mutable 可变  StringBuilder

     

    其中,Mutable常常意味着Nonvariant,但是Noncovariant与Mutable分别表示两个不同的范畴。


    即:可变的,一般意味着“不可型变”,但是“不可协变”和可变的,分别表示两个不同范畴。


    型变(Variance)拥有三种基本形态:协变(Covariant), 逆变(Contravariant), 不变(Nonconviant),可以形式化地描述为:


    一般地,假设类型C[T]持有类型参数T;给定两个类型A和B,如果满足A <: B,则C[A]与 C[B]之间存在三种关系:

    如果C[A] <: C[B],那么C是协变的(Covariant);
    如果C[A] :> C[B],那么C是逆变的(Contravariant);
    否则,C是不变的(Nonvariant)。

     Scala的类型参数使用+标识“协变”,-标识“逆变”,而不带任何标识的表示“不变”(Nonvariable):

    trait C[+A]   // C is covariant
    trait C[-A] // C is contravariant
    trait C[A] // C is nonvariant

    如何判断一个类型是否有型变能力:

    一般地,“不可变的”(Immutable)类型意味着“型变”(Variant),而“可变的”(Mutable)意味着“不变”(Nonvariant)。
    其中,对于不可变的(Immutable)类型C[T]
    如果它是一个生产者,其类型参数应该是协变的,即C[+T];
    如果它是一个消费者,其类型参数应该是逆变的,即C[-T]。

    13.10 协变和逆变点

    先给一个示例,定义一个类 A,其类型参数是协变的: 
    class A[+T] {
      def func(x: T) {}
    } 
    上面的代码通不过编译,报错如下:
    covariant type T occurs in contravariant position in type T of value x
    要解释这个问题,需要理解协变点和逆变点的概念。我们可以考虑这样
    一种情况来解释程序为什么报错,既然 A 的类型参数 T 是协变的,那么
    A[AnyRef]是 A[String]的父类,A[AnyRef]对应的 func 为 func(AnyRef),
    A[String]对应的 func 为 func(String),我们定义 father 是一个 A[AnyRef]实
    例,child 是 A[String]实例。当我定义了另一个函数 g,g 的参数为
    A[AnyRef],因此 g(father)当然是没有问题的,又因为 child 是 father 的子类,
    因此按理来说 g(child)也是没有问题的,但是 father 的 func 可以接受 AnyRef
    类型的参数,而 child 的 func 只能接受 String 类型的参数。因此,如果编译
    器允许用 child 替换 father,那么替换后 g 中的参数调用 func 就只能传入
    String 类型的参数了,相当于 g 的处理范围缩小了。所以编译器不允许这种情
    况,因此会报错。反过来一想,如果传入的是 father 的父类,那么 g 的处理
    范围就变大了,所有适用于 father 的情况都适用于 father 的父类,因此,如果
    把 A 的类型参数 T 声明为逆变的,就不会有问题了。 
     
    class A[-T] {
      def func(x: T) {}
    } 
    总结:传入 A 的类型参数会作为 A 中方法的参数的类型(如果有参数的
    话),我们知道一个方法中如果有类型为 X 的参数,那么这个方法可以接受类
    型为 X 的子类的参数。同理上面的情况,func 原来可以接受类型为 AnyRef
    及 AnyRef 的子类作为参数,但是如果一协变,那么 func 就只能接受类型为
    String 及 String 的子类作为参数,作用范围减小了。 
     
     
      因此方法的参数的位置被称为逆变点,A 中的类型参数声明为协变,因
    此编译时出错,声明为协变则没有问题。
    而方法返回值的位置被称为协变点,同样考虑上面的情况: 
     
    class A[+T] {
      def func(): T = {
        null.asInstanceOf[T]
      }
    } 
    也是考虑 father:A[AnyRef]和 child:A[String],当用 child 替换 father 后,
    child 的 func 方法会返回 String 类型的对象来替换 AnyRef,因此是合理的。
     
  • 相关阅读:
    关于软件开发代码的纯洁问题
    乱七八糟
    苹果屏幕变化问题
    注意
    eclipse juno创建maven web项目目录生成方法
    Verilog HDL建模(四)
    Verilog HDL建模(三)
    Verilog HDL建模(二)
    Verilog HDL建模(五)
    Verilog HDL的建模学习(一)
  • 原文地址:https://www.cnblogs.com/LXL616/p/11133578.html
Copyright © 2011-2022 走看看