隐式转换的本质:可以减少从一个类型显式转换成另一个类型的需要。
1.隐式规则
隐式定义:指允许编译器插入程序以解决类型错误的定义。隐式转换受一下规则约束:
1)标记规则:只有标记为implicit的定义才可用。
2)作用域规则:被插入的隐式转换必须是当前作用域的单个标识符,或者跟隐式转换的源类型或目标类型有关联。也就是说编译器会从隐式转换的源类型或目标类型的伴生对象中查找隐式定义。
3)每次一个原则:每次只能有一个隐式定义被插入。
4)显式优先原则:只要代码按编写的样子能通过类型检查,就不尝试隐式定义。
2.隐式定义的使用
Scala总共会有3个地方会使用到隐式定义:转换到一个预期的类型;对成员转换接收端(字段、方法);隐式参数。
2.1隐式转换到一个预期的类型
当方法的参数类型是X的时候,如果想要把类型Y的参数传进去,可以使用使用隐式定义。
class Food(val name:String) {} object Food { implicit def string2Food1(s:String) = new Food(s) } class Cat { implicit def string2Food2(s:String) = new Food(s) def eat(food:Food) = println(s"cat eat ${food.name}") } import Yin.string2Food3 object test { implicit def string2Food4(s:String) = new Food(s) def main(args:Array[String]) = { val cat = new Cat cat.eat("fish") } implicit def string2Food5(s:String) = new Food(s) } object Yin { implicit def string2Food3(s:String) = new Food(s) }
Cat类的eat方法需要的是Food类型的参数,现在把String类型的参数添加进去,所以需要加隐式定义。这里的隐式转换是把String类型转换为Food类型。
1)string2Food1定义在Food的伴生对象中,因而是可以起作用的
2)string2Food2定义在Cat类中,不在当前作用域也不在源类型或目标类型中,因此不起作用
3)string2Food3是把隐式定义放在一个专门的对象中,之后再引用,因此是可以的
4)string2Food4在当前作用域中,起作用
5)string2Food5不在当前作用域中,没有用处
2.2转换接收端
隐式转换可以应用于方法调用的接收端,比如有个obj.doit方法,假设obj没有doit这个方法,那么可以把obj隐式转换为另一个有doit方法的对象,假装obj有doit这个方法。下面是给Int类型假装有**这个方法:
//下面空行代表类在不同文件中 import Invisib.int2Calcu object test{ def main(args:Array[String]) = { val a = 1 ** 2 println(a) } } class Calcu(val x:Int) { def **(y:Int) : Int = x * (y + 1) - 3 } object Invisib { implicit def int2Calcu(x:Int) = new Calcu(x) }
隐式类
隐式类以implicit修饰,编译器会生成一个从类的构造方法参数到类本身的隐式转换。比如下面的StringMaker会自动生成implicit def StringMaker(str:String) = new StringMaker(str)。
//下面的空行代表类在不同文件中 import StringUtils.StringMaker object test { def main(args:Array[String]) = { println("abcdefg" - 2) } } object StringUtils { implicit class StringMaker(str:String) { def -(len:Int) = { val length = str.length if(len <= length) str.substring(0, length - len) else "" } } }
隐式类有以下的约束:
1)隐式类不能是样例类
2)其构造方法必须有且仅有一个
3)隐式类必须在另一个对象、类或特质里面
2.3隐式参数
方法的最后一个参数列表可以被隐式的提供,在调用时可以不需要最后一个参数列表的参数。
def test(implicit x:Int, y:Int) = {} //下面这个方法提供的不是参数列表,编译错误 //def test(x:Int, implicit y:Int) = {}
下面给出一个简单的例子用于理解隐式参数:
//下面空行代表类在不同的文件
import Tech.hobby object test { def main(args:Array[String]) = { val t = new Tech println(t) } } class Tech { def printHobby(implicit hobby:String) = println(hobby) } object Tech { implicit val hobby = "Listen" }
3.上下文界定
上下文界定是当需要一个M[T]类型的隐式值时,其中T是泛型,如果方法体内可以去掉对隐式值的使用,则可以省掉这个参数的名称使用上下文界定T:M缩短方法签名。
object test { def main(args:Array[String]) = { val bg = bigger(1, 5) } def bigger[T](x:T, y:T)(implicit ordering:Ordering[T]) : T = { if(ordering.gt(x, y)) x //去掉方法体中对隐式值ordering的引用 //if(implicitly[Ordering[T]].gt(x, y)) x else y } }
bigger方法是用来对比任意类型的2个值的,因为类型的不同,使用Ordering[T]类型的隐式值用来对比,在方法体中可以使用标准类库的方法:def implicitly[T](implicit t:T) = t 去掉对ordering的引用。因为在标准类库中很多常见的类型都提供了这种排序方法,所以隐式值ordering不需要自己提供,下面使用上下文界定缩短方法:
object test { def main(args:Array[String]) = { val bg = bigger(1, 5) } //上下文界定 def bigger[T:Ordering](x:T, y:T) : T = { if(implicitly[Ordering[T]].gt(x, y)) x else y } }
4.上界、下界与视图界定
4.1上界、下界
在使用泛型的时候,可以指定泛型类型是某个类的子类,或是某个类的父类,从而约束泛型的类型范围。T <: M代表上界,T需要是类型M的子类或本身;T >: M代表下界,T需要是M的父类或本身。在下面的例子中,为了能进行compareTo方法的对比,参数类型需要是实现Comparable[T]的。
object test { def main(args:Array[String]) = { val cc = bigger("abc", "cba") } def bigger[T <: Comparable[T]](x:T, y:T) : T = { if (x.compareTo(y) > 0) x else y } }
4.2视图界定
在上面的那个例子中,String类型因为实现了Comparable[T],所以可以调用bigger方法,而如果使用bigger(1, 2),则会报错。这时可以使用视图界定<%,编译器会查找Int类型的隐式转换类型有没有是实现Comparable的,下面是视图界定的例子:
object test { def main(args:Array[String]) = { val cc = bigger(1, 2) } def bigger[T <% Comparable[T]](x:T, y:T) : T = { if (x.compareTo(y) > 0) x else y } }