Scala的函数(1) :函数与方法【原创】

建议:阅读此文需要Java8及更高版本的基础

问题

Scala中,通常函数(Function)指的是一段代码逻辑的集合,可以有输入和输出,通常在开发中用来封装一段逻辑或者功能。方法(Method)是函数的特殊形式,指的是类或者对象中定义的函数成员。体现在Scala的语法层面就是val关键字定义的是函数,def关键字定义的是方法。那么问题来了,在Scala中,valdef定义的“Function”有啥区别?

val isEvenVal = (i: Int) => i % 2 == 0

def isEvenDef(i: Int) = i % 2 == 0

注意:下文没有特殊说明时,Function指的是val定义的函数,Methoddef定义的方法。

区别

Method在每次被调用时先被实例化成“Function”(new function)再进行计算。Function则在定义时计算。

val evaluates when defined, def when called:

举个例子

val test1: () => Int = {
    val r = util.Random.nextInt
    () => r
}
// 运行 val test1
test1() //-761561683
test1() //-761561683
test1() //-761561683

def test2: () => Int = {
    val r = util.Random.nextInt
    () => r
}
// 运行 def test2
test2() //1228034657
test2() //1312215631
test2() //310909645

test1的返回结果每次执行的的输出都是固定的,说明在test1被定义的时候,结果已经固定了。而test2返回的结果每次执行后的输出都是不一样的,说明被调用时才被实际执行。

说到这里有牵扯出了两个细节问题,第一个是闭包,也就是test1的现象。第二个是Method可以被转换成Function来进行调用或者传递。关于第二个问题简单提一嘴,其实利用Scala的占位句法,很容易手动把Method转成Function,比如下面的例子:

// def 定义方法addOne
def addOne(a: Int): Int = a + 1

// 转成Function,然后传值调用
val addOneFunc = addOne _
addOneFunc(5) // 6

后续的文章会详细进行讨论函数的各种使用。

探索

def

为什么会有上面提到的现象,造成的原因是什么?下面我们来简单进行下探索。Scala也是jvm方言,所以我们可以通过把Scala的二进制文件反编译成Java来观察,就可以很容易的得出结论。

先定义一个类DesTest,类里面有个方法add1,如下:

class DefTest {
    def add1(a: Int) = a + 1
}

然后使用scalac,编译并输出:

$ scalac -Xprint:all DefTest.scala

package <empty> {
  class DefTest extends Object {
    def add1(a: Int): Int = a.+(1);  //<-- 定义的方法在这里
    def <init>(): DefTest = {
      DefTest.super.<init>();
      ()
    }
  }
}

然后再使用javap对编译出的class文件输出其结构:

$ javap DefTest

Compiled from "DefTest.scala"
public class DefTest {
    public int add1(int);   //<-- 定义的方法在这里
    public DefTest();
}

简单观察其实与平时的java类和方法并无大的差别。再使用jad进行反编译,输出详细的java代码如下。可以看到,与我们平时写的java代码并无区别,也就是说,使用def关键字定义的方法在java中就是常规的方法。

public class DefTest {
    public int add1(int a) {
        return a + 1;
    }
    public DefTest() {}
}

val

先定义一个类ValTest,里面继续包含一个方法add1,使用val关键字申明。

class ValTest {
    val add1 = (a: Int) => a + 1
}

同样使用scalac编译并输出:

ValTest.class
ValTest$$anonfun$1.class

这里已经和上面有所不同了,我们继续使用javap来观察:

$ javap ValTest

Compiled from "ValTest.scala"
public class ValTest {
    public scala.Function1<java.lang.Object, java.lang.Object> add1();
    public ValTest();
}

可以看到,此时add1在java中还是一个方法,但是返回值却上刚才有了很大的不同。变成了:scala.Function1<java.lang.Object, java.lang.Object>,对应的是scala的Function1特质

关于特质,可以简单理解为一种特殊的接口

最后,来看下反编译的代码:

import scala.Function1;
import scala.Serializable;
import scala.runtime.BoxesRunTime;

public class ValTest {

    public Function1 add1() {
        return add1;
    }

    public ValTest() {}

    private final Function1 add1 = new Serializable() {

        public final int apply(int a) {
            return apply$mcII$sp(a);
        }

        public int apply$mcII$sp(int a) {
            return a + 1;
        }

        public final volatile Object apply(Object v1) {
            return BoxesRunTime.boxToInteger(apply(BoxesRunTime.unboxToInt(v1))); 
        }

        public static final long serialVersionUID = 0L;

    };
}

可以看到:

  • ValTest中也有一个方法add1,返回Function1的实例
  • 同时又有一个私有的实例字段add1,对应的为scala.Serializable接口的匿名实现。
  • 而实例add1apply方法中,调用了apply$mcII$sp方法,该方法中会包含scala源码中add1方法的具体实现。
  • 最下面,又有一个apply的重载形式,为实际调用的具体入口,包含了scala的自动拆箱和装箱

结论

到这里就基本都清楚了,def在类中定义的Method对应的就是java中的方法,而val定义的则对应的是scala.Serializable接口实现,每次具体调用是都调用的是该实现的入口方法apply。所以,在val定义的add1可以在scala中理解为一个字段(Field)。所以这也就解释了为什么val在定义时计算,因为已经被实例化了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。