Scala 3 — 书籍

方法特性

语言

本部分介绍如何在 Scala 3 中定义和调用方法的各个方面。

定义方法

Scala 方法具有许多特性,包括这些

  • 泛型(类型)参数
  • 默认参数值
  • 多个参数组
  • 上下文提供的参数
  • 按名称传递的参数
  • 等等…

本部分演示了其中一些特性,但是当您定义不使用这些特性的“简单”方法时,语法如下所示

def methodName(param1: Type1, param2: Type2): ReturnType = {
  // the method body
  // goes here
}
def methodName(param1: Type1, param2: Type2): ReturnType =
  // the method body
  // goes here
end methodName   // this is optional

在该语法中

  • 关键字 def 用于定义方法
  • Scala 标准是使用驼峰命名法命名方法
  • 方法参数始终使用其类型定义
  • 声明方法返回类型是可选的
  • 方法可以包含多行或仅一行
  • 在方法主体之后提供 end methodName 部分也是可选的,并且仅建议用于长方法

以下是名为 add 的单行方法的两个示例,该方法采用两个 Int 输入参数。第一个版本明确显示了方法的 Int 返回类型,而第二个版本没有

def add(a: Int, b: Int): Int = a + b
def add(a: Int, b: Int) = a + b

建议使用其返回类型对公开可见的方法进行注释。声明返回类型可以让您在几个月或几年后查看它,或者在查看其他人的代码时更容易理解它。

调用方法

调用方法很简单

val x = add(1, 2)   // 3

Scala 集合类具有数十种内置方法。以下示例展示了如何调用它们

val x = List(1, 2, 3)

x.size          // 3
x.contains(1)   // true
x.map(_ * 10)   // List(10, 20, 30)

注意

  • size 不接受参数,并返回列表中的元素数量
  • contains 方法接受一个参数,即要搜索的值
  • map 接受一个参数,即一个函数;在本例中,将一个匿名函数传递给它

多行方法

当一个方法超过一行时,从第二行开始方法体,并向右缩进

def addThenDouble(a: Int, b: Int): Int = {
  // imagine that this body requires multiple lines
  val sum = a + b
  sum * 2
}
def addThenDouble(a: Int, b: Int): Int =
  // imagine that this body requires multiple lines
  val sum = a + b
  sum * 2

在该方法中

  • sum 是一个不可变的局部变量;它无法在方法外部访问
  • 最后一行将 sum 的值加倍;此值从方法返回

将该代码粘贴到 REPL 中时,你会看到它按预期工作

scala> addThenDouble(1, 1)
res0: Int = 4

请注意,方法末尾不需要 return 语句。由于 Scala 中几乎所有内容都是表达式(这意味着每行代码都返回(或求值为)一个值),因此无需使用 return

当你缩减该方法并将其写在一行中时,这一点会变得更加清晰

def addThenDouble(a: Int, b: Int): Int = (a + b) * 2

方法体可以使用该语言的所有不同功能

  • if/else 表达式
  • match 表达式
  • while 循环
  • for 循环和 for 表达式
  • 变量赋值
  • 对其他方法的调用
  • 对其他方法的定义

作为真实的多行方法的一个示例,此 getStackTraceAsString 方法将其 Throwable 输入参数转换为格式良好的 String

def getStackTraceAsString(t: Throwable): String = {
  val sw = new StringWriter()
  t.printStackTrace(new PrintWriter(sw))
  sw.toString
}
def getStackTraceAsString(t: Throwable): String =
  val sw = StringWriter()
  t.printStackTrace(PrintWriter(sw))
  sw.toString

在该方法中

  • 第一行将 StringWriter 的新实例分配给值绑定器 sw
  • 第二行将堆栈跟踪内容存储到 StringWriter
  • 第三行产生堆栈跟踪的 String 表示形式

默认参数值

方法参数可以有默认值。在此示例中,timeoutprotocol 参数都给出了默认值

def makeConnection(timeout: Int = 5_000, protocol: String = "http") = {
  println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
  // more code here ...
}
def makeConnection(timeout: Int = 5_000, protocol: String = "http") =
  println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
  // more code here ...

因为参数有默认值,所以可以这样调用方法

makeConnection()                 // timeout = 5000, protocol = http
makeConnection(2_000)            // timeout = 2000, protocol = http
makeConnection(3_000, "https")   // timeout = 3000, protocol = https

以下是关于这些示例的一些要点

  • 在第一个示例中,没有提供任何参数,所以方法使用 5_000http 的默认参数值
  • 在第二个示例中,为 timeout 值提供了 2_000,所以它与 protocol 的默认值一起使用
  • 在第三个示例中,为两个参数都提供了值,所以它们都被使用

请注意,通过使用默认参数值,对于使用者来说,他们似乎可以使用三种不同的重写方法。

命名参数

如果你愿意,你也可以在调用方法时使用方法参数的名称。例如,makeConnection 也可以这样调用

makeConnection(timeout=10_000)
makeConnection(protocol="https")
makeConnection(timeout=10_000, protocol="https")
makeConnection(protocol="https", timeout=10_000)

在某些框架中,命名参数被大量使用。当多个方法参数具有相同类型时,它们也非常有用

engage(true, true, true, false)

如果没有 IDE 的帮助,该代码可能很难阅读,但这段代码更清晰、更明显

engage(
  speedIsSet = true,
  directionIsSet = true,
  picardSaidMakeItSo = true,
  turnedOffParkingBrake = false
)

关于不带参数的方法的建议

当一个方法不带参数时,据说它的元数级别为元数-0。类似地,当一个方法带一个参数时,它是一个元数-1方法。当你创建元数-0 方法时

  • 如果该方法执行副作用,例如调用 println,则用空括号声明该方法
  • 如果该方法不执行副作用,例如获取集合的大小(类似于访问集合上的字段),则省略括号

例如,此方法执行副作用,因此用空括号声明

def speak() = println("hi")

这样做要求方法的调用者在调用方法时使用圆括号

speak     // error: "method speak must be called with () argument"
speak()   // prints "hi"

虽然这只是一个约定,但遵循它可以极大地提高代码的可读性:它使人们更容易一眼看出元数-0 方法执行副作用。

使用 if 作为方法体

因为 if/else 表达式返回一个值,所以它们可以用作方法体。这里有一个名为 isTruthy 的方法,它实现了 Perl 中 truefalse 的定义

def isTruthy(a: Any) = {
  if (a == 0 || a == "" || a == false)
    false
  else
    true
}
def isTruthy(a: Any) =
  if a == 0 || a == "" || a == false then
    false
  else
    true

这些示例展示了该方法的工作原理

isTruthy(0)      // false
isTruthy("")     // false
isTruthy("hi")   // true
isTruthy(1.0)    // true

使用 match 作为方法体

一个 match 表达式也可以用作整个方法体,而且通常是这样。以下是 isTruthy 的另一个版本,它用 match 表达式编写

def isTruthy(a: Any) = a match {
  case 0 | "" | false => false
  case _ => true
}
def isTruthy(a: Matchable) = a match
  case 0 | "" | false => false
  case _ => true

此方法的工作方式与使用 if/else 表达式的上一个方法一样。我们使用 Matchable 而不是 Any 作为参数的类型,以接受支持模式匹配的任何值。

有关 Matchable 特征的更多详细信息,请参阅 参考文档

控制类中的可见性

在类、对象、特征和枚举中,Scala 方法默认情况下是公共的,因此此处创建的 Dog 实例可以访问 speak 方法

class Dog {
  def speak() = println("Woof")
}

val d = new Dog
d.speak()   // prints "Woof"
class Dog:
  def speak() = println("Woof")

val d = new Dog
d.speak()   // prints "Woof"

方法也可以标记为 private。这使它们对当前类是私有的,因此它们不能在子类中被调用或覆盖

class Animal {
  private def breathe() = println("I’m breathing")
}

class Cat extends Animal {
  // this method won’t compile
  override def breathe() = println("Yo, I’m totally breathing")
}
class Animal:
  private def breathe() = println("I’m breathing")

class Cat extends Animal:
  // this method won’t compile
  override def breathe() = println("Yo, I’m totally breathing")

如果你想使一个方法对当前类是私有的,同时还允许子类调用或覆盖它,请将该方法标记为 protected,如本示例中 speak 方法所示

class Animal {
  private def breathe() = println("I’m breathing")
  def walk() = {
    breathe()
    println("I’m walking")
  }
  protected def speak() = println("Hello?")
}

class Cat extends Animal {
  override def speak() = println("Meow")
}

val cat = new Cat
cat.walk()
cat.speak()
cat.breathe()   // won’t compile because it’s private
class Animal:
  private def breathe() = println("I’m breathing")
  def walk() =
    breathe()
    println("I’m walking")
  protected def speak() = println("Hello?")

class Cat extends Animal:
  override def speak() = println("Meow")

val cat = new Cat
cat.walk()
cat.speak()
cat.breathe()   // won’t compile because it’s private

protected 设置意味着

  • 该方法(或字段)可以被同一类的其他实例访问
  • 它不会被当前包中的其他代码看到
  • 子类可以使用它

对象可以包含方法

之前你看到特征和类可以有方法。Scala object 关键字用于创建一个单例类,并且一个对象也可以包含方法。这是一个对一组“实用”方法进行分组的好方法。例如,此对象包含一组对字符串起作用的方法

object StringUtils {

  /**
   * Returns a string that is the same as the input string, but
   * truncated to the specified length.
   */
  def truncate(s: String, length: Int): String = s.take(length)

  /**
    * Returns true if the string contains only letters and numbers.
    */
  def lettersAndNumbersOnly_?(s: String): Boolean =
    s.matches("[a-zA-Z0-9]+")

  /**
   * Returns true if the given string contains any whitespace
   * at all. Assumes that `s` is not null.
   */
  def containsWhitespace(s: String): Boolean =
    s.matches(".*\\s.*")

}
object StringUtils:

  /**
   * Returns a string that is the same as the input string, but
   * truncated to the specified length.
   */
  def truncate(s: String, length: Int): String = s.take(length)

  /**
    * Returns true if the string contains only letters and numbers.
    */
  def lettersAndNumbersOnly_?(s: String): Boolean =
    s.matches("[a-zA-Z0-9]+")

  /**
   * Returns true if the given string contains any whitespace
   * at all. Assumes that `s` is not null.
   */
  def containsWhitespace(s: String): Boolean =
    s.matches(".*\\s.*")

end StringUtils

扩展方法

在许多情况下,你希望向封闭类添加功能。例如,假设你有一个 Circle 类,但你无法更改它的源代码。它可以在第三方库中这样定义

case class Circle(x: Double, y: Double, radius: Double)

当你想向此类添加方法时,你可以将它们定义为扩展方法,如下所示

implicit class CircleOps(c: Circle) {
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius
}

在 Scala 2 中使用 implicit class,在此处了解更详细信息 here

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius

在 Scala 3 中使用新的 extension 构造。有关更多详细信息,请参阅 本书 中的章节,或 Scala 3 参考

现在,当您有一个名为 aCircleCircle 实例时,您可以像这样调用这些方法

aCircle.circumference
aCircle.diameter
aCircle.area

更多

还有更多关于方法的知识,包括如何

  • 调用超类中的方法
  • 定义和使用按名称参数
  • 编写一个采用函数参数的方法
  • 创建内联方法
  • 处理异常
  • 使用 vararg 输入参数
  • 编写具有多个参数组的方法(部分应用函数)
  • 创建具有泛型类型参数的方法

参阅本书中的其他章节以了解有关这些功能的更多详细信息。

此页面的贡献者