Bootstrap

4. scala高阶之隐式转换与泛型

背景

上一节,我介绍了scala中的面向对象相关概念,还有一个特色功能:模式匹配。本文,我会介绍另外一个特别强大的功能隐式转换,并在最后介绍scala中泛型的使用

1. 隐式转换

Scala提供的隐式转换和隐式参数功能,是非常有特色的功能,是Java等编程语言所没有的功能。它可以允许手动指定将某种类型的对象转换成其他类型的对象。

Scala的隐式转换,其实最核心的就是定义隐式转换函数,即implicit conversion function。定义的隐式转换函数,只要在编写的程序内引入,就会被Scala自动使用。Scala会根据隐式转换函数的签名,在程序中使用到隐式转换函数接收的参数类型定义的对象时,会自动将其传入隐式转换函数,转换为另外一种类型的对象并返回。这就是“隐式转换”

隐式转换函数叫什么名字是无所谓的,因为通常不会由用户手动调用,而是由Scala进行调用。但是如果要使用隐式转换,则需要对隐式转换函数进行导入。因此通常建议将隐式转换函数的名称命名为“one2one的形式。

Spark源码中有大量的隐式转换和隐式参数。学好隐式转换会对读懂spark源码有很大的帮助。

1.1. 隐式转换的概念

隐式转换是将类型A转换成类型B,但并不是A真的就成了B,而是A本来的属性仍存在的同时又拥有了B的属性,这使得A本身不发生变化的同时扩大了功能,此属于蒙面设计模式。又因为A直接使用了B的功能而不需要对A进行修改,因此此转换是隐式的,使用implicit修饰。简言之,隐式转换就是增强类型、扩展功能。

1.2. 隐式转换的适用情况

隐式转换主要适用于以下两种情况:

  1. 如果表达式e是类型S,并且S不符合表达式的期望类型T。
  2. 在具有类型S的e的e.m表达中,如果m不表示S的成员。

1.3. 隐式转换的原理

当编译器第一次编译失败的时候,会在当前的环境中查找能让代码编译通过的方法,用于将类型进行转换,实现二次编译。当想调用对象功能时,如果编译错误,那么编译器会尝试在当前作用域范围内查找能调用对应功能的转换规则,这个调用过程是由编译器完成的,所以称之为隐式转换,或称之为自动转换。

1.4 隐式转换的作用域

Scala编译器仅考虑作用域之内的隐式转换。要使用某种隐式操作,必须以单一标识符的形式(一种情况例外)将其带入作用域之内。例如:

object TestImplicit {
  implicit def doubleToInt(x: Double) = x.toInt
}

object Test {
  def main(args: Array[String]): Unit = {
    // 以单一标识符引进doubleToInt的隐式转换
    import TestImplicit._
    val i: Int = 2.3
  }
}

单一标识符有一个例外,编译器还将在源类型和目标类型的伴生对象中寻找隐式定义。

1.5. 隐式转换的规则

  1. 显示定义规则:在使用带有隐式参数的函数时,如果没有明确指定与参数类型匹配相同的隐式值,编译器不会通过额外的隐式转换来确定函数的要求。
  2. 作用域规则:不管是隐式值、隐式对象、隐式类还是隐式转换函数,都必须在当前的作用域使用才能起作用。
  3. 无歧义规则:不能存在多个隐式转换使代码合法。例如,代码中不应该存在两个隐式转换函数能够同时使某一类型转换为另一类型,也不应该存在相同的两个隐式值、主构造函数参数类型以及成员方法等同的两个隐式类。
  4. 一次性转换规则:隐式转换从源类型到目标类型只会经过一次转换,不会经过多次隐式转换达到。

1.6. 常见的隐式转换类型

1.6.1. 隐式转换函数

隐式转换函数的格式通常为:

implicit def 函数名(参数): 目标类型 = {
  // 函数体
  // 返回值
}

例如:

package com.wanlong.next
//引入隐式转换函数

import com.wanlong.next.iTestImplicitConversionFunction._

object iTestImplicitConversionFunction2 {

def main(args: Array[String]): Unit = {
  val jack = new Man("jack")
  jack.fly
}

/**
 * 隐式转换强大之处就是可以在不知不觉中加强现有类型的功能。也就是说,可以为某个类定义-个加强版的类,并定义互相之间的隐式转换,
 * 从而让源类在使用加强版的方法时,由Scala自动进行隐式转换为加强类,然后再调用该方法。
 */
implicit def man2SuperMan(man: Man): SuperMan = {
  new SuperMan(man.name)
}
}

class Man(val name: String)
class SuperMan(val name: String) {
def fly = println("SuperMan:" + name + " is flying")
}
//SuperMan:jack is flying

1.6.2. 隐式类

Scala 2.10后提供了隐式类,可以使用implicit声明类。隐式类的主构造函数只能有一个参数,且这个参数的类型就是将要被转换的目标类型。允许开发者在不修改现有类的原始定义的情况下,为该类添加新的功能。在集合中隐式类会发挥重要的作用。

隐式类的格式通常为:

implicit class 类名(参数){
// 类体,可以定义新的方法
}

需要注意的是,隐式类必须定义在另一个类、对象或者包对象(package object)内部,并且其构造函数只能有一个非隐式参数。此外,隐式类的命名虽然没有严格的语法规定,但通常会采用“Rich”前缀加上原始类型名的方式(如RichInt、RichString等)来命名,以清晰地表明这个类是用于扩展某个原始类型的。

object Helpers {
implicit class MyRichInt(arg: Int) {
 def myMax(i: Int): Int = if (arg < i) i else arg
}
}

// 使用隐式类
import Helpers._
println(1.myMax(3))  // 输出: 3

在这个例子中,我们定义了一个隐式类MyRichInt,并为Int类型添加了一个myMax方法。当我们调用1.myMax(3)时,Scala编译器会自动将1转换为MyRichInt类型,并调用myMax方法

1.6.2.2 限制与注意事项
  1. 定义位置:隐式类必须定义在另一个类、对象或包对象内部。即隐式类不能是顶级的。
  2. 构造函数参数:隐式类的构造函数只能有一个非隐式参数。这是因为隐式转换是将一种类型转换为另外一种类型,源类型与目标类型是一一对应的。
  3. 命名规范:虽然命名没有严格规定,但建议使用“Rich”前缀加上原始类型名的方式来命名隐式类。
  4. 避免滥用:过度使用隐式类和隐式转换可能会导致代码的可读性变差。因为隐式转换是在编译器自动进行的,对于阅读代码的人来说,可能不容易发现代码中实际发生的转换。
  5. 与其他隐式机制的冲突:在Scala中,还有其他隐式机制(如隐式参数、隐式转换函数等),使用时需要注意避免冲突。
  6. 隐式类的运作方式是:隐式类将包裹目标类型,隐式类的所有方法都会自动“附加”到目标类型上。

1.7 注意事项

  1. 隐式转换函数的函数名可以是任意的,与函数名称无关,只与函数签名(函数参数和返回值类型)有关。即隐式函数的入参要是编译不通过的类型,返回值要是能正确编译的类型。
  2. 如果当前作用域中存在函数签名相同但函数名称不同的两个隐式转换函数,则在进行隐式转换时会报错。
  3. 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。
  4. 隐式类不能是case class。
  5. Scala的隐式转换是一种灵活且强大的功能,但使用时需要谨慎,以避免引入难以调试的隐式行为。 通常建议,仅仅在需要进行隐式转换的地方,比如某个函数或者方法内,用immport导入隐式转换函数,这样可以缩小隐式转换函数的作用域,避免不需要的隐式转换。

2. 泛型

Scala的泛型是一种强大的特性,它允许在定义类、特质(Traits)或方法时使用类型参数。这种机制使得代码更加通用和灵活,可以适应不同的数据类型而无需重复编写代码。以下是对Scala泛型的详细介绍:

2.1 泛型的基本语法

在Scala中,使用方括号[]来定义类型参数。例如,定义一个名为Box的泛型类,它有一个类型参数T

class Box[T](val item: T) {
  def getValue(): T = item
}

在这里,T可以是任何合法的Scala类型。在创建Box类的实例时,可以指定具体的类型参数,如Box[Int]Box[String]

2.2 泛型的应用场景

  1. 泛型类:如上所述的Box类就是一个泛型类的例子。泛型类允许在类的定义中引入类型参数,从而在创建对象时指定具体的类型。
  2. 泛型方法:泛型方法允许方法的参数类型或返回类型根据调用时的实际情况来确定。例如,定义一个泛型方法printList,它可以接受任何类型的列表并打印出列表中的元素:
def printList[T](list: List[T]): Unit = {
  list.foreach(println)
}

在调用printList方法时,可以传入不同类型的列表,如List[Int]List[String]

  1. 泛型特质:泛型特质允许在特质的定义中引入类型参数。这样,在定义特质的子类或子单例对象时,可以指定具体的类型参数。例如,定义一个泛型特质Logger,它有一个变量a和一个show方法,它们都使用Logger特质的泛型:
trait Logger[T] {
  val a: T
  def show(b: T): Unit = println(b)
}

然后,可以定义一个单例对象ConsoleLogger,它继承Logger特质并指定具体的类型参数为String

object ConsoleLogger extends Logger[String] {
  override val a: String = "张三"
}

2.3 泛型的上下界

在使用泛型时,有时需要限制泛型参数的类型范围。这时,可以使用泛型的上下界。

  1. 上界:使用T <: 类型名表示给类型添加一个上界,即泛型参数T必须是该类型或其子类。例如,定义一个泛型方法demo,它接受一个Array参数,并限定该Array的元素类型只能是Person或其子类:
class Person
class Student extends Person

def demo[T <: Person](arr: Array[T]): Unit = println(arr)
  1. 下界:使用T >: 类型名表示给类型添加一个下界,即泛型参数T必须是该类型或其父类。例如,定义一个泛型类Shelter,它接受一个类型参数T,并限定T必须是Dog或其父类:
class Animal
class Dog extends Animal

class Shelter[T >: Dog](val animal: T)

如果泛型既有上界又有下界,下界应写在前面,上界写在后面,即[T >: 类型1 <: 类型2]

2.4 协变、逆变与非变

在Scala中,泛型参数还可以声明为协变(Covariance)、逆变(Contravariance)或非变(Invariance)。

  1. 非变:默认情况下,泛型类是非变的。这意味着,如果BA的子类型,那么Pair[A]Pair[B]之间没有任何从属关系。
  2. 协变:如果类型B是类型A的子类型,那么Pair[B]可以认为是Pair[A]的子类型。这称为协变关系。在Scala中,可以通过在类型参数前加上+符号来声明协变关系(但在Scala的类声明中通常不显式声明,而是通过类型系统的规则来隐式处理)。
  3. 逆变:如果类型B是类型A的子类型,那么Pair[A]可以认为是Pair[B]的子类型(这在实际中较少见,因为通常函数参数的逆变和返回类型的协变是通过函数类型来处理的,而不是简单的泛型类)。这称为逆变关系。在Scala中,可以通过在类型参数前加上-符号来声明逆变关系(同样,在Scala的类声明中不显式声明逆变)。

需要注意的是,协变和逆变主要影响泛型类型的子类型关系,它们在Scala的类型系统中有着复杂而重要的应用,特别是在处理函数类型和集合类型时。

2.5 泛型的类型擦除

Scala的泛型系统在编译时会将泛型类型信息擦除,生成的字节码中不包含泛型类型的具体信息。这个过程被称为擦除(Type Erasure)。擦除机制是为了保持与Java的互操作性并减少运行时的开销。由于擦除机制,运行时无法获取泛型类型的具体信息,因此不能直接在泛型代码中执行某些类型特定的操作(如创建泛型类型的实例或检查泛型类型的参数类型)。然而,Scala提供了一些反射相关的工具和方法(如ManifestTypeTagClassTag等)来在一定程度上恢复泛型类型的信息。

Scala的泛型是一种强大的特性,它允许编写更加通用和灵活的代码。通过合理使用泛型的上下界、协变、逆变和非变等特性以及理解泛型的类型擦除机制,可以编写出更加健壮和可维护的Scala程序。

以上,如有错误,请不吝指正!

;