Scala Implicit 详解

Scala Implicit 详解Implicit 是 Scala 中一个很重要的特性 开始学习 Scala 之前一直以为它和 Java 差不多 然而真的看一些 Scala 的源码时却发现并没有想象中那么简单 所以准备写几篇文章来详解 Scala 中异于 Java 的特性 就从 Implicit 开始吧 在我看来 Implicit 做的事情也是 Scala 主要做的事情 那就是代码压缩 减少模块代码 talkischea

Implicit 是 Scala 中一个很重要的特性,开始学习 Scala 之前一直以为它和 Java 差不多,然而真的看一些 Scala 的源码时却发现并没有想象中那么简单,所以准备写几篇文章来详解 Scala 中异于 Java 的特性,就从 Implicit 开始吧。

在我看来,Implicit 做的事情也是 Scala 主要做的事情,那就是代码压缩,减少模块代码,talk is cheap,先用一个实例来了解一下 Implicit 的作用。

一个栗子

马上就到情人节了,你会如何表达你的爱意呢?

让我们做一些准备工作,首先要有一个恋人的接口,包含一个表达爱意的函数sendLove

trait Lover { 
    def sendLove(love: Love) {} }

然后再来一个爱意接口,爱不能总是在心口难开,而是应当有所行动,所以有一个行动函数takeActionevent是触发爱的事件,这里当然是情人节这件大事了

trait Love { 
    val event: Event val show = takeAction(event) def takeAction(event: Event) }

今年的情人节很尴尬,正好在过年的前一天,所以这一天很多恋人可能都各呆各家各找各妈了,所以这里应当是异地恋,吼吼吼吼让你们虐狗

class RemoteLover extends Lover { 
   } val lover: Lover = new RemoteLover

OK,准备工作完成,情人节到了,异地恋们来表达一波爱意吧!

// without implicit lover.sendLove( new Love { val event = Event("Valentine is coming!") def takeAction(event: Event) = { println("Buy a gift!") } } )

爱你的方式多种多样,但爱你的心却永远不变。可能是送一件礼物聊表心意,也可能是飞奔到你身边给一个拥抱。我并不是在装情圣,我只是想说明,上面代码中,会改变的其实只有这行代码

println("Buy a gift!")

Love及其实现的函数takeAction可以说是固定的模块化代码,如果能缩减成下面这个样子那就太棒了

lover.sendLove( (_: Event) => println("Give a hug!") )

看一下发生了什么。函数sendLove所需要的参数是Love对象:

new Love { val event = Event("Valentine is coming!") def takeAction(event: Event) = { println("Buy a gift!") } }

但是缩减后其对应的参数变成了一个函数:

(_: Event) => println("Give a hug!")

不行啊!这样搞参数类型不匹配啊!这时候就该想到类型转换了。如下是一个超简单的例子:

"abc" + 123

这里 123 是 Int 转换成了 String,参考这个思路,试着将函数转化成Love

implicit def function2Love(f: Event => Unit) = new Love { val event = Event("Valentine is coming!") def takeAction(event: Event): Unit = f(event) }

这时原来的代码就可以写成:

lover.sendLove( function2Love( (_: Event) => println("Give a hug!") ) )

这里我们明确指定函数function2Love进行类型转换,注意上面function2Love的前面使用了 implicit 关键字,这个关键字的作用就是可以隐式将一种类型转换为另一种类型,而不需要明确调用,如下:

// with implicit import Lover.function2Love lover.sendLove( (_: Event) => println("Give a hug!") )

在这里具体发生了什么呢?首先sendLove需要的参数类型是Love,但是这里的参数却是一个函数。当编译器发现代码中存在不匹配的类型的时候,它不会立即报错,而是选择先抢救一下,抢救的方式就是在当前代码域中找一下有没有 implicit 修饰的代码,然后找到了function2Love这个隐式转换函数,代码编译通过。

以上就是 Implicit 的一个入门例子,全部代码见 GitHub

一般来说 Implicit 主要用在两个方面:隐式转换和隐式参数,下面分别进行讲解


隐式转换

隐式转换到某个期望类型

任何时候编译器发现了类型 X,但是却需要类型 Y 的时候,它就会寻找一个可以把 X 转换成 Y 的隐式函数。

例如正常情况下一个 Double 类型是不能转换成 Int 类型,但是我们可以定义一个隐式函数来实现:

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

虽然我在这里举了一个这样的例子,但是这种从 Double 转到 Int 的方式是不推荐的,因为会丢失精度。相反从 Int 转到 Double 就是正常的,而且其底层实现就是用的隐式转换。Scala 有一个scala.Predef对象,默认引入到每一个程序中,就包含了类似这种从“小”数类型向“大”数类型的隐式转换:

implicit def int2double(x: Int): Double = x.toDouble scala> val x: Double = 3 x: Double = 3.0

隐式类

隐式类是 Scala2.10 新增的,为了更方便地写包装器类。有以下几个要点:

  1. 隐式类不能是 case class,并且构造器必须只有一个参数
  2. 一个隐式类必须在其他 object,class,trait 的内部
  3. 对于一个隐式类,编译器会自动生成一个隐式转换函数,该函数会产生一个隐式类对象

例如一个矩形类 Rectangle

case class Rectangle(width: Int, height: Int)

新建一个矩形需要这样:

scala> val rec = Rectangle(3, 4) rec: Rectangle = Rectangle(3,4)

这还是有点麻烦的,如果我们想通过 3 x 4 这种方式就新建一个对象那该怎么办呢,这个时候就要用到隐式类了:

implicit class RectangleMaker(width: Int) { 
     def x(height: Int) = Rectangle(width, height) }

我们再来建立一个矩形:

scala> val easyRec = 3 x 4 easyRec: Rectangle = Rectangle(3,4)

这看起来就简单直接多了嘛,那么它是怎么实现的呢?前面我们说了,对于隐式类,编译器会自动生成一个隐式转换函数,如下:

// Automatically generated implicit def RectangleMaker(width: Int) = new RectangleMaker(width)

所以当编译器看到 3 x 4 的时候,首先发现 3 这个 Int 没有 x 函数,然后就找隐式转换,通过上面的隐式转换函数生成了一个RectangleMaker(3),然后调用RectangleMaker的 x 函数,就产生了一个矩形Rectangle(3,4)


隐式参数

下面要开始谈隐式参数了,这也是我们最常用到的,隐式参数并不是用于类型转换的,更多的是为了减少代码量,要点如下:

  1. 隐式参数是指类或者函数所对应的参数列表,如下面 implicit 所在的参数列表
    implicit class Greet(name: String)(implicit hello: Hello, world: World)
  2. implicit 放在参数列表的最前面,不管有几个参数,它们全部都是隐式的
  3. 定义了隐式参数以后,我们新建一个类或者调用一个函数的时候就可以省略该参数列表了
    val greet = new Greet("Jack")
  4. 省略参数列表并不是不需要参数列表,我们需要使用 implicit 关键字定义出隐式参数列表中的所有变量,然后使用 import 导入
    implicit val hello = new Hello implicit val world = new Wrold

上面就是一些基本点,狗年春节马上就要来了,就让我们以此为例来写一个回家的 Demo 吧

case class Remote(address: String) case class Home(address: String) object Transportation { 
      def transport(name: String)(implicit remote: Remote, home: Home) = { println(s"To celebrate Spring Festival, go from $remote to $home, by $name") } } object Address { 
      implicit val remote = new Remote("Shanghai") implicit val home = new Home("Shanxi") }

这里定义了起始地址Remote Home,虽然其本质是 String 类型,但是对于隐式参数来说最好还是定义一个专门的类,因为如果直接使用 String 作为隐式变量,由于其太过于普遍编译器可能不太容易找到,或者和其他隐式变量混淆,而专用的类就不用担心这些。
还有一个表示交通方式的函数transport,该函数里面用到了隐式参数,最后Address对象定义了我们需要的隐式变量。

然后,准备回家喽!

object GoHome extends App { 
      import Address._ Transportation.transport("airplane") }

结果如下:

To celebrate Spring Festival, go from Remote(Shanghai) to Home(Shanxi), by airplane.

上下文绑定

了解了隐式参数以后,我们可以再看一个有趣的语法糖:上下文绑定 context bound

这里我用《Programming in Scala》中的一个例子来讲解,要求找出一个 List 中所有元素的最大值,首先我们看一下常规的实现了隐式参数的写法:

// with implicit def maxListOrdering[T](elements: List[T]) (implicit ordering: Ordering[T]): T = elements match { case List() => throw new IllegalArgumentException("empty lists!") case List(x) => x case x :: rest => val maxRest = maxListOrdering(rest) if (ordering.gt(x, maxRest)) x else maxRest }

这个函数的参数有两个,一个是 T 组成的 List,另一个是隐式参数 Ordering,对于一些常见的 T 类型,例如 Int 或者 String,它们都有一个默认的 Ordering 实现,所以不需要定义其隐式变量就可以实现排序:

println(maxListOrdering(List(1, 5, 10, 3)))

在上面代码中有一行用到了 ordering 参数:

if (ordering.gt(x, maxRest)) x

这里 ordering 就是 Ordering[T] 的一个对象,事实上 Scala 标准库中提供了一个方法让编译器可以自己寻找到一个类的隐式变量:

def implicitly[T](implicit t: T) = t

例如一个类型 Foo,调用implicitly[Foo]会产生什么结果呢?首先编译器会寻找 Foo 的隐式对象,找到以后调用这个对象的 implicitly 方法,然后返回该对象,所以通过implicitly[Foo]可以返回 Foo 的隐式对象,对应到我们上面举的例子,ordering 是 Ordering[T] 的一个隐式对象,事实上我们也可以通过 implicitly[Ordering[T]]来表示

if (implicitly[Ordering[T]].gt(x, maxRest)) x

那么现在看来,ordering 既然可以用 implicitly[Ordering[T]] 来代替,那么也就可以省略掉这个参数名了,所谓的上下文绑定就是做这个事情的,语法是 [T: Ordering],它主要做了两件事:第一,引入了一个参数类型 T;第二,增加了一个隐式参数 Ordering[T]。
与之很类似的一个语法是[T <: Ordering[T]],这个的意思是 T 就是一个 Ordering[T]。
现在再来看一下引入上下文绑定后的排序代码:




// context bound def maxListOrdering[T: Ordering](elements: List[T]) (implicit ordering: Ordering[T]): T = elements match { case List() => throw new IllegalArgumentException("empty lists!") case List(x) => x case x :: rest => val maxRest = maxListOrdering(rest) if (implicitly[Ordering[T]].gt(x, maxRest)) x else maxRest }

总结

implicit 使用起来还是很灵活的,可以将 implicit 关键字标记在变量、函数、类或者对象的定义中。而且记住想要用到隐式,一定要先使用 import 引入 implicit 代码(当然在同一个代码域例如同一个对象下面不需要)。但是也需要注意,如果太过于频繁地使用 implicit,代码可读性就会很低,所以在使用隐式转换之前,先看一看能否用其他方式例如继承、重载来实现,如果都不行并且代码仍然很冗余,那么就可以试试用 implicit 来解决。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/211595.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午10:03
下一篇 2026年3月18日 下午10:04


相关推荐

  • 智能体并发处理机制:AI Agents for Beginners并行计算技术

    智能体并发处理机制:AI Agents for Beginners并行计算技术

    2026年3月16日
    3
  • IDEA 2022 怎么激活mybatiscodehelperpro-激活码分享

    (IDEA 2022 怎么激活mybatiscodehelperpro)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月31日
    847
  • 语义分割的发展前景和概述[通俗易懂]

    语义分割的发展前景和概述[通俗易懂]感谢感谢!收藏用~原文出自:http://blog.geohey.com/ji-suan-ji-shi-jue-zhi-yu-yi-fen-ge/计算机视觉之语义分割2017年10月11日人工智能被认为是第四次工业革命,google,facebook等全球顶尖、最有影响力的技术公司都将目光转向AI,虽然免不了存在泡沫,被部分媒体夸大宣传,神经网络在图像识别,语音识别,自然语言处理,无人车等方面的贡…

    2022年8月21日
    7
  • Coturn配置

    Coturn配置原文链接 http blog csdn net u0 article details coturn 服务器下载 https github com coturn coturn 由三个地方需要修改 nbsp 1 vim etc default coturn 把上面打开编辑的文件中的这一行 TURNSERVER ENABLED 1 去掉

    2026年3月20日
    2
  • C语言为什么被人们称为表达式语言_c语言中’0’是什么意思

    C语言为什么被人们称为表达式语言_c语言中’0’是什么意思今天无意中敲下:#includeintmain(){printf(“~0==%d\n”,~0);}输出结果是~0==-1;为什么呢?我个人的大概理解如下(不保证对错):以下假设为32位系统;0的补码是0x00000000;~0则是:0xFFFFFFFF(~是按位取反,包括不好位,跟“取反”不是一个概念)0xFFFFFFFF的原码是0

    2026年2月4日
    8
  • java判断字符串是否为空的方法总结

    方法一:本人推荐的方法,开发中最常用的方法,看起来也比较高大上:  if(StringUtils.isNotBlank(str))//判断字符串不为空  或if(StringUtils.isBlank(str))//判断字符串为空12方法二:比价简单直接的方法  if(s==null||"".equals(s));1方法三:比较字符串长度,效率高,比较绕:…

    2022年4月3日
    50

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号