cisen / blog

Time waits for no one.
132 stars 20 forks source link

scala入门 #1064

Open cisen opened 3 years ago

cisen commented 3 years ago

总结

cisen commented 3 years ago

语法

cisen commented 3 years ago

内置对象

Vector

高阶函数

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+标识符:

trait HairColor

特征作为泛型类型和抽象方法非常有用。

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

扩展 trait Iterator [A] 需要一个类型 A 和实现方法hasNext和next。

使用特质

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的子类型中得到了实现。

tuple 元组

在 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 类。

通过混入(MIXIN)来组合类

当某个特质被用于组合类时,被称为混入。

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。如果是单一继承,我们将不会达到这样的灵活性。