本部分介绍如何在 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
表示形式
默认参数值
方法参数可以有默认值。在此示例中,timeout
和 protocol
参数都给出了默认值
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_000
和http
的默认参数值 - 在第二个示例中,为
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 中 true
和 false
的定义
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 参考。
现在,当您有一个名为 aCircle
的 Circle
实例时,您可以像这样调用这些方法
aCircle.circumference
aCircle.diameter
aCircle.area
更多
还有更多关于方法的知识,包括如何
- 调用超类中的方法
- 定义和使用按名称参数
- 编写一个采用函数参数的方法
- 创建内联方法
- 处理异常
- 使用 vararg 输入参数
- 编写具有多个参数组的方法(部分应用函数)
- 创建具有泛型类型参数的方法
参阅本书中的其他章节以了解有关这些功能的更多详细信息。