Scala中的隐式理解

我在学习Scala游戏框架教程时,遇到了这样一段让我困惑的代码:

def newTask = Action { implicit request =>
taskForm.bindFromRequest.fold(
errors => BadRequest(views.html.index(Task.all(), errors)),
label => {
Task.create(label)
Redirect(routes.Application.tasks())
}
)
}

所以我决定调查一下,发现了这篇文章

我还是不明白。

这两者的区别是什么:

implicit def double2Int(d : Double) : Int = d.toInt

而且

def double2IntNonImplicit(d : Double) : Int = d.toInt

除了它们有不同的方法名之外。

什么时候应该使用implicit,为什么?

147471 次浏览

我将在下面解释隐式的主要用例,但要了解更多细节,请参阅Scala编程的相关章节

隐式参数

方法的最后一个参数列表可以标记为implicit,这意味着这些值将从调用它们的上下文中获取。如果作用域中没有正确类型的隐式值,则不会编译。由于隐式值必须解析为单个值,并且为了避免冲突,使类型特定于其目的是一个好主意,例如,不要求您的方法找到隐式Int!

例子:

  // probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s


// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
addPrefix("abc")  // returns "***abc"

隐式转换

当编译器为上下文找到错误类型的表达式时,它将寻找允许它进行类型检查的类型的隐式Function值。因此,如果需要A并且它找到了B,它将在作用域中查找类型为B => A的隐式值(它还检查其他一些地方,如BA伴生对象,如果它们存在的话)。因为__abc6可以被“eta-expand”为Function对象,所以implicit def xyz(arg: B): A也可以。

因此,你的方法之间的区别在于,标记为implicit的方法将在找到Double而需要Int时被编译器插入。

implicit def doubleToInt(d: Double) = d.toInt
val x: Int = 42.0

会和

def doubleToInt(d: Double) = d.toInt
val x: Int = doubleToInt(42.0)

在第二种方法中,我们手动插入转换;在第一种情况下,编译器自动执行相同的操作。转换是必需的,因为左边有类型注释。


关于Play的第一个片段:

动作在Play文档中的这个页面中解释(另见API文档)。你正在使用

apply(block: (Request[AnyContent]) ⇒ Result): Action[AnyContent]

Action对象(它是同名特征的伴侣)。

所以我们需要提供一个Function作为参数,它可以写成这样的文字形式

request => ...

在函数字面量中,=>之前的部分是一个值声明,如果你愿意,可以标记为implicit,就像任何其他val声明一样。这里,request 必须标记为implicit以便进行类型检查,但这样做将为函数中可能需要它的任何方法标记为可作为隐式值使用(当然,它也可以显式使用)。在这个特殊情况下,这样做是因为形式类上的bindFromRequest方法需要一个隐式的Request参数。

为什么以及何时应该将request参数标记为implicit:

你将在动作的主体中使用的一些方法具有隐式参数表,例如,Form。Scala定义了一个方法:

def bindFromRequest()(implicit request: play.api.mvc.Request[_]): Form[T] = { ... }

你不一定会注意到这一点,因为你只会调用myForm.bindFromRequest()。你不必显式地提供隐式参数。不,你让编译器在每次遇到需要请求实例的方法调用时寻找任何有效的候选对象来传递。既然你有一个可用的请求,你所需要做的就是将它标记为implicit

显式地将它标记为可供隐式的使用。

你暗示编译器,它是“OK”使用的请求对象发送的Play框架(我们给了名字“请求”,但可以只使用“r”或“req”)在任何需要的地方,“偷偷地”。

myForm.bindFromRequest()

看到了吗?它不在那里,但是它在那里!

它发生的时候不需要你在每个需要它的地方手动插入它(但是如果你愿意,你可以可以显式传递它,不管它是否标记为implicit):

myForm.bindFromRequest()(request)

如果不将其标记为隐式,则可以必须执行上述操作。你不需要把它标记为隐式。

你是否应该将请求标记为implicit?只有在使用声明隐式参数列表,期望请求的实例的方法时才真正需要这样做。但是为了保持简单,你可以养成将请求标记为implicit 总是的习惯。这样你就可以写出漂亮简洁的代码。

警告:明智地包含讽刺!YMMV……

路易吉的回答是完整且正确的。这只是通过一个例子来扩展它,说明你可以光荣地过度使用值得一提的,因为这在Scala项目中经常发生。实际上,你甚至可以在“最佳实践”指南中找到它。

object HelloWorld {
case class Text(content: String)
case class Prefix(text: String)


implicit def String2Text(content: String)(implicit prefix: Prefix) = {
Text(prefix.text + " " + content)
}


def printText(text: Text): Unit = {
println(text.content)
}


def main(args: Array[String]): Unit = {
printText("World!")
}


// Best to hide this line somewhere below a pile of completely unrelated code.
// Better yet, import its package from another distant place.
implicit val prefixLOL = Prefix("Hello")
}

此外,在上述情况下,应该有only one隐式函数,其类型为double => Int。否则,编译器会感到困惑,无法正确编译。

//this won't compile


implicit def doubleToInt(d: Double) = d.toInt
implicit def doubleToIntSecond(d: Double) = d.toInt
val x: Int = 42.0

在scala中,隐式工作方式为:

转换器

参数值注入器

扩展方法

这里有一些隐式的用法

  1. 隐式类型转换:它将产生错误的赋值转换为预期的类型

    val x :String = "1"
    
    
    val y:Int = x
    

字符串不是Int子类型,所以错误发生在第2行。为了解决错误,编译器将在作用域中寻找这样一个方法,该方法具有隐式关键字,并以字符串作为参数并返回Int

所以

implicit def z(a:String):Int = 2


val x :String = "1"


val y:Int = x // compiler will use z here like val y:Int=z(x)


println(y) // result 2  & no error!
  1. 隐式接收者转换:我们通常通过接收器调用对象的属性,例如。方法或变量。因此,接收器要调用任何属性,该属性必须是接收器的类/对象的成员。

     class Mahadi{
    
    
    val haveCar:String ="BMW"
    
    
    }
    

    class Johnny{


val haveTv:String = "Sony"


}

   val mahadi = new Mahadi






mahadi.haveTv // Error happening

这里mahadi.haveTv将产生一个错误。因为scala编译器将首先寻找haveTv属性到mahadi接收器。它不会找到。其次,它将在作用域内寻找具有隐式的关键字的方法,该方法以Mahadi对象作为参数并返回约翰尼对象。但是这里没有。因此它将创建错误。但是下面的内容是可以的。

class Mahadi{


val haveCar:String ="BMW"


}

class Johnny{


val haveTv:String = "Sony"


}

val mahadi = new Mahadi


implicit def z(a:Mahadi):Johnny = new Johnny


mahadi.haveTv // compiler will use z here like new Johnny().haveTv


println(mahadi.haveTv)// result Sony & no error
  1. 隐式参数注入:如果我们调用一个方法并且没有传递它的参数值,它将导致一个错误。scala编译器的工作方式是这样的——首先将尝试传递值,但它不会获得参数的直接值。

     def x(a:Int)= a
    
    
    x // ERROR happening
    

其次,如果形参有任何隐式关键字,它将在范围中查找值为相同类型的任何瓦尔。如果得不到就会造成错误。

def x(implicit a:Int)= a


x // error happening here

为了解决这个问题,编译器将寻找具有整数类型隐式瓦尔,因为参数一个具有隐式的关键字

def x(implicit a:Int)=a


implicit val z:Int =10


x // compiler will use implicit like this x(z)
println(x) // will result 10 & no error.

另一个例子:

def l(implicit b:Int)


def x(implicit a:Int)= l(a)

我们也可以写成-

def x(implicit a:Int)= l

因为l有一个隐式参数,在方法x的主体的范围内,有一个隐式局部变量(参数是局部变量) 一个,它是x的参数,所以在x的体 方法中,方法签名l0由l1归档。

所以

 def x(implicit a:Int)= l

将在编译器这样

def x(implicit a:Int)= l(a)

另一个例子:

def c(implicit k:Int):String = k.toString


def x(a:Int => String):String =a


x{
x => c
}

它将导致错误,因为{x =祝辞;c}中的c需要在参数中显式地传递值或隐式地传递值在范围

因此,在调用方法x时,可以显式地将函数字面量的参数设为隐式的

x{
implicit x => c // the compiler will set the parameter of c like this c(x)
}

这已在Play-Framework的操作方法中使用

in view folder of app the template is declared like
@()(implicit requestHreader:RequestHeader)


in controller action is like


def index = Action{
implicit request =>


Ok(views.html.formpage())


}

如果你没有显式地提到请求参数,那么你必须写-

def index = Action{
request =>


Ok(views.html.formpage()(request))


}
  1. 扩展方法

想一下,我们想添加一个具有Integer对象的新方法。这个方法的名字是meterToCm,

> 1 .meterToCm
res0 100

为此,我们需要在对象/类/trait中创建一个隐式类。这个类不能是案例类。

object Extensions{
    

implicit class MeterToCm(meter:Int){
        

def  meterToCm={
meter*100
}


}


}

注意隐式类只接受一个构造函数参数

现在在您想要使用的范围中导入隐式类

import  Extensions._


2.meterToCm // result 200

scala中一个非常基本的隐式函数示例。

隐式参数:

val value = 10
implicit val multiplier = 3
def multiply(implicit by: Int) = value * by
val result = multiply // implicit parameter wiil be passed here
println(result) // It will print 30 as a result

这里multiplier将隐式传递给函数multiply。函数调用中缺失的参数将在当前作用域中按类型查找,这意味着如果作用域中没有Int类型的隐式变量,则代码将无法编译。

隐式转换:

implicit def convert(a: Double): Int = a.toInt
val res = multiply(2.0) // Type conversions with implicit functions
println(res)  // It will print 20 as a result

注意:当我们调用multiply函数传递一个双值时,编译器将尝试在当前作用域中找到转换隐式函数,该函数将Int转换为Double(作为函数multiply接受Int形参)。如果没有隐式convert函数,则编译器将不会编译代码。

我和你有同样的问题,我想我应该通过几个非常简单的例子来分享我是如何开始理解它的(注意,它只涵盖了常见的用例)。

在Scala中使用implicit有两个常见的用例。

  • 在变量上使用它
  • 在函数上使用它

例子如下

在变量上使用它。如你所见,如果implicit关键字在最后一个参数列表中使用,那么最接近的变量将被使用。

// Here I define a class and initiated an instance of this class
case class Person(val name: String)
val charles: Person = Person("Charles")


// Here I define a function
def greeting(words: String)(implicit person: Person) = person match {
case Person(name: String) if name != "" => s"$name, $words"
case _ => "$words"
}


greeting("Good morning") // Charles, Good moring


val charles: Person = Person("")
greeting("Good morning") // Good moring

在函数上使用它。如你所见,如果在函数上使用implicit,则将使用最接近的类型转换方法。

val num = 10 // num: Int (of course)


// Here I define a implicit function
implicit def intToString(num: Int) = s"$num -- I am a String now!"


val num = 10 // num: Int (of course). Nothing happens yet.. Compiler believes you want 10 to be an Int


// Util...
val num: String = 10 // Compiler trust you first, and it thinks you have `implicitly` told it that you had a way to covert the type from Int to String, which the function `intToString` can do!
// So num is now actually "10 -- I am a String now!"
// console will print this -> val num: String = 10 -- I am a String now!

希望这能有所帮助。