Scala之旅(10)——高级类型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Stefan_xiepj/article/details/80284499

高级类型

视界(“视图界定”)

有时候,我们并不需要执行一个类型是等[子|超]于另一个类,我们可以通过转换这个类来伪装这种关联关系。一个视界指定一个类型可以被“看做是”另一个类型。这对对象的只读操作时很有用的。

“隐式”函数允许类型自动转换。更确切地说,如果隐式函数有助于满足类型推断时,隐式函数可以按需地应用。举个栗子:


视界(view bound),就像类型边界,要求存在一个能够将某类型转换为指定类型的函数。我们可以使用<%指定类型限制。例如:

下述要求A必须可以“被视作”为Int。


其他类型限制

方法可以通过隐式参数执行更复杂的类型限制。例如,List支持对数字内容执行sum,但对其他内容却不行。可是Scala的数字类型并不都共享一个超类,所以我们不能使用 T <: Number。相反,要使之能工作,Scala的math库对适当的类型T定义了一个隐含的 Numeric[T]。然后在List定义中使用它:

sum[B >: A](implicit num: Numeric[B]):B

如果调用List(1,2).sum(),并不需要传入一个num参数,因为他是隐式设置的。但如果调用List("whoop").sum(),则会抛出异常无法设置num。

在没有设定陌生的对象为Numric的时候,方法可能会要求某种特定类型的“证据”。这是可以使用一下类型-关系运算符:

A =:= B A必须和B相等
A <:<B A必须是B的子类
A<%<B A必须可以被看做是B

(<:<he <%<在Scala2.10+版本中移除了)

使用视图进行泛型编程

在Scala标准库中,视图主要用于实现集合的通用函数。例如“min”函数(在Seq[]上)就使用了这种技术:

def min[B >:A](implicit cmp: Ordering[B]): A = {
    if(isEmpty)
        throw new UnsupportOperationException("empty.min")
    reduceLeft((x,y) => if(cmp.lteq(x,y)) x else y)
}

元素的实际类型A必须是B的子类,B实现了Ordering接口;这里cmp是隐式参数,隐式转换不一定存在,如果不存在,则会在上下文中查找该隐式转换,如果查找不到,则会报错。

这样有什么优势呢?

  • 首先,集合中的元素不必实现Ordered特质,但Ordered的使用任然可以执行静态类型检查。
  • 无需任何额外的库支持,可以自定义自己的排序实现。


在标准库中,有视图专门将Ordered转换为Ordering(反之亦然)。

隐式转换

在scala语言当中,隐式转换是一项强大的程序语言功能,它不仅能够简化程序设计,也能够是程序具有很强的灵活性。在scala语言中,隐式转换是无处不在的,只不过scala语言为我们隐藏了相应的细节。scala类层次结构中,存在大量的隐式转换。


同java一样,Double,Float,Long,Int,Short,Byte之间存在隐式转换,不需要人工进行干预。视图界定可以扩月类层次结构进行,他背后的实现原理就是隐式转换。举个栗子:Int类型在视图界定中会自动转换成RichInt,而RichInt实现了Comparable接口。这里隐式转换是scala语言库自带的。

隐式参数

当我们在定义方法时,可以把最后一个参数列表标记为implicit,表示该组参数是隐式参数。一个方法只会有一个隐式参数列表,置于方法的最后一个参数列表。如果方法有多个隐式参数,只需一个implicit修饰即可。当调用包含隐式参数的方法时,如果当前上下文中有合适的隐式值,则编译器会自动为该组参数填充合适的值。如果没有,编译器会抛出异常。当然,隐式参数我们也可以手动为该参数添加默认值。


隐式类型转换

使用隐式转换将变量转换成预期的类型是编译器最先使用 implicit 的地方。当编译器看到类型 N 而却需要类型 M 时,编译器就在当前作用于中查找是否定义了从类型 N 到类型 M 的隐式定义。举个栗子:


函数隐式调用

隐式调用函数可以转换调用方法的对象,具体来说:

当编译器看到 N.run,但N没有定义run方法(包括基类),那么,编译器就查找作用域内定义的从 N 到其他对象的类型转换,比如 M,如果类型 M 定义了 run方法,编译器就首先使用隐含类型转换把 N 转换为 M,然后调用 M 的 run 方法。

举个栗子:

class Car{
  def  driver(name : String) = println(name + " is driving...")
}
object CarFactory{
  implicit def getCar(p : Person) = new Car
}
class Person
object Person {
  def main(args: Array[String]): Unit = {
    import CarFactory._
    val p = new Person
    p.driver("stefan")
  }
}

这种隐式调用往往让人难以理解,但的确是合法的。

隐式转换规则

隐式转换可以定义在目标文件中,也可以将隐式转换集中放在特定的包中,在使用时直接引入该包即可。这种方式在scala源码中非常常见,scala会默认帮我们引用Predef对象中所有的方法,Predef中定义了很多隐式转换函数,举个栗子:

通过命令

:implicits -v

隐式转换调用时机

什么时候会发生隐式转换呢?一般在以下几种情况下发生隐式转换调用:

  • 当方法中参数的类型与实际类型不一致时;

  • 当调用了类中不存在的方法或成员时,会自动将对象进行隐式转换;

那什么时候不会发生隐式转换呢?

  • 编译器可以不在隐式转换的编译时通过,则不进行隐式转换。

举个栗子:下面不需要自己定义隐式转换


  • 当转换存在二义性时,则不会发生隐式转换。

举个栗子:

下述案例重复定义了隐式转换 AA到BB的转换,则会出现异常。


  • 隐式转换不会嵌套进行。


高阶多态性类型和特设多态性

Scala可以对“高阶”的类型进行抽象。例如,假设你需要用几种类型的容器处理几种类型的数据。你可能定义了一个Container的接口,它可以实现为几种类型的容器: Option,List等。你要定义可以使用这些容器里的值的接口,但不想确定值的类型。

这类似于函数柯里化。例如,尽管“一元类型”有类似List[A]的构造器,这意味着我们必须满足一个“级别”的类型变量来产生一个具体的类型(就像一个柯里化的函数需要值提供一个参数列表来被调用),更高阶的类型需要更多。

举个栗子:

Container是参数化类型的多态(“容器类型”)。


如果我们结合隐式转换 implicits 使用容器,我们会得到“特设的”多态性:即对容器写泛型函数的能力。


F-界多态性

通常很有必要来访问一个(泛型)特质的具体子类。

举个栗子:假如你有一些泛型特质,但需要可以与它的某一子类进行比较。

trait Container extends Ordered[Container]

子类必须实现compare方法

def compare(that: Container): Int

但我们不能访问具体子类型,例如:

class MyContainer extends Container {
    def compare(that: MyContainer): Int
}

上述代码是编译失败的,因为我们对Container指定了Ordered特质,而不是对特定子类型指定的。

一个可选的解决方案是将Container参数化,以便我们能在子类中访问其子类型。

trait Container[A] extends Ordered[A]

现在子类可以这样做:

class MyContainer extends Container[MyContiner]{
    def compare(that: MyContainer): Int
}

这里的问题在于 A 类型没有被任何东西约束,这导致你可能会做类似这样的事情:

class MyContainer extends Container[String] {
    def compare(that: String): Int
}

为了调和这一点,我们改用F-界的多态性。

trait Container[A<: Container[A]] extends Ordered[A]

现在A怎么用呢?A作为Ordered的类型参数,而A本身就是Container[A]

所以现在

class MyContainer extends Container[MyContainer]{
    def compare(that: MyContainer) = 0
}

他们是有序的了:


鉴于他们都是Container[_]的子类型,我们可以定义另一个子类并创建 Container[_]的一个混合列表:


当前的结果类型是由 YourContainer with MyContainer 类型确定的下界。这是类型推断器的工作。有趣的是,这种类型甚至不需要是有意义的,它仅仅对列表的统一类型提供了一个逻辑上最优的下界。如果现在我们尝试使用Ordered会发生什么?


对统一的类型 Ordered[]不存在了。

结构类型

scala支持结构类型(structural types),类型需求有接口结构表示,而不是有具体的类型表示。


这个实现过程使用了反射,使用的时候需要注意性能问题。

抽象类型成员

在特质中,你可以让类型成员保持抽象。


在做依赖注入等情况下,这往往是一个有用的技巧。

你可以使用hash操作符来引用一个抽象类型的变量:


类型擦除和清单

类型信息在编译的时候会因为擦除而丢失。Scala提供了“清单(Manifests)”功能,是我们能够选择性地恢复类型信息。清单作为一个隐式的值被提供,它由编译器根据需要生成。



猜你喜欢

转载自blog.csdn.net/Stefan_xiepj/article/details/80284499