Facebook Twitter LinkedIn E-mail
magnify
Home 2014 五月

Scala 专题教程-隐式变换和隐式参数(6):隐含参数(二)

隐含参数的另外一个用法是给前面明确定义的参数补充说明一些信息。 我们先给出一个没有使用隐含参数的例子:

def maxListUpBound[T <:Ordered[T]](element:List[T]):T =
    element match {
      case List() =>
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x::rest =>
        val maxRest=maxListUpBound(rest)
        if(x > maxRest) x
        else maxRest
    }

这个函数是求取一个顺序列表的最大值。但这个函数有个局限,它要求类型T是Ordered[T]的一个子类,因此这个函数无法求一个整数列表的最大值。
下面我们使用隐含参数来解决这个问题。 我们可以再定义一个隐含参数,其类型为一函数类型,可以把一个类型T转换成Ordered[T]。

def maxListImpParam[T](element:List[T])
                    (implicit orderer:T => Ordered[T]):T =
    element match {
      case List() =>
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x::rest =>
        val maxRest=maxListImpParam(rest)(orderer)
        if(orderer(x) > maxRest) x
        else maxRest
    }

在这个函数中,隐含参数使用中两个地方,一个递归调用时传入,第二个是检查列表的表头是否大于列表其余部分的最大值。这个例子的隐含参数给前面定义的类型T补充了一些信息,也就是如果比较两个类型T对象。
这种用法非常普遍以至于Scala的库缺省定义很多类型隐含的到Ordered类型的变换。例如我们调用这个函数:

scala> maxListImpParam(List(1,5,10,34,23))
res2: Int = 34

scala> maxListImpParam(List(3.4,5.6,23,1.2))
res3: Double = 23.0

scala> maxListImpParam(List("one","two","three"))
res4: String = two

在这几个调用中,编译器自动为函数添加了对应的orderer参数。

 

Scala 专题教程-隐式变换和隐式参数(5):隐含参数(一)

编译器可以自动插入implicit的最后一个用法是隐含参数。 比如编译器在需要是可以把someCall(a)修改为someCall(a)(b)或者new someClass(a) 修改为new SomeClass(a)(b),也就是说编译器在需要的时候会自动补充缺少的参数来完成方法的调用。其中(b)为一组参数,而不仅仅只最后一个参数。
这里我们给出一个简单的例子:假定你定义了一个类PreferredPrompt,其中定义了一个用户选择的命令行提示符(比如”$ “或者”> “)。

class PreferredPrompt(val preference:String)

另外又定义了一个Greeter对象,该对象定义了一个greet方法,该方法定义了两个参数,第一个参数代表用户姓名,第二个参数类型为PreferredPrompt,代表提示符。

object Greeter{
	def greet(name:String)(implicit prompt: PreferredPrompt) {
		println("Welcome, " + name + ". The System is ready.")
		println(prompt.preference)
	}
}

第二个参数标记为implicit,表明允许编译器根据需要自动添加。 我们首先采用一般方法的调用方法,提供所有的参数:

scala> val bobsPrompt =new PreferredPrompt("relax> ")
bobsPrompt: PreferredPrompt = PreferredPrompt@7e68a062

scala> Greeter.greet("Bob")(bobsPrompt)
Welcome, Bob. The System is ready.
relax> 

这种用法和我们不给第二个参数添加implicit调用时一样的结果。前面我们提过,隐含参数的用法有点类似某些Dependency Injection框架。 比如我们在某些地方定义一个PreferredPrompt对象,而希望编译器在需要时注入该对象,那么该如果使用呢。
首先,我们定义一个对象,然后在该对象中定义一个PreferredPrompt类型的隐含实例:

object JamesPrefs{
	implicit val prompt=new PreferredPrompt("Yes, master> ")
}

然后我们只提供第二个参数看看什么情况:

scala> Greeter.greet("James")
<console>:10: error: could not find implicit value for parameter prompt: PreferredPrompt
              Greeter.greet("James")
                           ^

出错了,这是因为编译器在当前作用域找不到PreferredPrompt类型的隐含变量,它定义在对象JamesPrefs中,因此需要使用Import引入:

scala> import JamesPrefs._
import JamesPrefs._

scala> Greeter.greet("James")
Welcome, James. The System is ready.
Yes, master> 

可以看到编译器自动插入了第二个参数,要注意的是,implicit 关键字作用到整个参数列表,我们修改一下上面的例子看看:

class PreferredPrompt(val preference:String)
class PreferredDrink(val preference:String)

object Greeter{
	def greet(name:String)(implicit prompt: PreferredPrompt, drink:PreferredDrink) {
		println("Welcome, " + name + ". The System is ready.")
		print("But while you work,")
		println("why not enjoy a cup of " + drink.preference + "?")
		println(prompt.preference)
	}
}

object JamesPrefs{
	implicit val prompt=new PreferredPrompt("Yes, master> ")
	implicit val drink=new PreferredDrink("coffee")
}

import JamesPrefs._

Greeter.greet("James")

scala> Greeter.greet("James")
Welcome, James. The System is ready.
But while you work,why not enjoy a cup of coffee?
Yes, master> 

这里有一点要注意的是,这里implicit参数的类型我们没有直接使用String类型,事实我们可以使用String类型:

object Greeter{
	def greet(name:String)(implicit prompt: String) {
		println("Welcome, " + name + ". The System is ready.")
		println(prompt)
	}
}
implicit val prompt="Yes, master> "
Greeter.greet("James")

scala> Greeter.greet("James")
Welcome, James. The System is ready.
Yes, master> 

当问题是如果有多个参数都使用implicit类型,而类型相同,你就无法提供多个参数,因此implicit类型的参数一般都是定义特殊的类型。

 

Scala 专题教程-隐式变换和隐式参数(4):转换被方法调用的对象

隐式变换也可以转换调用方法的对象,比如但编译器看到X.method,而类型X没有定义method(包括基类)方法,那么编译器就查找作用域内定义的从X到其它对象的类型转换,比如Y,而类型Y定义了method方法,编译器就首先使用隐含类型转换把X转换成Y,然后调用Y的method。
下面我们看看这种用法的两个典型用法:
支持新的类型
这里我们使用前面例子Scala开发教程(50): Ordered Trait中定义的Rational类型为例:

class Rational (n:Int, d:Int) {
	require(d!=0)
	private val g =gcd (n.abs,d.abs)
	val numer =n/g
	val denom =d/g
	override def toString = numer + "/" +denom
	def +(that:Rational)  =
	  new Rational(
		numer * that.denom + that.numer* denom,
		denom * that.denom
	  )
	def +(i:Int) :Rational =
		new Rational(numer +1*denom,denom)
	def * (that:Rational) =
	  new Rational( numer * that.numer, denom * that.denom)
	def this(n:Int) = this(n,1)
	private def gcd(a:Int,b:Int):Int =
	  if(b==0) a else gcd(b, a % b)
   
}

类Rational重载了两个+运算,参数类型分别为Rational和Int。因此你可以把Rational和Rational相加,也可以把Rational和整数相加。

scala> val oneHalf = new Rational(1,2)
oneHalf: Rational = 1/2

scala> oneHalf + oneHalf
res0: Rational = 1/1

scala> oneHalf + 1
res1: Rational = 3/2

但是我们如果使用 1+ oneHalf会出现什么问题呢?

scala> 1 + oneHalf
<console>:10: error: overloaded method value + with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int <and>
  (x: String)String
 cannot be applied to (Rational)
              1 + oneHalf
                ^

整数和其相关类型都没定义和Rational类型相加的操作,因此编译器报错,此时编译器在1能够转换成Rational类型才可以编译过,因此我们可以定义一个从整数到Rational的隐含类型变换:

scala> implicit def int2Rational(x:Int) = new Rational(x)
int2Rational: (x: Int)Rational

现在再执行1+oneHalf

scala> 1 + oneHalf
res3: Rational = 3/2

在定义了int2Rational之后,编译器看到1+oneHalf,发现1没有定义和Rational相加的操作,通常需要报错,编译器在报错之前查找当前作用域从Int到其他类型的定义,而这个转换定义了支持和Rational相加的操作,本例发现int2Rational,因此编译器将 1+ oneHalf转换为

int2Rational(1)+oneHalf

模拟新的语法结构
隐式转换可以用来扩展Scala语言,定义新的语法结构,比如我们在定义一个Map对象时可以使用如下语法:

Map(1 -> "One", 2->"Two",3->"Three")

你有没有想过->内部是如何实现的,->不是scala本身的语法,而是类型ArrowAssoc的一个方法。这个类型定义在包Scala.Predef对象中。Scala.Predef自动引入到当前作用域,在这个对象中,同时定义了一个从类型Any到ArrowAssoc的隐含转换。因此当使用1 -> “One”时,编译器自动插入从1转换到ArrowAssoc转换。具体定义可以参考Scala源码。
利用这种特性,你可以定义新的语法结构,比如行业特定语言(DSL)

 

Scala 专题教程-隐式变换和隐式参数(3):隐含类型转换

使用隐含转换将变量转换成预期的类型是编译器最先使用implicit的地方。这个规则非常简单,当编译器看到类型X而却需要类型Y,它就在当前作用域查找是否定义了从类型X到类型Y的隐式定义。
比如,通常情况下,双精度实数不能直接当整数使用,因为会损失精度:

scala> val i:Int = 3.5
<console>:7: error: type mismatch;
 found   : Double(3.5)
 required: Int
       val i:Int = 3.5
                   ^

当然你可以直接调用3.5.toInt。
这里我们定义一个从Double到Int的隐含类型转换的定义,然后再把3.5赋值给整数,就不会报错。

scala> implicit def doubleToInt(x:Double) = x toInt
doubleToInt: (x: Double)Int

scala> val i:Int = 3.5
i: Int = 3

此时编译器看到一个浮点数3.5,而当前赋值语句需要一个整数,此时按照一般情况,编译器会报错,但在报错之前,编译器会搜寻是否定义了从Double到Int的隐含类型转换,本例,它找到了一个doubleToInt。 因此编译器将把

val i:Int = 3.5

转换成

val i:Int = doubleToInt(3.5)

这就是一个隐含转换的例子,但是从浮点数自动转换成整数并不是一个好的例子,因为会损失精度。 Scala在需要时会自动把整数转换成双精度实数,这是因为在Scala.Predef对象中定义了一个

implicit def int2double(x:Int) :Double = x.toDouble

而Scala.Predef是自动引入到当前作用域的,因此编译器在需要时会自动把整数转换成Double类型。

 

Scala 专题教程-隐式变换和隐式参数(2):使用implicits的一些规则

在Scala中的implicit定义指编译器在需要修复类型匹配时可以用来自动插入的定义。比如说,如果x+y类型不匹配,那么编译器可能试着使用convert(x) + y, 其中convert由某个implicit定义的,这有点类似一个整数和一个浮点数相加,编译器可以自动把整数转换为浮点数。Scala的implicit定义是对这种情况的一个推广,你可以定义一个类型在需要时,如何自动转换成另外一种类型。
Scala的implicit定义符合下面一些规则:
标记规则
只有哪些使用implicit关键字的定义才是可以使用的隐式定义。关键字implicit用来标记一个隐式定义。编译器才可以选择它作为隐式变化的候选项。你可以使用implicit来标记任意变量,函数或是对象。
例如下面为一个隐式函数定义:

implicit def intToString(x:Int) : x.toString

编译器只有在convert被标记成implicit 才会将x + y 改成convert(x) + y .当然这是在 x + y类型不匹配时。

范围规则
编译器在选择备选implicit定义时,只会选取当前作用域的定义,比如说编译器不会去调用someVariable.convert。如果你需要使用someVariable.convert,你必须把someVarible引入到当前作用域。也就是说编译器在选择备选implicit时,只有当convert是当前作用域下单个标志符时才会作为备选implicit。比如说,对于一个函数库来说,在一个Preamble对象中定义一些常用的隐式类型转换非常常见,因此需要使用Preamble的代码可以使用”import Preamble._” 把这些implicit定义引入到当前作用域才可以。
这个规则有一个例外,编译器也会在类的伙伴对象定义中查找所需的implicit定义。例如下面的定义:

object Dollar {
	implicit def dollarToEuro(x:Dollar):Euro = ...
	...
}

class Dollar {
   ...
}

如果在class Dollar的方法有需要Euro类型,但输入数据使用的是Dollar,编译器会在其伙伴对象object Dollar查找所需的隐式类型转换,本例定义一个从Dollar到Euro的implicit 定义可以使用。

一次规则
编译器在需要使用implicit定义时,只会试图转换一次,也就是编译器永远不会把x + y改写成 convert1(convert2(x)) + y。

优先规则
编译器不会在 x+y 已经是合法的情况下去调用implicit 规则。

命名规则
你可以为implicit定义任意的名称。通常情况下你可以任意命名,implicit的名称只在两种情况下有用:一是你想在一个方法中明确指明,另外一个是想把那一个引入到当前作用域。比如我们定义一个对象,包含两个implicit定义:

object MyConversions {
	implicit def stringWrapper(s:String):IndexedSeq[Char] = ...
	implicit def intToString(x:Int):String = ...
}

在你的应用中,你想使用stringWrapper变换,而不想把整数自动转换成字符串,你可以只引入stringWrapper。

import  MyConversions.stringWrapper

编译器使用implicit的几种情况
有三种情况使用implicit: 一是转换成预期的数据类型,而是转换selection的receiver,三是隐含参数。
转换成预期的数据类型比如你有一个方法参数类型是IndexedSeq[Char],在你传入String时,编译器发现类型不匹配,就检查当前作用域是否有从String到IndexedSeq隐式转换。
转换selection的receiver允许你适应某些方法调用,比如 “abc”.exist ,”abc”类型为String,本身没有定义exist方法,这时编辑器就检查当前作用域内String的隐式转换后的类型是否有exist方法,发现stringWrapper转换后成IndexedSeq类型后,可以有exist方法,这个和C# 静态扩展方法功能类似。
隐含参数有点类似是缺省参数,如果在调用方法时没有提供某个参数,编译器会查找当前作用域是否有符合条件的implicit对象作为参数传入(有点类似dependency injection)

 

Scala 专题教程-隐式变换和隐式参数(1):概述

应用中自己写的代码和调用的第三方函数库有着一个基本的区别:也就是你可以任意修改和扩展自己写的代码,而一般来说在没有源码的情况下很难扩展第三方函数库,只能利用函数库提供什么就是什么。
C#3.0支持静态扩展方法,可以为已经定义的库,类进行扩展。
在Scala中解决这个问题是使用隐含类型变换和隐时参数。它们可以使调用函数库变得更加方便,并避免一些繁琐和显而易见的细节。
Scala 的implicit 可以有implicit 类,方法和参数。
本系列文章介绍Scala的隐式变换和隐式参数的用途。