1. 概述

下划线 _ 是 Scala 中广泛使用的一个符号,有时它被称为“语法糖”,因为它可以让代码更简洁、更短。然而,这也常常带来困惑,增加了学习曲线。

在本篇文章中,我们将介绍 Scala 中下划线 _ 最常见的一些用法。

2. 版本警告 ⚠️

在 Scala 3 中,一些下划线的用法已被弃用或移除,例如 通配符参数变长参数展开 的语法,新的语法已经替代了这些用法。本文中讨论的特性在 Scala 2 中可以正常使用,但在 Scala 3 中可能并不完全兼容。

3. 模式匹配与通配符

下划线最常见的用途之一是作为通配符,用于匹配未知的模式。这也是我们学习 Scala 时最先接触到的用法之一。

3.1. 模块导入

在导入包时,我们可以使用下划线来表示导入模块中的所有成员(相当于 Java 中的 * 导入):

// 导入 junit 包中的所有成员(等价于 Java 中的 import org.junit.*)
import org.junit._

// 导入 junit 中除 Before 外的所有成员
import org.junit.{Before => _, _}

// 导入 junit 中所有成员,并将 Before 重命名为 B4
import org.junit.{Before => B4, _}

3.2. 存在类型

在类型构造器中(如 ListArraySeqOptionVector 等),我们可以使用下划线作为通配符来匹配所有类型:

def getLength(x : List[List[_]]): Int = x.length

assertEquals(getLength(List(List(8), List("str"))), 2)
assertEquals(getLength(List(List(5.00), List("str"))), 2)
assertEquals(getLength(List(List(Array(7)), List("str"))), 2)

通过使用 _,我们允许内层 List 中存储任意类型的元素。

3.3. 模式匹配

使用 match 关键字时,我们可以使用下划线来捕获所有未被显式处理的模式:

def itemTransaction(price: Double): String = {
  price match {
    case 130 => "Buy"
    case 150 => "Sell"
  
    // 如果价格不是 130 或 150,则执行此分支
    case _ => "Need approval"
  }
}

itemTransaction(130) // Buy
itemTransaction(150) // Sell
itemTransaction(70) // Need approval
itemTransaction(400) // Need approval

我们也可以参考我们的 模式匹配 教程了解更多示例。

4. 忽略内容

下划线可以用来忽略代码中不需要使用的变量或类型。

4.1. 忽略参数

在函数执行中,我们可以使用下划线来忽略未使用的参数:

val ints = (1 to 4).map(_ => "Int")
assertEquals(ints, Vector("Int", "Int", "Int", "Int"))

在这个例子中,我们忽略了 map 函数中的参数,直接返回 "Int"

也可以使用下划线作为占位符来简化函数调用:

val prices = Seq(10.00, 23.38, 49.82)
val pricesToInts = prices.map(_.toInt)
assertEquals(pricesToInts, Seq(10, 23, 49))

这等价于:

prices.map(x => x.toInt)

还可以使用下划线访问嵌套集合:

val items = Seq(("candy", 2, true), ("cola", 7, false), ("apple", 3, false), ("milk", 4, true))
val itemsToBuy = items
  .filter(_._3)  // 只保留可用商品(true)
  .filter(_._2 > 3)  // 只保留价格大于 3 的商品
  .map(_._1)  // 只保留商品名
assertEquals(itemsToBuy, Seq("milk"))

4.2. 忽略变量

在解构赋值中,如果只关心部分变量,可以用下划线忽略其他变量:

val text = "a,b"
val Array(a, _) = text.split(",")
assertEquals(a, "a")

如果只关心第二个元素:

val Array(_, b) = text.split(",")
assertEquals(b, "b")

也可以忽略多个元素:

val text = "a,b,c,d,e"
val Array(a, _*) = text.split(",")
assertEquals(a, "a")

如果只想忽略某个特定元素:

val Array(a, b, _, d, e) = text.split(",")
assertEquals(a, "a")
assertEquals(b, "b")
assertEquals(d, "d")
assertEquals(e, "e")

4.3. 变量初始化为默认值

当变量不需要初始值时,可以使用下划线将其初始化为默认值:

var x: String = _
x = "real value"
println(x) // real value

⚠️ 注意:这种方式不适用于局部变量,局部变量必须显式初始化。

5. 类型转换与函数转换

下划线在类型转换和函数转换中也有多种用途。

5.1. 方法转函数(Eta 展开)

使用下划线可以把一个方法转换为函数:

def multiplier(a: Int, b: Int): Int = a * b

val times = multiplier _ // 将 multiplier 方法转换为函数
assertEquals(multiplier(8, 13), times(8, 13))

5.2. 序列转可变参数

可以使用 seqName: _* 将序列转换为可变参数:

def sum(args: Int*): Int = {
  args.reduce(_ + _)
}
val sumable = Seq(4, 5, 10, 3)
val sumOfSumable = sum(sumable: _*) // 将序列转换为可变参数
assertEquals(sumOfSumable, 22)

5.3. 部分应用函数

通过只提供部分参数,可以生成一个部分应用函数,未提供的参数用下划线代替:

def sum(x:Int,y:Int): Int = x + y
val sumToTen = sum(10, _: Int)
val sumFiveAndTen = sumToTen(5)

assertEquals(sumFiveAndTen, 15)

⚠️ 这种使用方式也可以归类为“忽略内容”。

还可以忽略参数组来生成部分应用函数:

def bar(x:Int,y:Int)(z:String,a:String)(b:Float,c:Float): Int = x
val foo = bar(1,2) _

assertEquals(foo("Some string", "Another string")(3/5, 6/5), 1)

5.4. 赋值操作符(重写 setter)

使用下划线可以重写默认的 setter 方法:

class Product {
  private var a = 0
  def price = a
  def price_=(i: Int): Unit = {
    require(i > 10)
    a = i
  }
}

val product = new Product
product.price = 20
assertEquals(product.price, 20)

try {
  product.price = 7 // 会失败,因为 7 不大于 10
  fail("Price must be greater than 10")
} catch {
  case _: IllegalArgumentException => assertNotEquals(product.price, 7)
}

6. 其他用途

还有一些不属于上述分类的用法。

6.1. 连接字母与操作符/标点符号

在变量名中不能直接使用标点符号,但可以通过下划线连接字母与标点符号:

def list_++(list: List[_]): List[_] = List.concat(list, list)
val concatenatedList = list_++(List(2, 5))
assertEquals(concatenatedList, List(2, 5, 2, 5))

6.2. 数字字面量分隔符(Scala 2.13+)

从 Scala 2.13 开始,可以使用下划线作为数字字面量的分隔符:

var x = 1_000_000 // 1000000
x = 1_00_00_00 // 1000000
x = 1_0000_00 // 1000000

var pi = 3_14e-0_2 // 3.14
pi =  3_14e-02 // 3.14
pi =  314e-0_2 // 3.14
pi =  314e-02 // 3.14

6.3. 高阶类型(Higher-Kinded Type)

高阶类型是对类型构造器的抽象,类似于存在类型。可以使用下划线定义高阶类型:

trait ObjectContainer[T[_]] { // 高阶类型参数
  def checkIfEmpty(collection: T[_]): Boolean
}
object SeqContainer extends ObjectContainer[Seq] {
  override def checkIfEmpty(collection: Seq[_]): Boolean = !collection.nonEmpty
}

var seqIsEmpty = SeqContainer.checkIfEmpty(Seq(7, "7"))
assertTrue(seqIsEmpty == false)
seqIsEmpty = SeqContainer.checkIfEmpty(Seq())
assertTrue(seqIsEmpty == true)

7. 总结

在本文中,我们介绍了 Scala 中下划线 _ 的多种常见用法。这些用法涵盖了从模式匹配、忽略变量到函数转换等多个方面。

✅ 项目源码可以在 GitHub 上找到。


原始标题:Usages of Underscore (_) in Scala