文章正文

通过隐式转换,程序员可以在编写Scala程序时故意漏掉一些信息,让编译器去尝试在编译期间自动推导出这些信息来,这种特性可以极大的减少代码量,忽略那些冗长,过于细节的代码。

1、Spark 中的隐式思考

隐式转换是Scala的一大特性, 如果对其不是很了解, 在阅读Spark代码时候就会很迷糊,有人这样问过我?

RDD这个类没有reduceByKey,groupByKey等函数啊,并且RDD的子类也没有这些函数,但是好像PairRDDFunctions这个类里面好像有这些函数 为什么我可以在RDD调用这些函数呢?

答案就是Scala的隐式转换; 如果需要在RDD上调用这些函数,有两个前置条件需要满足:

  • 首先rdd必须是RDD[(K, V)], 即pairRDD类型
  • 需要在使用这些函数的前面Import org.apache.spark.SparkContext._;否则就会报函数不存在的错误;

参考SparkContext Object, 我们发现其中有上10个xxToXx类型的函数:

 implicit def intToIntWritable(i: Int) = new IntWritable(i)
implicit def longToLongWritable(l: Long) = new LongWritable(l)
implicit def floatToFloatWritable(f: Float) = new FloatWritable(f)
implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null) = {
new PairRDDFunctions(rdd)
}

这么一组函数就是隐式转换,其中rddToPairRDDFunctions,就是实现:隐式的将RDD[(K, V)]类型的rdd转换为PairRDDFunctions对象,从而可以在原始的rdd对象上 调用reduceByKey之类的函数;类型隐式转换是在需要的时候才会触发,如果我调用需要进行隐式转换的函数,隐式转换才会进行,否则还是传统的RDD类型的对象;

还说一个弱智的话,这个转换不是可逆的;除非你提供两个隐式转换函数; 这是你会说,为什么我执行reduceByKey以后,返回的还是一个rdd对象呢? 这是因为reduceByKey函数 是PairRDDFunctions类型上面的函数,但是该函数会返回一个rdd对象,从而在用户的角度无法感知到PairRDDFunctions对象的存在,从而精简了用户的认识, 不知晓原理的用户可以把reduceByKey,groupByKey等函数当着rdd本身的函数

上面是对spark中应用到隐式类型转换做了分析,下面我就隐式转换进行总结;

从一个简单例子出发,我们定义一个函数接受一个字符串参数,并进行输出

def func(msg:String) = println(msg)

这个函数在func("11")调用时候正常,但是在执行func(11)或func(1.1)时候就会报error: type mismatch的错误. 这个问题很好解决

  • 针对特定的参数类型, 重载多个func函数,这个不难, 传统JAVA中的思路, 但是需要定义多个函数
  • 使用超类型, 比如使用AnyVal, Any;这样的话比较麻烦,需要在函数中针对特定的逻辑做类型转化,从而进一步处理

上面两个方法使用的是传统JAVA思路,虽然都可以解决该问题,但是缺点是不够简洁;在充满了语法糖的Scala中, 针对类型转换提供了特有的implicit隐式转化的功能;

隐式转化是一个函数, 可以针对一个变量在需要的时候,自动的进行类型转换;针对上面的例子,我们可以定义intToString函数

implicit def intToString(i:Int)=i.toString

此时在调用func(11)时候, scala会自动针对11进行intToString函数的调用, 从而实现可以在func函数已有的类型上提供了新的类型支持,这里有几点要说一下:

  • 隐式转换的核心是from类型和to类型, 至于函数名称并不重要;上面我们取为intToString,只是为了直观, int2str的功能是一样的;隐式转换函数只关心from-to类型之间的匹配 比如我们需要to类型,但是提供了from类型,那么相应的implicit函数就会调用
  • 隐式转换只关心类型,所以如果同时定义两个隐式转换函数,from/to类型相同,但是函数名称不同,这个时候函数调用过程中如果需要进行类型转换,就会报ambiguous二义性的错误, 即不知道使用哪个隐式转换函数进行转换

上面我们看到的例子是将函数的参数从一个类型自动转换为一个类型的例子,在Scala中, 除了针对函数参数类型进行转换以外,还可以对函数的调用者的类型进行转换.

比如A+B,上面我们谈到是针对B进行类型自动转换, 其实可以在A上做类型转换,下面我们拿一个例子来说明

class IntWritable(_value:Int){
def value = _value
def +(that:IntWritable): IntWritable ={
new IntWritable(that.value + value)
}
}
implicit def intToWritable(int:Int)= new IntWritable(int)
new IntWritable(10) + 10

上面我们首先定义了一个类:IntWritable, 并为int提供了一个隐式类型转换intToWritable, 从而可以使得IntWritable的+函数在原先只接受IntWritable类型参数的基础上, 接受一个Int类型的变量进行运算,即new IntWritable(10) + 10可以正常运行

现在换一个角度将"new IntWritable(10) + 10" 换为"10 + new IntWritable(10)"会是什么结果呢?会报错误吗?

按道理是应该报错误,首先一个Int内置类型的+函数,没有IntWritable这个参数类型; 其次,我们没有针对IntWritable类型提供到Int的隐式转换, 即没有提供writableToInt的implicit函数.

但是结果是什么?10 + new IntWritable(10)的是可以正常运行的,而且整个表达的类型为IntWritable,而不是Int, 即Int的10被intToWritable函数隐式函数转换为IntWritable类型;

结论:隐式转换可以针对函数参数类型和函数对象进行类型转换; 现在问题来了,看下面的例子

implicit  def intToWritable(int:Int)= new IntWritable(int)
implicit def writableToInt(that:IntWritable)=that.value val result1 = new IntWritable(10) + 10
val result2 = 10 + new IntWritable(10)

在上面的IntWritable类的基础上,我们提供了两个隐式类型转换函数, 即Int和IntWritable之间的双向转换;这样的情况下result1和result2两个变量的类型是什么?

答案:result1的类型为IntWritable, result2的类型Int;很好理解, result1中的Int类型的10被intToWritable隐式转换为IntWritable;而result2中的IntWritable(10)被writableToInt 隐式转换为Int类型;

你肯定会问?result2中为什么不是像上面的例子一样, 把Int类型的10隐式转换为IntWritable类型呢?原因就是隐式转换的优先级;

发生类型不匹配的函数调用时, scala会尝试进行类型隐式转换;首先优先进行函数参数的类型转换,如果可以转换, 那么就完成函数的执行; 否则尝试去对函数调用对象的类型进行转换; 如果两个尝试都失败了,就会报方法不存在或者类型不匹配的错误;

OK, Scala的隐式转换是Scala里面随处可见的语法, 在Spark中也很重要, 这里对它的讲解,算是对Shuffle做一个补充了, 即一个RDD之所以可以进行基于Key的Shuffle操作 是因为RDD被隐式转换为PairRDDFunctions类型。

2、Scala 隐式使用方式

1.将方法或变量标记为implicit
2.将方法的参数列表标记为implicit
3.将类标记为implicit

Scala支持两种形式的隐式转换:
隐式值:用于给方法提供参数
隐式视图:用于类型间转换或使针对某类型的方法能调用成功

2.1 隐式值

例1:声明person方法。其参数为name,类型String

scala> def person(implicit name : String) = name   //name为隐式参数
person: (implicit name: String)String

直接调用person方法

scala> person
<console>:9: error: could not find implicit value for parameter name: String
person
^
报错!编译器说无法为参数name找到一个隐式值
定义一个隐式值后再调用person方法
scala> implicit val p = "mobin"   //p被称为隐式值
p: String = mobin
scala> person
res1: String = mobin
因为将p变量标记为implicit,所以编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺少参数。
但是如果此时你又在REPL中定义一个隐式变量,再次调用方法时就会报错
scala> implicit val p1 = "mobin1"
p1: String = mobin1
scala> person
<console>:11: error: ambiguous implicit values:
both value p of type => String
and value p1 of type => String
match expected type String
person
^

匹配失败,所以隐式转换必须满足无歧义规则,在声明隐式参数的类型是最好使用特别的或自定义的数据类型,不要使用Int,String这些常用类型,避免碰巧匹配

2.2 隐式视图

隐式转换为目标类型:把一种类型自动转换到另一种类型

例2:将整数转换成字符串类型:

scala> def foo(msg : String) = println(msg)
foo: (msg: String)Unit scala> foo(10)
<console>:11: error: type mismatch;
found : Int(10)
required: String
foo(10)
^

显然不能转换成功,解决办法就是定义一个转换函数给编译器将int自动转换成String

scala> implicit def intToString(x : Int) = x.toString
intToString: (x: Int)String scala> foo(10)
10

隐式转换调用类中本不存在的方法

例3:通过隐式转换,使对象能调用类中本不存在的方法

class SwingType{
def wantLearned(sw : String) = println("兔子已经学会了"+sw)
}
object swimming{
implicit def learningType(s : AminalType) = new SwingType
}
class AminalType
object AminalType extends App{
import com.mobin.scala.Scalaimplicit.swimming._
val rabbit = new AminalType
rabbit.wantLearned("breaststroke") //蛙泳
}
编译器在rabbit对象调用时发现对象上并没有wantLearned方法,此时编译器就会在作用域范围内查找能使其编译通过的隐式视图,找到learningType方法后,编译器通过隐式转换将对象转换成具有这个方法的对象,之后调用wantLearned方法
可以将隐式转换函数定义在伴生对象中,在使用时导入隐式视图到作用域中即可(如例4的learningType函数)
还可以将隐式转换函数定义在凶对象中,同样在使用时导入作用域即可,如例4
例4:
class SwingType{
def wantLearned(sw : String) = println("兔子已经学会了"+sw)
} package swimmingPage{
object swimming{
implicit def learningType(s : AminalType) = new SwingType //将转换函数定义在包中
}
}
class AminalType
object AminalType extends App{
import com.mobin.scala.Scalaimplicit.swimmingPage.swimming._ //使用时显示的导入
val rabbit = new AminalType
rabbit.wantLearned("breaststroke") //蛙泳
}

像intToString,learningType这类的方法就是隐式视图,通常为Int => String的视图,定义的格式如下:

implicit def originalToTarget (<argument> : OriginalType) : TargetType

其通常用在于以两种场合中:

1.如果表达式不符合编译器要求的类型,编译器就会在作用域范围内查找能够使之符合要求的隐式视图。如例2,当要传一个整数类型给要求是字符串类型参数的方法时,在作用域里就必须存在Int => String的隐式视图
2.给定一个选择e.t,如果e的类型里并没有成员t,则编译器会查找能应用到e类型并且返回类型包含成员t的隐式视图。如例3

2.3 隐式类

在scala2.10后提供了隐式类,可以使用implicit声明类,但是需要注意以下几点:
1.其所带的构造参数有且只能有一个
2.隐式类必须被定义在类,伴生对象和包对象里
3.隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
4.作用域内不能有与之相同名称的标示符

object Stringutils {
implicit class StringImprovement(val s : String){ //隐式类
def increment = s.map(x => (x +1).toChar)
}
}
object Main extends App{
import com.mobin.scala.implicitPackage.Stringutils._
println("mobin".increment)
}

编译器在mobin对象调用increment时发现对象上并没有increment方法,此时编译器就会在作用域范围内搜索隐式实体,发现有符合的隐式类可以用来转换成带有increment方法的StringImprovement类,最终调用increment方法。

3、Scala 隐私注意事项

3.1 转换时机

1.当方法中的参数的类型与目标类型不一致时
2.当对象调用类中不存在的方法或成员时,编译器会自动将对象进行隐式转换

3.2 解析机制

即编译器是如何查找到缺失信息的,解析具有以下两种规则:

1.首先会在当前代码作用域下查找隐式实体(隐式方法  隐式类 隐式对象)
2.如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找
类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T它的查找范围如下:
    (1)如果T被定义为T with A with B with C,那么A,B,C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索
    (2)如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如List[String]的隐式搜索会搜索List的
伴生对象和String的伴生对象
    (3) 如果T是一个单例类型p.T,即T是属于某个p对象内,那么这个p对象也会被搜索
    (4) 如果T是个类型注入S#T,那么S和T都会被搜索

3.3 转换前提

1.不存在二义性(如例1)

2.隐式操作不能嵌套使用,即一次编译只隐式转换一次(One-at-a-time Rule)

Scala不会把 x + y 转换成 convert1(convert2(x)) + y

3.代码能够在不使用隐式转换的前提下能编译通过,就不会进行隐式转换。

文章参考

  • https://github.com/ColZer/DigAndBuried/blob/master/spark/scala-implicit.md
  • https://blog.csdn.net/jameshadoop/article/details/52337949
  • https://www.cnblogs.com/MOBIN/p/5351900.html

Scala 隐式(implicit)详解的更多相关文章

  1. Scala进阶之路-Scala高级语法之隐式(implicit)详解

    Scala进阶之路-Scala高级语法之隐式(implicit)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 我们调用别人的框架,发现少了一些方法,需要添加,但是让别人为你一 ...

  2. Scala中的Implicit详解

    Scala中的implicit关键字对于我们初学者像是一个谜一样的存在,一边惊讶于代码的简洁, 一边像在迷宫里打转一样地去找隐式的代码,因此我们团队结合目前的开发工作,将implicit作为一个专题进 ...

  3. javascript隐式转换详解

    Javascript是web前端开发的必学技术,今天和大家分享的就是javascript的基础知识隐式转换,希望可以帮助大家更好的学习. 转换成布尔类型假 undefined->falSe nu ...

  4. JavaScript隐式类型转换(详解 +,-,*,/,==)

    JavaScript 在 运算 或 比较 之前, 会自动进行隐式类型转换. 下面我们来仔细讲一讲 + - * / == 运算符经历了哪些过程. 类型转换 ECMAScript 运行时系统会在需要时从事 ...

  5. scala 隐式转换

    先参考这篇文章:http://www.jianshu.com/p/a344914de895 package com.test.scalaw.test /** * scala隐式转换 */ object ...

  6. SpringMVC强大的数据绑定(2)——第六章 注解式控制器详解

    SpringMVC强大的数据绑定(2)——第六章 注解式控制器详解 博客分类: 跟开涛学SpringMVC   6.6.2.@RequestParam绑定单个请求参数值 @RequestParam用于 ...

  7. scala 隐式详解(implicit关键字)

    掌握implicit的用法是阅读spark源码的基础,也是学习scala其它的开源框架的关键,implicit 可分为: 隐式参数 隐式转换类型 隐式调用函数 1.隐式参数 当我们在定义方法时,可以把 ...

  8. Scala之隐式转换implicit详解

    假设我们有一个表示文本的行数的类LineNumber: class LineNumber ( val num : Int ) 我们可以用这个类来表示一本书中每一页的行数: val lineNumOfP ...

  9. Scala 隐式转换及应用

    什么是隐式转换 我们经常引入第三方库,但当我们想要扩展新功能的时候通常是很不方便的,因为我们不能直接修改其代码.scala提供了隐式转换机制和隐式参数帮我们解决诸如这样的问题. Scala中的隐式转换 ...

随机推荐

  1. C++ 中递归实现 二项式展开式(a+b)^ n 的表达式

    C++ 中递归实现 二项式展开式 的表达式 前几天,一个数学系读研的同学来问有什么软件可以来求 (a+b)^n 这种表达式类型的展开式,我随口一说了 Octave , 毕竟这个开源的还是可以的,后来他 ...

  2. checkbox jquery操作总结

    $('input[name="myCheckbox"]').prop('checked','true'); // 全选 $('input[name="myCheckbox ...

  3. BZOJ-2-4870: [Shoi2017]组合数问题 矩阵优化 DP

    就 是 要 我 们 从  n k  件 物 品 里 面 选 出 若 干 件,使 得 其 数 量 模 k 等 于 r 的 方 案 数 . dp方程 f [ i , j ] 表示前 i 件物品拿了若干件使 ...

  4. incomplete type is not allowed ofstream

    错误: incomplete type is not allowed 解决方案: #include<fstream>

  5. 要过年啦,用canvas做了个烟火效果

    声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢! 要过年了,过年想到的就是放烟火啦....于是就用canvas写了个放烟火的效果,鼠标点击也会产生烟火,不过不要产生太多烟火哦,一个烟火散出 ...

  6. java第七周动手动脑

    public class ParentChildTest { public static void main(String[] args) { Parent parent=new Parent(); ...

  7. 基于Python Pillow库生成随机验证码

    from PIL import Image from PIL import ImageDraw from PIL import ImageFont import random class ValidC ...

  8. oracle 分析函数 keep(dense_rank first/last)

    SQL : select * from crisis_sales where dept_id = 'D02' order by sale_date; DEPT_ID  SALE_DATE  GOODS ...

  9. JS_高程6.面向对象的程序设计(2)创建对象_3 构造函数存在的问题

    # 上次讲到用构造函数的模式来创建对象,相对于工厂模式,解决可对象识别的问题. function Person(name,age,job){ this.name=name; this.age=age; ...

  10. JS_高程5.引用类型(5)Array类型的操作方法

    一.操作方法 1.concat()方法 基于当前数组中的所有项创建一个新数组.具体说,是先创建当前数组的一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组.在没有给concat() ...