class Upper1:
def convert(strings: Seq[String]): Seq[String] =
strings.map((s: String) => s.toUpperCase)
val up = new Upper1()
val uppers = up.convert(List("Hello", "World!"))
println(uppers)
- 使用 class 关键字声明一个类 Upper1, 后跟冒号, 整个类体在后面的行上缩进, Upper1 包含了一个名为 convert 的方法
- 方法定义以 def 关键字开始, 然后是方法名称和可选的参数列表, 方法签名以可选的返回类型结束, 等号将方法签名和方法体分隔开
- 如果省略冒号和返回类型, Scala 会推断返回类型
- 如果方法不接受参数, 也可以省略圆括号
- 等号提醒我们函数式编程原则: 变量和函数是统一处理的。函数可以像值一样作为参数传递给其他函数, 也可以从函数中返回, 并赋值给变量
- convert 方法接收一个由零个或多个输入字符串组成的序列(Seq)并返回一个新的序列, 其中每个输入字符串都被转换为大写
- Seq 是可以迭代的集合的抽象, convert 方法返回的实际类型将与作为参数传递给它的具体类型相同, 比如 Vector 或者 List (两者都是不可变集合)
- 像 Seq[T] 这样的集合类型是参数化类型, T 是序列中元素的类型
- List[T] 是一个 immutable linked list, 访问List的头部是O(1),而访问位置N的任意元素是O(N)
- Vector[T] 是 Seq[T] 的子类型,几乎所有访问模式都是O(1)。
- 在方法体中, 使用了大多数集合可用的一种强大方法 map, 它遍历集合, 对每个元素调用所提供的方法, 并返回带有转换后元素的新集合
- 传递给 map 方法的是一个未命名的函数字面量 (parameters) => body
(s: String) => s.toUpperCase
- 它接受一个名为 s 的字符串作为参数, 函数体位于 => 之后, 对 s 调用 toUpperCase 方法
- 在Scala中,函数、方法或其他代码块中的最后一个表达式是返回值
- 最后两行使用 new Upper1() 创建 Upper1 的实例,命名为 up, 并使用它将两个字符串转换为大写,最后打印结果
- 通常,println 需要一个字符串参数,但如果你传递给它一个对象,比如 Seq 时,将调用 toString 方法。
package progscala3.introscala // <1>声明包的位置
object UpperMain1:
def main(params: Array[String]): Unit = // <2>在对象内部声明一个main方法,一个程序入口点。
print("UpperMain1.main: ")
params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
println("")
def main(params: Array[String]): Unit = // <3>将另一个主入口点声明为顶级方法,位于任何对象之外,但作用域限于当前包
print("main: ")
params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
println("")
@main def Hello(params: String*): Unit = // <4>声明一个入口点方法,我们可以在其中使用不同的名称,并且对参数列表有更灵活的选项
print("Hello: ")
params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
println("")
- Scala中没有static关键字, 相反, 使用 object 关键字声明一个对象 UpperMain1 然后使用与在类中相同的语法声明 main 和其他成员, 将 UpperMain1 声明为一个 object 将使它成为一个单例, 它永远只有一个实例
- 这个文件有三个入口点
- 第一个: UpperMain1. main, 是 Scala2 中声明入口点的方式
- 第二个: Scala3 新特性, 可以在 object 和 class 外部声明方法和变量
- 第三个: Scala3 新特性, 使用 @main 注释将方法标记为一个入口点, 可以在object 内外部声明
- 减少了定义时的样板文件, 可以定义参数列表
- String* 表示0到多个字符串(重复参数)
s => s.toUpperCase
- 前面的例子使用了 (s: String) => s.toUpperCase, 大多数时候, Scala 可以推断函数字面量的参数类型, 因为 map 提供的上下文告诉编译器需要什么类型, 所以类型声明 String 可以省略
- 当希望处理每个元素并只执行副作用而不返回新值时就会使用 foreach 方法, 这里将一个字符串打印输出, 相反, map 方法为每个元素返回一个新值(应该避免产生副作用)
- 副作用意味着传递给 foreach 的函数会影响局部上下文之外的状态, 比如写入数据库或文件, 打印到控制台
package progscala3.introscala
@main def Hello2(params: String*): Unit =
val output = params.map(_.toUpperCase).mkString(" ")
println(output)
- 将字符串序列映射到一个新的字符串序列后, 调用 mkString 方法, 将字符串连接成一个最终的字符串
(s: String) => s.toUpperCase s => s.toUpperCase _.toUpperCase
- 这三个函数字面量的本质是相同的
- 单次引用一个或多个参数, 可以使用 _ 作为占位符代替一个参数, 而不为参数提供名称
package progscala3.introscala.shapes //1 为二维点声明一个类。因为没有定义成员,所以省略了类签名末尾的冒号(:) case class Point(x: Double = 0.0, y: Double = 0.0) //2 为几何形状声明一个抽象类。它需要一个冒号,因为它定义了一个方法 draw abstract class Shape(): //3 实现了draw方法 def draw(f: String => Unit): Unit = f(s"draw: $this") //4 一个具有中心和半径的圆, 它是 Shape 的子类 case class Circle(center: Point, radius: Double) extends Shape //5 一个具有左下角点、高度和宽度的矩形, 它是 Shape 的子类 case class Rectangle(lowerLeft: Point, height: Double, width: Double) extends Shape //6 一个由三点确定的三角形 case class Triangle(point1: Point, point2: Point, point3: Point) extends Shape
- Point 类名后面的参数列表是构造函数参数列表
- case 关键字修饰的类有特殊处理
- 构造函数参数自动转换为只读字段(val)
- 会自动生成 toString, equals, hashCode 方法
- 编译器会为每个 case 类生成一个伴生对象(同名的单例对象), 我们声明了 case class Point, 编译器就创建了 object Point
- 当一个 object 和一个 class 有相同的名称并且同一个文件中定义时, 它们是伴生的
- 编译器还会自动向伴生对象中添加几个方法, 其中一个方法名为apply, 它接受与构造函数相同的参数列表
- 可以不使用 new 关键字构造实例 (实际上是调用了伴生对象的 apply 方法)
object Point: def apply(x: Double = 0.0, y: Double = 0.0) = new Point(x, y) val p1 = Point.apply(1.0, 2.0) val p2 = Point(1.0, 2.0) // Same!
- 显式声明 object Point 就可以向伴生对象添加方法, 包括重载的 apply 方法, 默认的apply方法仍然会生成
- 可以在任何 class 中定义 apply 方法
当参数列表放在 object 或 instance 之后, Scala 会寻找一个 apply 方法调用
- 继续代码例子, Shape 是一个抽象类, Shape. draw 方法被定义
- draw 方法的参数 f 是 String => Unit 类型的函数
- 当一个函数返回 Unit 时它是完全的副作用
- 通常在 FP 中更喜欢没有副作用的纯函数, 只在必要的地方使用副作用, 保持其余代码的纯粹性
- f 方法由 draw 方法调用, 它使用插值字符串构造最终的字符串
package progscala3.introscala.shapes //1 声明了名为 Message 的 trait,trait 类似于 abstract base class 抽象基类 //所有交换的信息都是 Message 的子类 sealed trait Message //2 绘制形状的信息 case class Draw(shape: Shape) extends Message //3 对前一条信息做出响应的信息 case class Response(message: String) extends Message //4 一个终止的信息 case object Exit extends Message
- sealed 关键字意味着只能在同一个文件中定义 Message 的子类型
- sealed hierarchies 密封的层次结构
package progscala3.introscala.shapes
object ProcessMessages:
def apply(message: Message): Message =
message match
case Exit =>
println(s"ProcessMessage: exiting...")
Exit
case Draw(shape) =>
shape.draw(str => println(s"ProcessMessage: $str"))
Response(s"ProcessMessage: $shape drawn")
case Response(unexpected) =>
val response = Response(s"ERROR: Unexpected Response: $unexpected")
println(s"ProcessMessage: $response")
response
- apply 方法引入了一个很强大的特性 pattern matching 模式匹配
- match 表达式的工作原理很像 if/else 表达式, 但更加强大和简洁,
- 当其中一个模式匹配时, 计算箭头(=>)后的表达式块, 直到下一个 case 或结尾
- 如果 case 子句没有涵盖可以传递给 match 表达式的所有可能值, 则会在运行时抛出 MatchError
- pattern matching 的一个强大特性是从匹配的对象中提取数据的能力, 称为 deconstruction 解构
最后运行这个例子
@main def ProcessShapesDriver =
val messages = Seq(
Draw(Circle(Point(0.0,0.0), 1.0)),
Draw(Rectangle(Point(0.0,0.0), 2, 5)),
Response(s"Say hello to pi: 3.14159"),
Draw(Triangle(Point(0.0,0.0), Point(2.0,0.0), Point(1.0,2.0))),
Exit)
messages.foreach { message =>
val response = ProcessMessages(message)
println(response)
}
确保你知道每一行是如何处理的



