Scala语法

本文基于 Scala 2.13

https://docs.scala-lang.org/tour/tour-of-scala.html

import

import 路径.目标  // 基础导入
import 路径.{成员1, 成员2}  // 选择性导入
import 路径.{旧名 => 新名}  // 重命名导入
import 路径._  // 通配导入(类似 Java 的 *)

import 可在任何作用域使用(包、类、方法、代码块内),作用域内有效。Scala 中 package 是目录结构映射,但导入时支持 “相对路径”(基于当前包)或 “绝对路径”(以 root 开头,跳过当前包层级)。

注意:通配导入可能导致命名冲突(如同时导入 java.util.Listscala.collection.immutable.List),尽量避免在顶层滥用,可在局部作用域(如方法内)使用。

导入时排除成员(过滤不需要的成员)

// 导入 java.util 包下所有成员,但排除 Date 类(避免冲突)
import java.util.{Date => _, _}

val map = new HashMap[String, Int]()  // 可用
// val date = new Date()  // 编译报错:Date 已被排除

导入作用域

import 的作用域由声明位置决定,遵循 “就近原则”(局部导入覆盖外层导入):

  1. 顶层导入:在包声明后、类 / 对象定义前,作用于整个文件。
  2. 类 / 对象内导入:作用于整个类 / 对象。
  3. 方法 / 代码块内导入:仅作用于该方法 / 代码块(局部生效,推荐用于避免冲突)。
package com.example

// 顶层导入:作用于整个文件
import scala.collection.immutable.List

class Demo {
  // 类内导入:作用于整个 Demo 类
  import java.util.HashMap

  def test(): Unit = {
    // 方法内导入:仅作用于 test 方法(局部生效)
    import scala.collection.mutable.ListBuffer

    val list = List(1, 2)          // 顶层导入的 immutable.List
    val map = new HashMap[String, Int]()  // 类内导入的 HashMap
    val buffer = ListBuffer(3, 4)  // 方法内导入的 ListBuffer
  }
}

Object

https://docs.scala-lang.org/tour/singleton-objects.html

object 关键字用于定义单例对象

object Circle {
  private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}

伴生对象

一个与类同名的对象被称为伴生对象。反之,该类即为该对象的伴生类。伴生类或对象可以访问其伴生对象的私有成员。使用伴生对象来封装那些不特定于伴生类实例的方法和值。

伴生对象可以访问伴生类的私有成员,反之亦然。

class Person(val name: String, val age: Int) {
  // 伴生类的私有成员
  private var secret: String = "This is a secret"

  def revealSecret(): String = secret
}

object Person {
  // 伴生对象的工厂方法
  def apply(name: String, age: Int): Person = new Person(name, age)

  // 伴生对象的静态方法
  def greet(person: Person): String = s"Hello, ${person.name}!"

  // 隐式转换
  implicit def stringToPerson(name: String): Person = new Person(name, 0)
}

// 使用伴生对象
object Main extends App {
  // 使用工厂方法创建实例
  val john = Person("John", 30)
  println(Person.greet(john)) // 输出:Hello, John!

  // 使用隐式转换
  val jane: Person = "Jane" // 隐式转换
  println(Person.greet(jane)) // 输出:Hello, Jane!
}

在Java中,静态成员在Scala中就是伴生对象的普通成员。

当从Java代码中使用伴生对象时,成员将被定义在带有静态修饰符的伴生类中。这称为静态转发。即使没有定义伴生类,这种情况也会发生。

Case Class

https://docs.scala-lang.org/tour/case-classes.html

主要特性:

  1. 不可变性:case class 的实例默认是不可变的(immutable),一旦创建就不能修改。
  2. 自动生成的方法:Scala 自动为 case class 生成以下方法:
    • equalshashCode 方法:用于比较实例。
    • toString 方法:提供友好的字符串表示。
    • copy 方法:用于创建实例的副本,可以在副本中修改某些属性。
    • apply 方法:允许使用不带 new 关键字来创建实例。
  3. 模式匹配:case class 可以与模式匹配结合使用,非常方便。

参数默认是 val:case class 的构造参数默认是 val,这意味着它们是不可变的。如果需要可变参数,可以显式指定为 var,但不推荐这样做。

简化构造:使用 case class 时可以省略 new 关键字来创建实例,代码更简洁。

// 定义一个 case class
case class Person(name: String, age: Int)

object Main extends App {
  // 创建实例
  val john = Person("John", 30)
  val jane = Person("Jane", 25)

  // 打印实例
  println(john) // 输出:Person(John,30)
  println(jane) // 输出:Person(Jane,25)

  // 使用 equals 方法
  println(john == Person("John", 30)) // 输出:true

  // 使用 copy 方法
  val olderJohn = john.copy(age = 31)
  println(olderJohn) // 输出:Person(John,31)

  // 模式匹配
  john match {
    case Person(name, age) => println(s"Name: $name, Age: $age")
  }
}

Package Object

Package Object 是一种特殊的对象,它允许在一个包范围内定义共享的变量、函数和类型。这种机制可以帮助你组织代码,避免在多个文件中重复定义相同的内容。

特点

  1. 共享内容:Package Object 中的成员可以被包内的所有类和对象访问,无需导入。
  2. 与包关联:Package Object 在定义时与包相结合,使用 package 关键字。
  3. 避免命名冲突:可以有效地组织代码,减少命名冲突的风险。

一个目录或者说一个包下面只能有一个 Package Object

package scala

package object math {
    ...
}

implicit

https://www.artima.com/pins1ed/implicit-conversions-and-parameters.html

implicit parameter

implicit关键字加在函数参数上,函数可以有2个参数列表,一个是普通的参数列表,另一个是隐式参数列表,例如:

// probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s

// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
// 调用函数时不需要传隐式参数列表的参数,会自动从当前上下文中取对应的类型
addPrefix("abc")  // returns "***abc"

调用时若未显式传入,Scala 会自动在当前作用域(包括当前类、父类、导入的包 / 对象)中查找「类型匹配的隐式值」并自动注入。

关键规则

  • 隐式参数必须放在最后一个参数列表中(便于区分普通参数和隐式参数);
  • 作用域内只能有「一个类型匹配的隐式值」(否则编译报错 “歧义”);
  • 隐式值的查找优先级:当前局部作用域 > 导入的隐式 > 父类 / 伴生对象中的隐式。

implicit function

implicit关键字加在函数声明上,函数就变成了隐式函数。例如:

implicit def doubleToInt(d: Double) = d.toInt

这种函数有什么用呢,举个例子:

val x: Int = 42.0  // 使用Int接收Double类型,会报错类型不匹配

当编译器发现上下文需要的表达式类型不匹配时,它会寻找能使其类型检查通过的隐式函数值。例如,若需A类型却检测到B类型,编译器会查找作用域内是否存在B => A类型的隐式值(如果存在B和A的伴生对象的话,同时还会检查伴生对象等其他位置)。

所以,如果当前上下文中存在doubleToInt函数,那么编译就能通过了

implicit def doubleToInt(d: Double) = d.toInt
val x: Int = 42.0  // 使用 doubleToInt 函数进行隐式转换
// 等效于直接调用函数进行转换
val x: Int = doubleToInt(42.0)

核心用途:扩展现有类的功能(无需继承或修改原类)、解决类型不兼容问题。让一个类型自动转换为另一个类型,从而为原类型 “补充方法” 或 “适配不同接口”。

// 隐式函数:将 Int 转换为 String
implicit def intToString(n: Int): String = n.toString

// 原 Int 类型本身没有 concat 方法,但转换后可调用 String 的 concat
val num: Int = 123
val result: String = num.concat("456")  // 等价于 intToString(123).concat("456")
println(result)  // 输出:123456

implicit class

Scala 2.10 引入的一个特性:隐式类

创建隐式类时,只需要在对应的类前加上implicit关键字。

object Helpers {
  implicit class IntWithTimes(x: Int) {
    def times[A](f: => A): Unit = {
      def loop(current: Int): Unit =
        if(current > 0) {
          f
          loop(current - 1)
        }
      loop(x)
    }
  }
}

隐式类必须包含一个主构造函数,其第一个参数列表中仅有一个参数。它还可以包含一个额外的隐式参数列表。隐式类必须定义在允许方法定义的作用域内(而非顶层)。隐式类会被转换为一个类与隐式方法的配对,其中隐式方法模拟了类的构造函数

生成的隐式方法将与隐式类同名。这样可以通过类名导入隐式转换,就像预期的其他隐式定义一样。

使用隐式类时,类名必须在当前作用域内可见且无歧义,这一要求与隐式值等其他隐式类型转换方式类似。

隐式类有以下限制条件:

  1. 只能在别的trait/类/对象内部定义。
  2. 构造函数只能携带一个非隐式参数
  3. 虽然可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。
  4. 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。这意味着隐式类不能是case class

implicit的问题

过度使用implicit会增加代码隐蔽性,复杂场景(如多层隐式转换)可能难以调试;

import package1._
import package2._
import package3._
import package4._

object HelloWorld {
  case class Text(content: String)
  case class Prefix(text: String)

  implicit def String2Text(content: String)(implicit prefix: Prefix) = {
    Text(prefix.text + " " + content)
  }

  def printText(text: Text): Unit = {
    println(text.content)
  }

  def main(args: Array[String]): Unit = {
    printText("World!")
  }
}

假设上面导入的包中恰好存在一个隐式变量,但是你并不知道

// Best to hide this line somewhere below a pile of completely unrelated code.
// Better yet, import its package from another distant place.
implicit val prefixLOL = Prefix("Hello")

因此你在不知情的情况下调用String2Text函数,并传入参数,并预期返回你想要的结果World,如下所示:

val res = String2Text("World")  // 实际上会返回Hello World, 这是一个意料之外的行为
posted @ 2025-11-01 23:42  vonlinee  阅读(4)  评论(0)    收藏  举报