印度朋友手把手教你学Scala(3):方法


/**
 * 谨献给我最爱的YoYo
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-3-methods/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-02-25
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

Scala里的方法

自从2015年12月写了最后一篇关于Scala的文章后,到现在已经有很长一段时间了。希望我能写多一点。^_^

这是关于Scala系列教程的第三章。点击这里可查看整个系列。

这篇文章是关于怎么处理Scala里的方法。大部分都是关于风格,但当我们进入函数式编程的世界前这是很重要的。

目录

  • Primer
  • 方法的语法
  • Unit类型
  • 不可变的方法参数
  • return关键字的注意事项
  • 异构返回类型(Heterogeneous return types)
  • 传值调用和传名调用
  • 方法参数的默认值
  • 剔除方法

Primer

回到那些汇编编程的日子,那时有一种叫做子例程的东西。

相同的理念演变成了今天什么是方法。一种把程序识别成做不同事情的小代码块的简单方式。

子例程,程序,函数,方法可能意味相同的东西,也可能不是,并且真的很难给出一个广义的区别,因为根据深思熟虑的编程语言有不同的含义。

在Scala,我们只关心函数(Function)和方法(Method)。

方法的语法

声明和使用方法有好几种方式。以下只是一些方便熟悉语法糖的示例。

  • 带两个参数并返回一个值的方法
  • 不带大括号的方法
  • 不带任何参数的方法
  • 既没参数又没返回值的方法
  • 返回了未指明类型的值的方法

带两个参数并返回一个值的方法

def max(x:Int, y:Int) : Int = {  
    if (x>y) x else y
}

以上是一个简单的方法,接收两个参数x和y,然后返回这两个之中较大的那个。

所有的方法以关键字def开头,接着的是方法名称。随后跟着可选的参数和返回类型。

x和y是怎么返回的?暂且把这个问题放一边,稍候我们会说到。

不带大括号的方法

和Java相似,方法声明(译者注:应该是方法实现)放在大括号里,但这是可选的。

def max(x:Int, y:Int):Int= if (x>y) x else y  

这纯粹是出于简单考虑,当方法很小时,我们可以忽略括号。但当方法很大时,最好是把代码块放在大括号里。

不带任何参数的方法

如前面所述,方法参数是可选的。

def getValueOfPi: Double = 3.14159  

注意,为了可读性我已经省略了小括号。也可以像下面这样为空参数。

def getValueOfPi (): Double = 3.14159  

既没参数又没返回值的方法

打印Hello World信息到控制台。

def printHelloMsg = println("Hello there !!!!")  

关于用这种方式来声明重要的一点是不能使用像printHelloMsg()这样来调用,它只能使用例如没有小括号的printHelloMsg来调用。

当带小括号调用时,意味着我们传递了一个空参数,而不是没有参数。

这被称为0元数方法(0-arity method),这点在方法调用文档里有解释。

返回了未指明类型的值的方法

def greetPerson(msg :String) = "Hello " + msg + "!!!"  

这会返回“Hello”后面加上你传入的任何字符串。

在所有这些例子里,不管哪里需要类型信息,Scala都使用了类型接口

要注意的一件事是如果省略了=符号,编译器会把它作为一个Unit类型方法,即使我们从那个方法返回了一些东西。

例如,

def whichIsGreater (a : Int , b: Int)  {

    if(a>b) a else b

}


在函数式编程术语里,一个不返回任何类型的方法,例如返回Unit,被称为过程(procedure)。这些方法没有副作用,并且状态独立。

Unit类型

如果在Scala REPL里运行以下示例,

def printHelloMsg = println("Hello there !!!!")  

可以看到Unit类型的方法。

在Java里,和上面方法等价的示例,如下面所示。

void printHelloMsg() {  
    System.out.println("Hello there !!!!");
}

如果我们调用了这个方法,没有东西会打印/返回。

val t = someMethod()  
println(t)  

编译器会抛出一个类似以下的警告。
warning: enclosing method someMethod has result type Unit: return value discarded

把返回类型赋值给一个变量会给到一个类似()这样的输出。可以从运行时boxed unit理解这点,而它会被Unit.scala unboxing所调用。这只是Unit类型的一个toString展示。

不可变的方法参数

默认地,传给方法的方法参数是不可变的,并且不能改变。以下图片展示了一个例子。

记住,Scala比Java更加信奉不可变性。等效的Java方法可能像是这样:

public void calculateInterest(final int interestAmount){  
        interestAmount = interestAmount % 10;
}

重写它而不改变方法参数是非常直截了当的,但之所以这样设计的原因,再一次是因为函数式编程、摆脱可变状态以及钟爱[不可变对象](http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html。

return关键字的注意事项

Scala编译器以这种的方式来构建,在缺少return语句里会取最后一条语句并返回它。

但是,在最后的语句不打算作为方法的输出的地主,那么结果可能会和我们期待的有所出入。

def whichIsGreater (a : Int , b: Int)  = {

    if (a>b) a else b

    // 这是故意的
    "Some String"

}

我们可以使用任何整型参数来调用这个方法,它都会返回Some String。记住,Scala类型接口有限,在这种特殊场景里这是它能想到的最好方式。

如果你使用IDE,你可以看到鼠标经过时会有个警告在这些变量上。

展开是:“高亮表达式,如果它没有副作用并且不会影响任何变量值或者函数的返回。可以删除或者转换成一个返回语句”。

这告诉我们这条语句没有副作用,因为在下面我们声明了一个字符串变量。

如果使用同样的代码,现在使用return关键字,编译器顿时就抱怨了。

这个错误的原因是,我们立即切断了执行流程,并且接着从那里返回。如果我们推断类型,它将涉及遍历整个调用堆栈,所以编译器采用一种更安全的方式使程序员明确注明返回类型。

这里是另外一个例子。

 def whichIsGreater (a : Int , b: Int)  = {

    if (a>b) a

  }

如果a大于b则返回a,否则在编译时会推断返回Unit类型。注意方法返回类型是AnyVal,因为else部分是Unit,而if部分是一个Int,这会提升到导次结构中可用的下一个可用的类型。

根据我们的分析,可以推断出以下几点。

  • 子类型用于提升类型顺序。
  • 如果使用return关键字,必须强制指明方法类型。
  • 最后一条语句会用来作为返回类型,如果给了一个错误的类型,编译器会识别不了。
  • 全部类型错误都在编译时,所以确保了完全类型安全。
  • 应该指明方法参数的类型。因为Scala使用本地类型接口,所以不能在编译时推断他们。

当用的是纯函数式编程,你根本都不需要用到return关键字。

避免return关键可能一开始看起来奇怪,但一旦我们学习了更多的函数式编程就习以为常了。

异构返回类型(Heterogeneous return types)

子类型使得我们可以做很多东西。其中一点就是可以通过异构的方式混合类型。

考虑以下示例。

def whichIsGreater (a : Int , b: Int)  = {

    if (a>b) a else "a is lesser"

}

此方法返回类型是Any,位于层次结构的顶级。如果你是一位Java程序员,你会很快就用instanceof来猜测返回类型。

使用instanceof是一个反模式(anti-pattern)并且被视为糟糕的编程实践。在Scala里你应该使用Either类型。但那是另外一篇博客的主题。

传值调用和传名调用

当正在调用方法时,理解编译器如何对待它是迫切的。在Scala,方法对待它的参数有两种方式。

  • 传值调用
  • 传名调用

在参数被求值前就减少的地方,传值调用是一种通常的策略,而传名调用直到它在代码里的某个地方被用到时才会对表达式进行求值。

def multiply(x : Int, y: Int) : Int ={  
    x * x
}

如果我们通过multiply(6 + 4,3)来调用这个方法,会发生以下事情。

注意:方法名称中的值描述了计算 ,而不是对实际方法的调用。

传值调用 -> multiply(10,3) -> multiply(10 * 10) -> 100
传名调用 -> multiply(6+4,3) -> multiply(10,3) -> multiply(10 * 10) -> 100

在传名调用,表达式6 + 4只有当在它进入了调用栈后才会被求值,而在传值调用里它在进入前求值。

调用也可以变成这样:multiply(10,4+3)

传值调用 -> multiply(10,7) -> multiply(10 * 10) -> 100
传名调用 -> multiply(10*10) -> 100

在这里,传名调用更短,因为第二个变量压根都不需要所以不用求值。

可以看到这两种策略都可以产出同样的结果。

Scala默认使用传值调用,因为在通常的使用场景里它比它的竞争对手要好, 但也支持传名调用如果强制的话。

以下是一个现实世界的例子。

object FunctionCallType extends App {


  println(callByValue(2 + 2))
  println(callByName(2 + 2))


  def callByValue(value: Int): Int = {
    value + 1

  }


  def callByName(value: => Int): Int = {
    value + 1
  }

}

=>用于描述这个变量应该传名调用。

如果使用IDE来调试,可以看到callByValue已经把变量value计算成4.

而callByName则不是。

如果执行的话,这两个例子都会得到同样的结果,区别是参数被调用的方式。

通过这个例子,可以更清晰明白关于变量是如何被求值的。

好了,但在什么场景我们需要传名调用,以及哪里我们需要传值调用?

这个问题需要更进一步对函数式编程的理解,函数式编程会在后面的文章中讲到。通常你会用传值调用,但有些场景用传名调用是一个更好的选择。

方法参数的默认值

在声明方法时,我们也可以指定参数的默认值,以防调用者什么也不提供。

  def isAllowedURl(url: String = "default"): String = {
      if(url.equals("default"))
        "No URL provided"
      else
        "Access allowed"
  }

在不提供参数值的情况下,默认值就会赋给String类型的url

剔除方法

一个普遍广泛使用的编程实践是把方法声明为Stubs,例如没有逻辑实现。人们选择这样做可能有几个原因,这取决于开发人员在合适的地方使用它。

在Java里,你可能通常使用java.lang.UnsupportedOperationException来添加被剔除的方法(stubbed methods),无名的一种类型,给定一个缺失的实现,表示不支持的操作。

Scala用???来暗示方法尚未被实现,如下面的声明。

// 被剔除的方法
def getID(): String = ???  

当调用这些方法,可以看到一个更清晰的异常调用栈。

Exception in thread "main" scala.NotImplementedError: an implementation is missing  
    at scala.Predef$.$qmark$qmark$qmark(Predef.scala:230)
    at Child.getAge(Child.scala:7)
    at Runnable$.delayedEndpoint$Runnable$1(Runnable.scala:8)
    at Runnable$delayedInit$body.apply(Runnable.scala:4)
    at scala.Function0$class.apply$mcV$sp(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App$$anonfun$main$1.apply(App.scala:76)
    at scala.App$$anonfun$main$1.apply(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:381)
    at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:35)
    at scala.App$class.main(App.scala:76)
    at Runnable$.main(Runnable.scala:4)
    at Runnable.main(Runnable.scala)

这里我所解释的大部分都是关于理解Scala面向对象的方面。以我的经验,最好先理解这方面,再进入函数式编程。

敬请关注我下一篇关于对象和类的文章。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州