返回介绍

数学基础

统计学习

深度学习

工具

Scala

一、函数定义

发布于 2023-07-17 23:38:23 字数 19634 浏览 0 评论 0 收藏 0

  1. 函数定义:以def 开始,然后是函数名,然后是圆括号()和圆括号包围的、以逗号分隔的参数列表,然后是冒号:,然后是返回类型,然后是等号=,最后是花括号{}包围的函数体。

    
    def max(x:Int,y:Int): Int = {
        if (x>y) x
        else y
    }
    • 参数列表中的每个参数都必须加上冒号: 开始的类型标注,因为编译器无法推断函数参数的类型。

      如果函数不接收任何参数,则参数列表为空,此时使用空的圆括号(),表示不接收任何参数。

    • 圆括号包围的参数列表之后是返回结果的类型标注。

      如果函数没有返回结果,则结果类型标注为:UnitUnit 结果类型类似与Javavoid 类型,表示函数并不会返回任何有实际意义的结果。

    • 函数体之前的等号也有特别的含义,表示在函数式编程中,函数定义是一个表达式。

  2. 一个返回结果类型为Unit 的函数通常带有副作用(如:修改某个变量,或者输入/输出),这样的函数也被称作过程procedure

    • 每个有用的 preocedure 都必须有某种形式的副作用,否则它对于外部世界没有任何价值。
    • 函数式编程倾向于使用没有副作用的函数,这鼓励你设计的代码副作用尽可能小。其好处是:更容易测试。
  3. 一旦定义好函数,则可以通过按函数名来调用:max(3,5) 。函数定义中的参数列表x,y 称作形参,函数调用中的值3,5 称作实参。

  4. Scala 函数的参数都是val 而不是var,这意味着不能在函数体内部对入参重新赋值:

    
    
    xxxxxxxxxx
    def add(b:Byte):Unit ={ // b = 1; 错误:b 是一个val println(b) }
  5. 在没有任何显式的return 语句时,Scala 函数返回的是该函数计算出的最后一个表达式的值。

    Scala 推荐的风格是:尽量避免使用任何显式的return 语句,尤其是多个return 语句。这样可以将每个函数视作一个最终提交某个值的表达式。这种哲学鼓励你编写短小的函数,将大的函数拆解成小的函数。

    • 当一个函数只会计算一个返回结果的表达式时,可以不写花括号。如果这个表达式很短,它甚至可以被放置在def 的同一行。

    • 为了极致的精简,还可以省略掉结果类型,Scala 会帮你推断出来。

      但是为了代码的可读性,推荐对类中声明的公有方法(它是函数的一种)显式的给出结果类型。

1.1 定义缩略

  1. 有时候没必要采用完整的函数定义,Scala 编译器可以帮助我们做推断。

  2. 如果函数的返回结果的类型是已知的,则函数定义中可以省略掉结果类型部分。

    • 如果函数是递归的,则必须显式给出函数的结果类型。
    • 通常建议显式的给出函数的结果类型,虽然编译器不会强求,但是这会让代码更容易阅读。
  3. 如果函数体只有一条语句,则可以省略掉花括号{}

    如:

    
    
    xxxxxxxxxx
    def max(x:Int,y:Int) = if (x>y) x else y
  4. 如果可以推断参数类型信息,则可以省略参数类型声明。

    
    
    xxxxxxxxxx
    val intList = List[Int]() intList.filter((x) => x > 0 )

    其中intList 是一个整数列表。由于Scala 编译器知道x 必定是个整数,因此(x:Int) 可以简写为(x)

    这被称作目标类型target typing,因为一个表达式的目标使用场景会影响该表达式的类型。该机制原理细节不必掌握,你可以不需指定任何参数类型,直接使用函数字面量,当编译器报错时再添加类型声明。

  5. 如果省略了参数类型,则参数两侧的圆括号也可以省略。

    
    
    xxxxxxxxxx
    intList.filter(x => x > 0 )
  6. 为了让函数字面量更精简,还可以使用下划线作为占位符,用于表示一个或者多个参数,只需要满足每个参数只在函数字面量中只出现一次即可。

    
    
    xxxxxxxxxx
    intList.filter(_ > 0 )

    可以将下划线当作是表达式中的需要被“填”的“空”。函数每次被调用时,该“空”会被入参填上。

    事实上,当 _ 表示一个参数,且该参数恰好也是一个函数时,其语法会比较怪异:

    
    
    xxxxxxxxxx
    actionList.foreach(_ ())

    其中 actionList 是一个列表,列表中每个元素都是一个函数。这些函数的参数列表都为空。因此上式等于:

    
    
    xxxxxxxxxx
    actionList.foreach(f => f())
  7. 有时候使用_ 作为参数占位符时,编译器可能没有足够多的信息来推断缺失的参数类型。此时可以通过冒号来显式给出参数类型。

    
    
    xxxxxxxxxx
    val f = _ + _ // 错误:无法知道参数类型 val f = (_:Int) + (_:Int) // 正确,知道参数类型

    Scala 编译器将 _ + _ 展开为一个接收两个参数的函数字面量。

    多个下划线意味着多个参数,而不是一个参数的多次使用:第一个下划线代表第一个参数,第二个下划线代表第二个参数,依次类推。

  8. 下划线_ 不仅可以替换掉单个参数,它甚至可以替换掉整个参数列表。如:

    
    
    xxxxxxxxxx
    intList.foreach(println _)

    这等价于intList.foreach(x => println(x)) 。这里的下划线_ 是整个参数列表的占位符。注意:需要保留println_ 之间的空格。

    这种语法是部分应用函数partially applied function 。在Scala 中,当你调用某个函数,传入任何需要的参数时,你实际上是 apply 那个函数到这些参数上。

1.2 部分应用函数

  1. 部分应用函数是一个表达式,在这个表达式中,并不需要函数给出所有的参数,而是部分给出甚至完全不给出。如:

    
    
    xxxxxxxxxx
    def sum(a:Int,b:Int,c:Int) = a + b +c val i = sum(1,2,3) // 函数调用 val a = sum _ // partially applied function println(a(1,2,3)) // 输出:6
  2. 部分应用函数将返回一个新的函数。背后的原理是:

    • Scala 编译器根据 partially applied function 创建一个类,并实例化一个值函数,并将这个新的值函数的引用赋值给变量 a

      该类有一个接收 3 个参数的apply 方法,这是因为表达式sum _ 缺失的参数个数为 3

    • 然后编译器将表达式 a(1,2,3) 翻译成对值函数apply 方法的调用:a.apply(1,2,3)

      这个apply 方法只是简单的将三个缺失的参数转发给sum ,然后返回结果。

  3. 你也可以通过给出一些实参来表达一个部分应用的函数。如:

    
    
    xxxxxxxxxx
    val a = sum(1,_:Int,3) // partially applied function

    由于只有一个参数缺失,因此Scala 编译器生成的新的函数类,这个函数类的apply 方法接收一个参数。

  4. 如果你的部分应用函数表达式并不给出任何参数,则可以简化成:sum _,甚至连下划线也不用写。

    如果什么都不写,则要求在明确需要函数的地方,否则会引起编译错误:

    
    
    xxxxxxxxxx
    intList.foreach(println) // foreach 的参数要求是个函数

1.3 可变长度参数列表

  1. Scala 允许你标识出函数的最后一个参数可以被重复,这使得调用方可以传入一个可变长度的参数列表。

    可变长度参数列表的语法为:在参数的类型之后加上一个星号 *

    
    
    xxxxxxxxxx
    def echo(args : String*) = for (arg <- args) println(arg)

    在函数内部,该重复参数的类型是一个所声明的参数类型的 Array。因此在echo 内部,args 的类型其实是 Array[String]

    • 如果你有一个Array[String] 变量,则它不能作为echo 的实参。因为echo 的参数必须都是String 类型。

    • 你可以为数组实参arr的后面加上一个冒号和一个 _* 符号,这种表示法告诉编译器:将数组实参arr的每个元素作为参数传给函数,而不是将数组实参arr作为单个参数传入。

      
      
      xxxxxxxxxx
      def echo(args : String*) = for (arg <- args) println(arg) val arr = Array("Hello","World") echo(arr) // 编译失败,参数不匹配 echo(arr : _*) // OK

1.4 带名字的参数

  1. 在一个普通的函数调用中,实参是根据被调用的函数的参数定义,逐一匹配起来的。Scala 支持带名字的参数,使得调用方可以用不同的顺序将实参传给函数。其语法是:在每个实参之前加上参数名和等号。

    
    
    xxxxxxxxxx
    def getArea(width:Float,height:Float) = width * height println(getArea(3.0,4.0)) // width = 3.0, height = 4.0 println(getArea(height=4.0,width=3.0)) // width = 3.0, height = 4.0
  2. 通过带名字的参数,实参可以在不改变含义的情况下交换位置。

  3. 可以混合使用按位置的参数和带名字的参数,此时按位置的参数需要放在前面。

  4. 带名字的参数最常用的场合是跟默认参数一起使用。

1.5 默认参数

  1. Scala 允许你给函数参数指定默认值。这些带有默认值的参数可以不出现在函数调用中,此时这些参数被填充为默认值。其语法是:在函数定义时,形参类型之后使用 = 默认值 的格式。

    
    
    xxxxxxxxxx
    def printTime(out: java.io.PrintStream = Console.out) = { out.println("time = " + System.currentTimeMillis()) }

1.6 高阶函数

  1. 高阶函数higher-order function:接收函数作为参数的函数。

    
    
    xxxxxxxxxx
    def filesMatching(query : String, matcher : (String,String) => Boolean) = { // 这里是函数定义 }

    其中形参matcher 是一个函数,因此类型声明中有个=> 符号。这个函数接收两个字符串类型的参数并返回一个布尔值。

  2. 高阶函数的优势:

    • 高阶函数用于创建减少重复代码的控制抽象。

      如:返回指定模式的文件名:

      
      
      xxxxxxxxxx
      def filesMatching(query : String, matcher : (String,String) => Boolean) = { // 这里是函数定义 }

      这种方式避免了重复定义一些函数:

      
      
      xxxxxxxxxx
      def files_begin(query : String)= { // 这里是函数定义 } def files_end(query : String)= { // 这里是函数定义 } ...
    • API 中采用高阶函数,从而使得调用代码更精简。

      如:列表的exists 方法就是高阶函数:

      
      
      xxxxxxxxxx
      val has_neg = intList.exists(_ < 0) // 列表是否包含负数

      由于existsScala collection API 中的公共函数,因此它减少了API 使用方的代码重复。大量的循环逻辑都被抽象到了 exists 方法里了。

1.7 柯里化curring

  1. 常规的函数定义包含一个参数列表,而一个柯里化的函数定义包含多个参数列表。

    柯里化函数调用时,每个参数列表都需要提供对应的实参。

    ​x
    def regular_f(x:Int,y:Int,z:Int) = x + y + z    // 常规函数定义
    def currying_f(x:Int)(y:Int)(z:Int) = x + y + z // 柯里化函数定义
    ​
    println(regular_f(1,2,3))    // 常规函数调用
    println(currying_f(1)(2)(3)) // 柯里化函数调用

    当调用currying_f 时,实际上是连续进行了三次传统的函数调用:

    • 第一次调用接收了一个名为xInt 参数。
    • 第二次调用接收了一个名为yInt 参数。
    • 第三次调用接收了一个名为zInt 参数。
  2. 可以通过占位符表示法来获取柯里化函数内部的“传统函数”的引用。

    
    
    xxxxxxxxxx
    def f(x:Int)(y:Int)(z:Int) = x + y + z // 柯里化函数定义
    • 无法通过下面的方式获取,会引发编译失败:

      
      
      xxxxxxxxxx
      val f1 = f(1) // 编译失败 val f2 = f(1)(2) // 编译失败
    • 可以通过括号外的_ 来获取。

      
      
      xxxxxxxxxx
      val f1 = f(1)_ // 指向一个函数的引用,类型:Int => Int => Int // (参数为 Int,返回值为一个函数) val f2 = f(1)(2)_ // 指向一个函数的引用,类型:Int => Int

      理论上_ 之前要放置空格,但是由于这里 _ 之前是圆括号,因此不必放置空格。如println _ 就必须放置空格,因为println_ 是另一个合法的标识符。

    • 也可以通过括号内的_ 来获取,但是此时需要给定参数类型。

      
      
      xxxxxxxxxx
      val f1 = f(_:Int)(_:Int)(_:Int) // 指向一个函数的引用,类型:(Int, Int, Int) => Int val f2 = f(1)(_:Int)(_:Int) // 指向一个函数的引用,类型:(Int, Int) => Int val f3 = f(1)(2)(_) // 指向一个函数的引用,类型:Int => Int

1.8 传名参数

  1. 当函数的参数是另一个函数,且该参数函数的参数列表为空时,参数函数可以退化为传名参数by-name parameter

    
    
    xxxxxxxxxx
    def filter_line1(file:File,op:() => Boolean) = { // 函数体 } def filter_line2(file:File,op: => Boolean) = { // 退化为传名参数 // 函数体 }

    传名参数是一个以 => 开头的类型声明的参数,而不是 () => 开头。

  2. 传名参数比常规的参数函数的优点在于:调用时可以去掉空的参数列表。

    
    
    xxxxxxxxxx
    filter_line1(new File("data.txt"),() => 5>3 ) // 常规调用,必须带上空的参数列表 filter_line2(new File("data.txt"), 5>3 ) // 传名参数调用,不用带上空的参数列表
  3. 传名参数是相对于传值参数by-value parameter来说的。

    
    
    xxxxxxxxxx
    def filter_line3(file:File,op: Boolean) = { // 传值参数 // 函数体 } filter_line3(new File("data.txt"), 5>3 ) // 传值参数调用
    • 传名参数和传值参数在调用时完全相同;在定义时,传名参数的类型声明多了一个 =>

    • 传名参数本质上是一个值函数对象,因此5>3 转换成一个返回 Boolean 值的值函数。

      因此:

      • 对于传值参数,表达式5>3 首先被求值。

      • 对于传名参数,表达式5>3 并不会立即求值,而是在这个值函数对象apply 方法被应用时才会求值。

        如果该值函数的apply 方法从未被调用,则表达式5>3 始终不会被求值。

  4. 传名参数和传值参数的典型比较:

    
    
    xxxxxxxxxx
    def assert_by_p_name(enable_assert:Boolean,predicate: => Boolean)={ if (enable_assert && ! predicate()) throw new AssertionError } // 传名参数 ​ def assert_by_p_value(enable_assert:Boolean,predicate: Boolean)={ if (enable_assert && ! predicate) throw new AssertionError } // 传值参数 ​ assert_by_p_name(false,5/0 == 0 ) // 正常执行,因为 5/0 不会被求值 assert_by_p_value(false,5/0 == 0 ) // 抛出异常,因为 5/0 被求值了

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文