Scala学习笔记-其他特性(1)-异常、泛型和隐式转换

异常

Scala的异常处理在语法上和Java类似,但是又不尽相同。

在Java中,异常处理采用try-catch-finally的方式。其中使用多个catch语句来捕获对应的异常,这时候需要将范围小的异常类写在前面,大的异常类写在后面。

在Scala中同样使用try-catch-finally的方式来获取,其中只有一个catch代码块,在里面通过case进行异常的判定

1
2
3
4
5
6
7
8
9
10
11
12
try{
val n = 10 / 0
} catch {
case e: ArithmeticException => {
println("发生算术异常")
}
case e: Exception => {
println("发生一般异常")
}
} finally {
println("处理结束")
}
  • Scala的异常工作机制与Java一样,但是没有编译时异常,所有的异常都是运行时处理

  • Scala中也是用throw关键字抛出异常,所有异常都是Throwable的子类,throw表达式是有类型的,就是NothingNothing主要用在一个函数总是不能正常工作,总是抛出异常的时候用作返回值类型。

  • Java中提供了throws关键字来声明方法可能引发的异常,在Scala中可以使用throws注解来声明异常

    1
    2
    3
    4
    @throws(classOf[NumberFormatException])
    def f11()={
    "abc".toInt
    }

隐式转换

总结来说,在Scala中,当编译器第一次编译失败的时候,会在当前的环境中查找能够让代码编译通过的方法,用于将类型进行转换,实现二次编译。当编译错误的时候,编译器会尝试在当前作用域范围内查找能够调用对应功能的转换规则,这个过程由编译器完成,称之为隐式转换或者自动转换。(隐式转换能够在不修改类代码的情况下扩充类的功能)

首先描述下面场景:

现在我们完成了一个自定义类MyRichInt,其中实现了自定义的比较大小的方法。

1
2
3
4
5
6
7
// 自定义类
class MyRichInt(val value: Int) {
// 自定义比较大小的方法
def myMax(n: Int): Int = if (n < value) value else n

def myMin(n: Int): Int = if (n > value) value else n
}

但是能够调用我们方法的只有MyRichInt对应的对象,如果是一个整数的话,是无法调用我们的自定义比较大小方法的。下面的代码会报错:

1
println(12.myMax(15))

隐式转换就能够解决这样的场景。

隐式函数

在函数定义前加上implicit将其声明为隐式转换函数。

之前Int类型的对象无法调用我们的自定义比较大小方法,但是编译器发现了一个隐式函数,能够完成从Int到MyRichInt的转换,完成转换之后能够调用,因此能够正常运行。

隐式函数定义如下:

1
2
// 隐式函数
implicit def convert(num: Int): MyRichInt = new MyRichInt(num)

有了隐式函数之后,下面代码能够正常运行(当然注意隐式函数需要在下面代码的作用域内,即定义在它之前)

能够运行是因为编译器找到了隐式函数,然后将Int对象转换成了一个MyRichInt对象

1
println(12.myMax(15))

隐式参数

普通方法或者函数中的参数可以通过implicit关键字声明为隐式参数,调用该方法的时候,如果传入了,则以传入参数为准,如果没有传入,则编译器在当前作用域寻找符合条件的隐式值。

关于隐式值:

  • 在同一个作用域,相同类型的隐式值只能有一个(避免二义性)
  • 编译器会按照隐式参数的类型去寻找对应的隐式值,与隐式值的名称无关
  • 隐式参数优于默认参数

示例代码如下:

1
2
3
4
5
6
7
8
9
// 指定隐式参数
implicit val str: String = "hello world"

def hello(implicit arg: String = "goodbye world") = {
println(arg)
}

hello // hello world
hello() // goodbye world

输出结果如下:

1
2
hello world
goodbye world

如上所示,如果参数列表中只有一个隐式参数,那么无论这个隐式参数是否提供默认参数,需要使用隐式参数的话,在调用的时候需要将括号一起省略掉。(如同上面的hello

如果调用的时候,又想要括号,则可以在函数定义的时候,在隐式参数列表之前添加一个空参数列表(),那么会出现以下情况

  • func:调用隐式参数

  • func():调用隐式参数

  • func()():使用默认参数

  • func()(arg):使用传入参数

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 指定隐式参数
implicit val str: String = "hello world"

def hello()(implicit arg: String = "goodbye world") = {
println(arg)
}

hello // hello world
hello() // hello world
hello()() // goodbye world
hello()("wow") // wow

输出结果如下:

1
2
3
4
hello world
hello world
goodbye world
wow

示例代码可以简写为如下:

1
2
3
def hello() = {
println(implicitly[String])
}

这样的话就无法传入参数了(这样不像是一个全局变量吗?)

隐式类

在Scala2.10之后提供了隐式类。通过implicit来声明类。此时类的构造方法将被声明为隐式转换函数。也就是说,如果第一次编译没有通过,就可能将数据直接传给构造函数转换成对应的类,可以将隐式类看作是隐式函数的一个扩展。

隐式类的说明:

  • 所带构造参数有且只能有一个
  • 隐式类必须定义在“类”或者“伴生对象”或者“包对象”中,即隐式类不能是顶级的

在同一个作用域中定义隐式转换函数和隐式类会冲突,定义一个就行

1
2
3
4
5
6
7
8
9
10
11
12
13
object Test {
// 自定义隐式类
implicit class MyRichInt(val value: Int) {
// 自定义比较大小的方法
def myMax(n: Int): Int = if (n < value) value else n

def myMin(n: Int): Int = if (n > value) value else n
}

def main(args: Array[String]): Unit = {
println(12.myMax(15))
}
}

隐式解析机制

  • 首先在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)。
  • 如果第一条规查找隐式对象失败,会继续在隐式参数的类型的作用域中查找。类型的作用域是指该类型相关联的全部伴生对象以及该类型所在包的包对象。

泛型

协变和逆变

语法如下:

1
2
3
class MyList[+T] {} // 协变
class MyList[-T] {} // 逆变
class MyList[T] {} // 不变

假设SonFather是父子关系,Son是子类。

  • 协变(Covariance):MyList[Son]MyList[Father]的子类,协同变化。
  • 逆变(Contravariance):MyList[Son]MyList[Father]的父类,逆向变化。
  • 不变(Invariant):MyList[Father] MyList[Son]没有父子关系。

泛型上下限

规定泛型的上下限,对传入的泛型进行限定

  • 泛型上限:class MyList[T <: Type],可以传入Type自身或者子类。
  • 泛型下限:class MyList[T >: Type],可以传入Type自身或者父类。

上下文限定

  • def f[A : B](a: A) = println(a)等同于def f[A](a: A)(implicit arg: B[A])
  • 是将泛型和隐式转换结合的产物,使用上下文限定(前者)后,方法内无法使用隐式参数名调用隐式参数,需要通过implicitly[Ordering[A]]获取隐式变量。

(待补充...)


Scala学习笔记-其他特性(1)-异常、泛型和隐式转换
http://example.com/2022/05/03/Scala学习笔记-其他特性-1-异常、泛型和隐式转换/
作者
EverNorif
发布于
2022年5月3日
许可协议