函数和闭包

Posted 百里琰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数和闭包相关的知识,希望对你有一定的参考价值。

scala的函数式头等函数,你不仅可以定义和调用函数,还可以把它们写成匿名的字面量,并把它们作为值传递。scala函数字面量的语法:(x: Int, y: Int) => x + y。函数字面量被编译进类,并在运行期实例化为函数值。因此,函数字面量和函数值的区别在于函数字面量存在于源代码,而函数值作为对象存在于运行期。这个区别很想类(源代码)和对象(运行期)之间的关系。

函数值是对象,所以如果愿意,可以将其存入变量。它们也是函数,所以你可以使用通常的的括号函数调用写法调用它们,如代码1-1

代码1-1

scala> var increase = (x: Int) => x + 1
increase: Int => Int = <function1>

scala> increase(1)
res0: Int = 2

scala> increase = (x: Int) => x + 99
increase: Int => Int = <function1>

scala> increase(100)
res1: Int = 199

 如果想让函数字面量包含多条语句,可以用花括号包住函数体,如代码1-2

代码1-2

scala> increase = (x: Int) => {
      println("hello world")
      println("hello Scala")
      x + 99
    }
increase: Int => Int = <function1>

scala> increase(100)
hello world
hello Scala
res2: Int = 199

 

占位符

  如果想让函数字面量更简洁,可以把下划线当做一个或更多参数的占位符,只要每个参数在函数字面量内仅出现一次(代码1-4处会再解释),如代码1-3

代码1-3

scala> val arr = Array(-1, 10, 8, -9, 20)
arr: Array[Int] = Array(-1, 10, 8, -9, 20)

scala> arr.foreach(x => println(x))
-1
10
8
-9
20

scala> arr.filter(x => x > 0)
res1: Array[Int] = Array(10, 8, 20)

scala> arr.foreach(println _)
-1
10
8
-9
20

scala> arr.filter(_ > 0)
res3: Array[Int] = Array(10, 8, 20)

 可以看到,我们将字面量x => println(x)、字面量println _和字面量x => x > 0、字面量_ > 0分别传入数组arr的foreach和filter方法中,得到的结果是一致的。可以把下划线看做表达式里需要“填入”的“空白”。这个空白在每次函数被调用的时候用函数的参数填入。以_ > 为例,filter方法会把_ > 0里的下划线先用-1替换,就如-1 > 0,再用10替换,如 10 > 0,一直替换到最后一个值。

下划线还可以当成参数占位符,但是要注明参数的类型,如代码1-4

代码1-4

scala> def add = (_: Int) + (_: Int)
add: (Int, Int) => Int

scala> add(1, 2)
res6: Int = 3

 _ + _将扩展成带两个参数的函数字面量,这样也解释为何仅当每个参数在函数字面量中最多出现一次,才可以使用这种格式,多个下划线代表多个参数,而不是单个参数的重复使用,第一个下划线代表第一个参数,第二个下划线代表第二个参数,第三个下划线代表第三个参数……以此类推。

尽管前面的例子里下划线替代的只是单个参数,但有时候我们可以用单个下划线替换整个参数列表,我们称之为部分应用函数,如代码1-5。 代码1-5中,下划线不再是单个参数的占位符,它是整个参数列表的占位符,占位符和函数名之间要留有一个空格。

代码1-5

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> val a = sum _
a: (Int, Int, Int) => Int = <function3>

scala> a(1, 2, 3)
res7: Int = 6

scala> a.apply(1, 2, 3)
res8: Int = 6

scala> val b = sum(1, _: Int, 3)
b: Int => Int = <function1>

scala> b(2)
res9: Int = 6

部分应用函数时一种表达式,你不需要提供函数需要的所有参数。如变量a指向的函数值对象依旧需要传入3个参数,而变量b指向的函数值对象,仅仅传入第二个参数就足够了。函数值是由scala编译器照部分应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法,scala编译器会把表达式a(1, 2, 3)翻译成a.apply(1, 2, 3),即函数值的apply方法的调用,而apply调用了sum(1, 2, 3),并返回了6,同理,b(2)也是先转化为对apply方法的调用,而apply方法再调用sum(1, 2, 3)。

 闭包

  到这里为止,所有函数字面量的例子仅参考了传入的参数,例如(x: Int) => x + 1,函数体x + 1用到的唯一的变量x,被定义为函数参数,然而也可以参考定义在其他地方的变量:

(x: Int) => x + more

函数把more加入参数,从函数上来看,more是个自由变量,因为函数字面量自身没有给出关于more的含义。相对地,x变量是一个绑定变量,因为它在函数的上下文中有明确意义:被定义为函数的唯一参数时Int,因此,如果想要使上面的代码正常运行,需要如代码1-6这样,预先定义more,否则编译器便会报错

代码1-6

scala> val addMore = (x: Int) => x + more
addMore: Int => Int = <function1>

scala> addMore(10)
res10: Int = 20

scala> more = 50
more: Int = 50

scala> addMore(10)
res11: Int = 60

 依照这个函数字面量在运行时创建的函数值对象称为闭包。名称来源自通过捕获自由变量的绑定,从而对函数字面量执行“关闭”行动。不带自由变量的函数字面量,如(x: Int) => x + 1被称为封闭项,这里的项指的是一小部分源码。因此依照这个函数字面量在运行时创建的函数值严格意义上来讲就不是闭包,因为(x: Int) => x + 1在编写的时候就已经封闭了。但任何带有自由变量的函数字面量,如(x: Int) => x + more,都是开放项。因此,任何以(x: Int) => x + more为模板在运行期间创建的函数值将必须捕获对自由变量more的绑定。因此得到的函数值将包含指向捕获的more变量的索引。又由于函数值是关闭这个开放项(x: Int) => x + more的行动的最终产物,因此称为闭包。

scala的闭包捕获的是变量的本身,而不是变量指向的值,因此,在闭包内对捕获变量做出的改变,在闭包外一样可见,如代码1-7

scala> var sum = 0
sum: Int = 0

scala> val someNumbers = Array(1, 2, 3, 4, 5)
someNumbers: Array[Int] = Array(1, 2, 3, 4, 5)

scala> someNumbers.foreach(sum += _)

scala> sum
res5: Int = 15

 如果闭包访问了某些在程序运行时有若干不同备份的变量,例如闭包使用了某个函数的本地变量,如代码1-8

代码1-8

scala> def makeIncreaser(more: Int) = (x: Int) => x + more
makeIncreaser: (more: Int)Int => Int

scala> val inc1 = makeIncreaser(1)
inc1: Int => Int = <function1>

scala> val inc999 = makeIncreaser(999)
inc999: Int => Int = <function1>

scala> inc1(9)
res12: Int = 10

scala> inc999(1)
res13: Int = 1000

 每次函数inc1和inc999被调用时,都会创建一个新闭包。每个闭包都会访问闭包创建时活跃的more变量,调用makeIncreaser(1)时,捕获值1当做more的绑定的闭包被创建并返回,同理makeIncreaser(999)也一样。

重复参数

scala中,你可以指定函数的最后一个参数时重复的,从而允许用户向函数传入可变长度参数列表,想要标明一个重复参数,可在参数的类型之后放一个星号,如果用户有一个适合类型的数组,并尝试把它当做重复参数传入,需要在数组参数后添加一个号码和一个_*符号,这个标注告诉编译器把数组的每个元素当做参数,而不是当做单一的参数传给echo,如果不加标注,则会报错,如代码1-9

代码1-9

scala> def echo(args: String*) = args.foreach(println)
echo: (args: String*)Unit

scala> echo("Scala", "Java", "Python")
Scala
Java
Python

scala> val arrs = Array("Scala", "Java", "Python")
arrs: Array[String] = Array(Scala, Java, Python)

scala> echo(arrs)
<console>:10: error: type mismatch;
 found   : Array[String]
 required: String
              echo(arrs)
                   ^

scala> echo(arrs: _*)
Scala
Java
Python

 

 

 

 

 

 

以上是关于函数和闭包的主要内容,如果未能解决你的问题,请参考以下文章

JS---闭包

javascript 匿名函数及闭包----转载

Python嵌套函数和闭包

CSIC_716_20191112闭包函数和装饰器

Go语言-make陷阱和闭包函数

Swift函数和闭包