Open cisen opened 3 years ago
val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000)
val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(_ * 2)
// 返回函数的函数
def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
val schema = if (ssl) "https://" else "http://"
(endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}
val domainName = "www.example.com" def getURL = urlBuilder(ssl=true, domainName) val endpoint = "users" val query = "id=1" val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String
## 案例类(case class)
- https://docs.scala-lang.org/zh-cn/tour/case-classes.html
案例类(Case classes)和普通类差不多,只有几点关键差别,接下来的介绍将会涵盖这些差别。案例类非常适合用于不可变的数据。下一节将会介绍他们在模式匹配中的应用。
定义一个案例类
一个最简单的案例类定义由关键字case class,类名,参数列表(可为空)组成:
```scala
case class Book(isbn: String)
val frankenstein = Book("978-0486282114")
注意在实例化案例类Book时,并没有使用关键字new,这是因为案例类有一个默认的apply方法来负责对象的创建。
当你创建包含参数的案例类时,这些参数是公开(public)的val
case class Message(sender: String, recipient: String, body: String)
val message1 = Message("guillaume@quebec.ca", "jorge@catalonia.es", "Ça va ?")
println(message1.sender) // prints guillaume@quebec.ca
message1.sender = "travis@washington.us" // this line does not compile
你不能给message1.sender重新赋值,因为它是一个val(不可变)。在案例类中使用var也是可以的,但并不推荐这样。
比较 案例类在比较的时候是按值比较而非按引用比较:
case class Message(sender: String, recipient: String, body: String)
val message2 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
val message3 = Message("jorge@catalonia.es", "guillaume@quebec.ca", "Com va?")
val messagesAreTheSame = message2 == message3 // true
尽管message2和message3引用不同的对象,但是他们的值是相等的,所以message2 == message3为true。
拷贝 你可以通过copy方法创建一个案例类实例的浅拷贝,同时可以指定构造参数来做一些改变。
case class Message(sender: String, recipient: String, body: String)
val message4 = Message("julien@bretagne.fr", "travis@washington.us", "Me zo o komz gant ma amezeg")
val message5 = message4.copy(sender = message4.recipient, recipient = "claire@bourgogne.fr")
message5.sender // travis@washington.us
message5.recipient // claire@bourgogne.fr
message5.body // "Me zo o komz gant ma amezeg"
上述代码指定message4的recipient作为message5的sender,指定message5的recipient为”claire@bourgogne.fr”,而message4的body则是直接拷贝作为message5的body了。
在Scala中,运算符即是方法。 任何具有单个参数的方法都可以用作 中缀运算符。 例如,可以使用点号调用 +:
10.+(1)
而中缀运算符则更易读:
10 + 1
定义和使用运算符 你可以使用任何合法标识符作为运算符。 包括像 add 这样的名字或像 + 这样的符号。
case class Vec(x: Double, y: Double) {
// 跟js的+号一样,特色的类型就会执行特色的处理。
// 只是scala更进一步将这种特色处理暴露出来
def +(that: Vec) = Vec(this.x + that.x, this.y + that.y)
}
val vector1 = Vec(1.0, 1.0)
val vector2 = Vec(2.0, 2.0)
val vector3 = vector1 + vector2
vector3.x // 3.0
vector3.y // 3.0
类 Vec 有一个方法 +,我们用它来使 vector1 和 vector2 相加。 使用圆括号,你可以使用易读的语法来构建复杂表达式。 这是 MyBool 类的定义,其中有方法 and 和 or:
case class MyBool(x: Boolean) {
def and(that: MyBool): MyBool = if (x) that else this
def or(that: MyBool): MyBool = if (x) this else that
def negate: MyBool = MyBool(!x)
}
现在可以使用 and 和 or 作为中缀运算符:
def not(x: MyBool) = x.negate
def xor(x: MyBool, y: MyBool) = (x or y) and not(x and y)
这有助于让方法 xor 的定义更具可读性。
优先级 当一个表达式使用多个运算符时,将根据运算符的第一个字符来评估优先级:
(characters not shown below)
* / %
+ -
:
= !
< >
&
^
|
(all letters, $, _)
这也适用于你自定义的方法。 例如,以下表达式:
a + b ^? c ?^ d less a ==> b | c
等价于
((a + b) ^? (c ?^ d)) less ((a ==> b) | c)
?^ 具有最高优先级,因为它以字符 ? 开头。 + 具有第二高的优先级,然后依次是 ==>, ^?, |, 和 less。
https://docs.scala-lang.org/zh-cn/tour/by-name-parameters.html 传名参数 仅在被使用时触发实际参数的求值运算。 它们与 传值参数 正好相反。 要将一个参数变为传名参数,只需在它的类型前加上 =>。
def calculate(input: => Int) = input * 37
传名参数的优点是,如果它们在函数体中未被使用,则不会对它们进行求值。 另一方面,传值参数的优点是它们仅被计算一次。 以下是我们如何实现一个 while 循环的例子:
// 最后一个参数默认就是执行体,这里的{}应该是执行体的包裹体
def whileLoop(condition: => Boolean)(body: => Unit): Unit =
if (condition) {
// 这里就是执行{}的东西
body
whileLoop(condition)(body)
}
var i = 2
whileLoop (i > 0) {
println(i)
i -= 1
} // prints 2 1
方法 whileLoop 使用多个参数列表来分别获取循环条件和循环体。 如果 condition 为 true,则执行 body,然后对 whileLoop 进行递归调用。 如果 condition 为 false,则永远不会计算 body,因为我们在 body 的类型前加上了 =>。
现在当我们传递 i > 0 作为我们的 condition 并且 println(i); i-= 1 作为 body 时,它表现得像许多语言中的标准 while 循环。
如果参数是计算密集型或长时间运行的代码块,如获取 URL,这种延迟计算参数直到它被使用时才计算的能力可以帮助提高性能。
方法可以具有 隐式 参数列表,由参数列表开头的 implicit 关键字标记。 如果参数列表中的参数没有像往常一样传递, Scala 将查看它是否可以获得正确类型的隐式值,如果可以,则自动传递。
Scala 将查找这些参数的位置分为两类:
Scala 在调用包含有隐式参数块的方法时,将首先查找可以直接访问的隐式定义和隐式参数 (无前缀)。 然后,它在所有伴生对象中查找与隐式候选类型相关的有隐式标记的成员。 更加详细的关于 Scala 到哪里查找隐式参数的指南请参考 常见问题
在下面的例子中,我们定义了一个方法 sum,它使用 Monoid 类的 add 和 unit 方法计算一个列表中元素的总和。 请注意,隐式值不能是顶级值。
abstract class Monoid[A] {
def add(x: A, y: A): A
def unit: A
}
object ImplicitTest {
implicit val stringMonoid: Monoid[String] = new Monoid[String] {
def add(x: String, y: String): String = x concat y
def unit: String = ""
}
implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
def add(x: Int, y: Int): Int = x + y
def unit: Int = 0
}
def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
if (xs.isEmpty) m.unit
// 根据数据类型自动调用字符串的加法还是int的add
else m.add(xs.head, sum(xs.tail))
def main(args: Array[String]): Unit = {
println(sum(List(1, 2, 3))) // uses IntMonoid implicitly
println(sum(List("a", "b", "c"))) // uses StringMonoid implicitly
}
}
类 Monoid 定义了一个名为 add 的操作,它将一对 A 类型的值相加并返回一个 A,以及一个名为 unit 的操作,用来创建一个(特定的)A 类型的值。
为了说明隐式参数如何工作,我们首先分别为字符串和整数定义 Monoid 实例, StringMonoid 和 IntMonoid。 implicit 关键字表示可以隐式使用相应的对象。
方法 sum 接受一个 List[A],并返回一个 A 的值,它从 unit 中取初始的 A 值,并使用 add 方法依次将列表中的下一个 A 值相加。在这里将参数 m 定义为隐式意味着,如果 Scala 可以找到隐式 Monoid[A] 用于隐式参数 m,我们在调用 sum 方法时只需要传入 xs 参数。
在 main 方法中我们调用了 sum 方法两次,并且只传入参数 xs。 Scala 会在上例的上下文范围内寻找隐式值。 第一次调用 sum 方法的时候传入了一个 List[Int] 作为 xs 的值,这意味着此处类型 A 是 Int。 隐式参数列表 m 被省略了,因此 Scala 将查找类型为 Monoid[Int] 的隐式值。 第一查找规则如下
Scala 在调用包含有隐式参数块的方法时,将首先查找可以直接访问的隐式定义和隐式参数 (无前缀)。
intMonoid 是一个隐式定义,可以在main中直接访问。 并且它的类型也正确,因此它会被自动传递给 sum 方法。
第二次调用 sum 方法的时候传入一个 List[String],这意味着此处类型 A 是 String。 与查找 Int 型的隐式参数时类似,但这次会找到 stringMonoid,并自动将其作为 m 传入。
当调用方法时,实际参数可以通过其对应的形式参数的名称来标记:
def printName(first: String, last: String): Unit = {
println(first + " " + last)
}
printName("John", "Smith") // Prints "John Smith"
printName(first = "John", last = "Smith") // Prints "John Smith"
printName(last = "Smith", first = "John") // Prints "John Smith"
注意使用命名参数时,顺序是可以重新排列的。 但是,如果某些参数被命名了,而其他参数没有,则未命名的参数要按照其方法签名中的参数顺序放在前面。
printName(last = "Smith", "john") // error: positional after named argument 注意调用 Java 方法时不能使用命名参数。
定义一个特质 最简化的特质就是关键字trait+标识符:
trait HairColor
特征作为泛型类型和抽象方法非常有用。
trait Iterator[A] {
def hasNext: Boolean
def next(): A
}
扩展 trait Iterator [A] 需要一个类型 A 和实现方法hasNext和next。
trait Iterator[A] {
def hasNext: Boolean
def next(): A
}
class IntIterator(to: Int) extends Iterator[Int] { private var current = 0 override def hasNext: Boolean = current < to override def next(): Int = { if (hasNext) { val t = current current += 1 t } else 0 } }
val iterator = new IntIterator(10) iterator.next() // returns 0 iterator.next() // returns 1
这个类 IntIterator 将参数 to 作为上限。它扩展了 Iterator [Int],这意味着方法 next 必须返回一个Int。
### 子类型
凡是需要特质的地方,都可以由该特质的子类型来替换。
```scala
import scala.collection.mutable.ArrayBuffer
trait Pet {
val name: String
}
class Cat(val name: String) extends Pet
class Dog(val name: String) extends Pet
val dog = new Dog("Harry")
val cat = new Cat("Sally")
val animals = ArrayBuffer.empty[Pet]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name)) // Prints Harry Sally
在这里 trait Pet 有一个抽象字段 name ,name 由Cat和Dog的构造函数中实现。最后一行,我们能调用pet.name的前提是它必须在特质Pet的子类型中得到了实现。
在 Scala 中,元组是一个可以容纳不同类型元素的类。 元组是不可变的。
当我们需要从函数返回多个值时,元组会派上用场。
元组可以创建如下:
val ingredient = ("Sugar" , 25):Tuple2[String, Int]
这将创建一个包含一个 String 元素和一个 Int 元素的元组。
Scala 中的元组包含一系列类:Tuple2,Tuple3等,直到 Tuple22。 因此,当我们创建一个包含 n 个元素(n 位于 2 和 22 之间)的元组时,Scala 基本上就是从上述的一组类中实例化 一个相对应的类,使用组成元素的类型进行参数化。 上例中,ingredient 的类型为 Tuple2[String, Int]。
访问元素 使用下划线语法来访问元组中的元素。 ‘tuple._n’ 取出了第 n 个元素(假设有足够多元素)。
println(ingredient._1) // Sugar
println(ingredient._2) // 25
解构元组数据 Scala 元组也支持解构。
val (name, quantity) = ingredient
println(name) // Sugar
println(quantity) // 25
元组解构也可用于模式匹配。
val planetDistanceFromSun = List(("Mercury", 57.9), ("Venus", 108.2), ("Earth", 149.6 ), ("Mars", 227.9), ("Jupiter", 778.3))
planetDistanceFromSun.foreach{ tuple => {
tuple match {
case ("Mercury", distance) => println(s"Mercury is $distance millions km far from Sun")
case p if(p._1 == "Venus") => println(s"Venus is ${p._2} millions km far from Sun")
case p if(p._1 == "Earth") => println(s"Blue planet is ${p._2} millions km far from Sun")
case _ => println("Too far....")
}
}
}
或者,在 ‘for’ 表达式中。
val numPairs = List((2, 5), (3, -7), (20, 56))
for ((a, b) <- numPairs) {
println(a * b)
}
类型 Unit 的值 () 在概念上与类型 Tuple0 的值 () 相同。 Tuple0 只能有一个值,因为它没有元素。
用户有时可能在元组和 case 类之间难以选择。 通常,如果元素具有更多含义,则首选 case 类。
当某个特质被用于组合类时,被称为混入。
abstract class A {
val message: String
}
class B extends A {
val message = "I'm an instance of class B"
}
trait C extends A {
def loudMessage = message.toUpperCase()
}
class D extends B with C
val d = new D
println(d.message) // I'm an instance of class B
println(d.loudMessage) // I'M AN INSTANCE OF CLASS B
类D有一个父类B和一个混入C。一个类只能有一个父类但是可以有多个混入(分别使用关键字extends和with)。混入和某个父类可能有相同的父类。
现在,让我们看一个更有趣的例子,其中使用了抽象类:
abstract class AbsIterator {
type T
def hasNext: Boolean
def next(): T
}
该类中有一个抽象的类型T和标准的迭代器方法。
接下来,我们将实现一个具体的类(所有的抽象成员T、hasNext和next都会被实现):
class StringIterator(s: String) extends AbsIterator {
type T = Char
private var i = 0
def hasNext = i < s.length
def next() = {
val ch = s charAt i
i += 1
ch
}
}
StringIterator带有一个String类型参数的构造器,可用于对字符串进行迭代。(例如查看一个字符串是否包含某个字符):
现在我们创建一个特质,也继承于AbsIterator。
trait RichIterator extends AbsIterator {
def foreach(f: T => Unit): Unit = while (hasNext) f(next())
}
该特质实现了foreach方法——只要还有元素可以迭代(while (hasNext)),就会一直对下个元素(next()) 调用传入的函数f: T => Unit。因为RichIterator是个特质,可以不必实现AbsIterator中的抽象成员。
下面我们要把StringIterator和RichIterator 中的功能组合成一个类。
object StringIteratorTest extends App {
class RichStringIter extends StringIterator("Scala") with RichIterator
val richStringIter = new RichStringIter
richStringIter foreach println
}
新的类RichStringIter有一个父类StringIterator和一个混入RichIterator。如果是单一继承,我们将不会达到这样的灵活性。
总结
numbers.foldLeft(0)(_ + _)