了解Scala中的隐式


308

我正在通过Scala Playframework教程学习,遇到了这个让我感到困惑的代码片段:

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什么时候应该使用,为什么?


Answers:


391

我将在下面解释隐式的主要用例,但有关更多详细信息,请参见《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在范围内查找type的隐式值(它还会检查诸如Band A对象中的其他位置(如果存在)。由于defs可以“ eta扩展”为Function对象,所以an implicit def xyz(arg: B): A也可以。

因此,您的方法之间的区别在于,找到a但要求implicita时,编译器将为您插入一个标记的方法。DoubleInt

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的第一个片段:

播放文档在此页面上说明了操作(另请参阅API docs)。您正在使用

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

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

因此,我们需要提供一个Function作为参数,可以将其写为以下形式的文字

request => ...

在函数文字中,之前的部分=>是值声明,并且可以根据需要进行标记implicit,就像在其他任何val声明中一样。在这里,request 不必为此进行标记implicit以进行类型检查,但是这样做可以将其作为隐式值提供给该函数中可能需要使用它的任何方法(当然,也可以显式使用它) 。在这种特殊情况下,这样做是因为FormbindFromRequest上的方法需要一个隐式参数。Request


12
感谢您的答复。第21章的链接确实很棒。欣赏它。
Clive 2012年

14
只是为了补充这一点,下面的视频很好地解释了隐式函数以及scala的其他功能youtube.com/watch?v=IobLWVuD-CQ
Shakti 2013年

跳到以上视频中的24:25(针对那些不想在55分钟内收听的人)
凌晨

36

警告:明智地包含讽刺!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")
}

1
哈哈。有幽默感。
Det

1
我很幽默。这种事情是很多年前我停止尝试学习Scala的原因之一,而直到现在我才重新开始学习它。我从不知道我正在查看的代码中某些(许多)隐式内容来自何处。
梅尔斯顿,

7

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

您将在操作主体中使用的某些方法具有隐式参数列表,例如Form.scala定义了一个方法:

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

您不一定会注意到这一点,就像您将要调用的那样。myForm.bindFromRequest()您不必显式提供隐式参数。不,您让编译器在每次遇到需要该请求实例的方法调用时寻找要传递的任何有效候选对象。由于您确实有一个请求,因此您要做的就是将其标记为implicit

您将其明确标记为可用于隐式使用。

您暗示编译器使用Play框架发送的请求对象(确定名称为“ request”,但可能只使用了“ r”或“ req”)是“确定”的,在“狡猾”的地方。

myForm.bindFromRequest()

看见?它不在那里,但是它那里!

它只是发生而无需将其手动插入所需的每个位置(但是您可以显式地传递它,如果您愿意的话,无论它是否被标记implicit):

myForm.bindFromRequest()(request)

如果不将其标记为隐式,则必须执行上述操作。不必将其标记为隐式。

何时应将请求标记为implicit?仅在使用声明隐式参数列表(需要Request实例)的方法时才需要。但是为了简单起见,您可以养成implicit 始终标记请求的习惯。这样,您就可以编写漂亮的简洁代码。


2
“那样一来,您就可以编写漂亮的简洁代码。” 或者,正如@DanielDinnyes指出的那样,代码很漂亮。跟踪隐式信息的来源可能是一个真正的痛苦,如果您不小心的话,它们实际上会使代码更难以阅读和维护。
梅尔斯顿

7

在scala中,隐式的工作方式是

转换器

参数值注入器

隐式使用有3种类型

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

    val x:String =“ 1”

    值y:Int = x

String不是Int子类型,因此错误在第2行发生。要解决该错误,编译器将在范围内寻找具有隐式关键字并采用String作为参数并返回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编译器将首先向mahadi接收器寻找haveTv属性。找不到。其次,它将寻找具有隐式关键字的范围内的方法,该方法以Mahadi对象作为参数并返回Johnny对象。但是这里没有。因此会产生错误。但是以下是可以的。

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

其次,如果参数具有任何隐式关键字,它将在作用域中查找具有相同类型值的任何val。如果没有得到它将导致错误。

def x(implicit a:Int)= a

x // error happening here

为了解决这个问题,编译器将查找具有Int类型的隐式val,因为参数a具有隐式关键字

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的主体范围内,所以存在一个隐式局部变量参数是局部变量a,它是x的参数,因此在x 方法主体中,方法签名l的隐式参数值是由x方法的局部隐式变量(参数)a隐式归档。

所以

 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 {X => C ^}需要显式传值在参数或隐式VAL 范围

因此,当我们调用方法x时,可以使函数文字的参数式隐式

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

这已在“播放框架”的动作方法中使用

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))  

}

4

另外,在上述情况下,应该存在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

0

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传递双精度值的函数时,编译器将尝试在当前范围内查找转换隐式函数,该隐式函数将转换IntDouble(作为函数multiplyaccept Int参数)。如果没有隐式convert函数,则编译器将不会编译代码。


0

我有一个与您完全相同的问题,我想我应该通过一些非常简单的示例来分享我如何开始理解它(请注意,它仅涵盖了常见的用例)。

在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!

希望这会有所帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.