[关闭]
@runzhliu 2018-06-28T06:09:30.000000Z 字数 8633 阅读 945

Scala 新手眼中的十种有趣用法[转]

scala


转载 https://n3xtchen.github.io/n3xtchen/scala/2016/06/15/scala-top-10-for-new

作为一个初学者,经过一个月系统的系统学习,用惯了动态语言的我来说,Scala 编译器型语言的编程体验真的是太棒了。作为阶段性的总结,我将给出我对 Scala 最佳初体验的 Top 10。

1 漂亮的操作系统调用方式

Scala 2.9里也提供类似功能:新增加了package: scala.sys 及scala.sys.process, 这些代码最初由 SBT (a simple build tool for Scala)项目贡献,主要用于简化与操作系统进程的交互与调用。

现在看看用法:

  1. scala> import scala.language.postfixOps
  2. import scala.language.postfixOps
  3. scala> import scala.sys.process._
  4. import scala.sys.process._
  5. scala> "java -version" !
  6. java version "1.7.0_80"
  7. Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
  8. Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
  9. res2: Int = 0

当你引入 scala.sys.process 后,scala 会为你给字符串或数组动态注入 ! 方法来调用系统命令。这是我见过调用 shell 最爽最优雅的方式之一,不是吗?现在看个复杂点的例子,爬取一个页面:

  1. scala> import java.io.File
  2. import java.io.File
  3. scala> import java.net.URL
  4. import java.net.URL
  5. scala> new URL("http://www.scala-lang.org/") #> new File("scala-lang.html") !
  6. res4: Int = 0

这里我们又学到一个新的操作符 #>,结果重定向。这条命令等价于如下 Bash:

  1. $ curl "http://www.scala-lang.org/ > scala-lang.html

看看结果:

  1. scala> "ls" !
  2. ...
  3. scala-lang.html
  4. ...
  5. res6: Int = 0

参考: http://www.scala-lang.org/api/rc2/scala/sys/process/package.html

2 管道(Pipeline)

  1. scala> import scala.language.implicitConversions
  2. import scala.language.implicitConversions
  3. scala> object Pipeline {
  4. | implicit class toPiped[V](value:V) {
  5. | def |>[R] (f : V => R) = f(value)
  6. | }
  7. | }
  8. defined module Pipeline
  9. scala> import Pipeline._
  10. import Pipeline._
  11. scala> 1 |> ((i:Int)=> i*10)
  12. res3: Int = 10

这样就可以将,函数返回的值作为后面函数参数,一条链式调用看起来是那么的优雅(你觉得呢?)。

短短几行的代码,足以让你领教到隐式转化(implicit)的威力吧!因为这个话题比较大,就不在这里作详细阐述了。

3 使用 {...} 替代 (...) 的语法糖

声明一个多参数表函数,如下

  1. scala> def m[A](s: A)(f: A=> String) = f(s)
  2. m: [A](s: A)(f: A => String)String

你可以这样调用它:

  1. scala> m(100)(i => s"$i + $i")
  2. res2: String = 100 + 100

你可以使用 {...} 替代 (...) 的语法糖,就可以把上面改写成下面的模式

  1. scala> m(100){ i => s"$i + $i" }
  2. res3: String = 100 + 100

竟然可以如此优雅优雅地调用函数,看起来就像标准的块代码(像 if 和 for 表达式)

4 创建自己的字符解释器

  1. import scala.util.parsing.json._
  2. object Interpolators {
  3. implicit class jsonForStringContext(val sc: StringContext) {
  4. def json(values: Any*): JSONObject = {
  5. val keyRE = """^[\s{,]*(\S+):\s*""".r
  6. val keys = sc.parts map {
  7. case keyRE(key) => key
  8. case str => str
  9. }
  10. val kvs = keys zip values
  11. JSONObject(kvs.toMap)
  12. }
  13. }
  14. }
  15. import Interpolators._
  16. val name = "Dean Wampler"
  17. val book = "Programming Scala, Second Edition"
  18. val jsonobj = json"{name: $name, book: $book}"
  19. println(jsonobj)

5 中缀表达法

在大部分情况下,中缀(Infix)标记和后缀(Postfix)标记可省略:1 + 2
这段语句大家再熟悉不过了,但是在 Scala 中,所有的表达式都是方法,实际上完整的写法,如下:1.+(2),即,+ 是整型对象(在 Scala 中,一切都是对象)的一个方法,如果了解到这里,会不会觉得能写成之前的样子很神奇。这里是 Scala 一个特性:

如果一个对象有一个带有一个参数的方法,它的中缀标记可以省略,在这里,点号和括号都可以省略。类似的,如果一个方法没有参数表,你调用它的时候,可以它的后缀标记(即括号)可以省略:

  1. scala> 1 toString // 1.toString()
  2. warning: there were 1 feature warning(s); re-run with -feature for details
  3. res2: String = 1

因为忽略后缀,有时候会让人很迷惑,方法?属性?傻傻分不清楚!所以在版本 2.10 之后,如果没有明确告诉编译器的话,会给出一个警告:

  1. scala> import scala.language.postfixOps
  2. scala> 1 toString
  3. res2: String = 1

警报解除,神奇吧?现在我们看一个复杂的例子:

  1. def isEven(n: Int) = (n % 2) == 0
  2. List(1, 2, 3, 4) filter isEven foreach println
  3. // 等同于 .filter(isEven).foreach(println)

6 简便的类声明:

Java 党可以看过来,不论是 IDE 和文本编辑器党,代码中到处充斥着大量的 setter 和 getter:

  1. // src/main/java/progscala2/basicoop/JPerson.java
  2. package progscala2.basicoop;
  3. public class JPerson {
  4. private String name;
  5. private int age;
  6. public JPerson(String name, int age) {
  7. this.name = name;
  8. this.age = age;
  9. }
  10. public void setName(String name) { this.name = name; }
  11. public String getName() { return this.name; }
  12. public void setAge(int age) { this.age = age; }
  13. public int getAge() { return this.age; }
  14. }

看看,Scala 可以这么写:

  1. class Person(var name: String, var age: Int)

有没有像流泪的感觉,哈哈,That is it!如果想要覆盖某些 setter 和 getter,只要类声明中,覆盖相应方法即可。有 var,自然有 val,如果你的对象属性都是不可变,那还可以使用如下声明:

  1. case class Person(name: String, age: Int)

用 Java 的同学会不会很心动啊?^_^

7 密封类型

密封类型(Sealed)强制其子类只能定义在同一个文件中,seals 关键字可以用在 class 和 trait 上。第一个作用如题,举个栗子,Scala 源码中 List 的实现用到 sealed 关键字:

  1. scala> class NewList extends List
  2. <console>:7: error: illegal inheritance from sealed class List
  3. class NewList extends List

这样子,妈妈再也不用担心我的类被人滥用。如果就这么个功能,怎么能称得上 Top 10 呢?在看一个黑魔法:

  1. scala> sealed abstract class Drawing
  2. defined class Drawing
  3. scala> case class Point(x: Int, y: Int) extends Drawing
  4. defined class Point
  5. scala> case class Circle(p: Point, r: Int) extends Drawing
  6. defined class Circle
  7. scala> case class Cylinder(c: Circle, h: Int) extends Drawing
  8. defined class Cylinder

如果你如之前,少写了其中一個案例:

  1. scala> def what(d: Drawing) = d match {
  2. | case Point(_, _) => "点"
  3. | case Cylinder(_, _) => "柱"
  4. | }
  5. <console>:14: warning: match may not be exhaustive.
  6. It would fail on the following input: Circle(_, _)
  7. def what(d: Drawing) = d match {
  8. ^
  9. what: (d: Drawing)String

编译器在告訴你,有些模式的类型你沒有列在 match 運算式的案例串(Case sequence)之中。你应该每個都列出來才合理:

  1. scala> def what(d: Drawing) = d match {
  2. | case Point(_, _) => "点"
  3. | case Circle(_, _) => "圆"
  4. | case Cylinder(_, _) => "柱"
  5. | }
  6. what: (d: Drawing)String

有時候,你使用別人密封過的案例类别,但也許你真的只想比对其中几個案例类型,如果不想要编译器饶人的警告,则可以在最后使用万用字元模式(_),例如:

  1. scala> def what(d: Drawing) = d match {
  2. | case Point(_, _) => "點"
  3. | case Cylinder(_, _) => "柱"
  4. | case _ => "" // 作你想作的事,或者丟出例外
  5. | }
  6. what: (d: Drawing)String

如果你真心不想要使用万用字元作额外处理,那么还可以可以使用 @unchecked 标注來告訴编译器住嘴:

  1. scala> def what(d: Drawing) = (d: @unchecked) match {
  2. | case Point(_, _) => "點"
  3. | case Cylinder(_, _) => "柱"
  4. | }
  5. what: (d: Drawing)String

参考:http://openhome.cc/Gossip/Scala/SealedClass.html

8 有趣的权限控制 private[]

protected 或 private 這表示权限限制到 x 的范围。

  1. class Some {
  2. private val x = 10
  3. def doSome(s: Some) = s.x + x
  4. }

对于大多数语言,访问控制就严格无非就这两种。在 Scala 中,可以更加严格,让 x 完全无法透过实例存取,则可以使用 private[this],這表示私有化至 this 实例本身才可以存取,也就是所謂物件私有(Object-private),例如以下就通不過编译了:

  1. class Some {
  2. private[this] val x = 10
  3. def doSome(s: Some) = s.x + x // 编译错误
  4. }

作为入门,就知道这里就可以了,可以跳到下一个话题了!

如果你看过 Spark(一个分布式计算框架)的源码,会发现这样的权限控制符到处都是,所以这个还是有必要搞清楚的。

如果能看懂下面这段代码,那你对 Scala 的域访问控制算是了解了:

  1. package scopeA {
  2. class Class1 {
  3. private[scopeA] val scopeA_privateField = 1
  4. protected[scopeA] val scopeA_protectedField = 2
  5. private[Class1] val class1_privateField = 3
  6. protected[Class1] val class1_protectedField = 4
  7. private[this] val this_privateField = 5
  8. protected[this] val this_protectedField = 6
  9. }
  10. class Class2 extends Class1 {
  11. val field1 = scopeA_privateField
  12. val field2 = scopeA_protectedField
  13. val field3 = class1_privateField // ERROR
  14. val field4 = class1_protectedField
  15. val field5 = this_privateField // ERROR
  16. val field6 = this_protectedField
  17. }
  18. }
  19. package scopeB {
  20. class Class2B extends scopeA.Class1 {
  21. val field1 = scopeA_privateField // ERROR
  22. val field2 = scopeA_protectedField
  23. val field3 = class1_privateField // ERROR
  24. val field4 = class1_protectedField
  25. val field5 = this_privateField // ERROR
  26. val field6 = this_protectedField
  27. }
  28. }

不懂也没关系,后面会花一个大篇幅来讲这块内容。

9 类型擦除和 implicit

  1. scala> :paste
  2. object M {
  3. def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")
  4. def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")
  5. }
  6. <ctrl-d>
  7. <console>:8: error: double definition:
  8. method m:(seq: Seq[String])Unit and
  9. method m:(seq: Seq[Int])Unit at line 7
  10. have same type after erasure: (seq: Seq)Unit
  11. def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")

由于历史原因,JVM 忘记了参数化类型的参数类型。例如例子中的 Seq[Int] 和 Seq[String],JVM 看到的只是 Seq,而附加的参数类型对于 JVM 来说是不可见,这就是传说中的类型擦除。于是乎,从 JVM 的视角来看,代码是这样子的:

  1. object M {
  2. // 两者都是接受一个 Seq 参数,返回Unit
  3. def m(seq: Seq): Unit = println(s"Seq[Int]: $seq")
  4. def m(seq: Seq): Unit = println(s"Seq[String]: $seq")
  5. }

这不就报错了吗?重复定义方法。那怎么办呢?(JAVA 语言的痛点之一)Scala 给你提供一种比较优雅的解决方案,使用隐式转换:

  1. scala> :paste
  2. object M {
  3. implicit object IntMarker
  4. implicit object StringMarker
  5. // 对于 JVM 来说是,接受 Seq 参数和 IntMarker.type 型 i 参数,
  6. // 返回 Unit
  7. def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit =
  8. println(s"Seq[Int]: $seq")
  9. // 对于 JVM 来说是,接受 Seq 参数和 StringMarker.type 型 i 参数,
  10. // 返回 Unit
  11. //
  12. def m(seq: Seq[String])(implicit s: StringMarker.type): Unit =
  13. println(s"Seq[String]: $seq")
  14. }
  15. <ctrl-d>
  16. scala> import M._
  17. scala> m(List(1,2,3)) // 在调用的时候,忽略隐式类型
  18. scala> m(List("one", "two", "three"))

10 异常捕捉与 Scalaz

先来看看,异常捕捉语句:

  1. try {
  2. source = Some(Source.fromFile(fileName))
  3. val size = source.get.getLines.size
  4. println(s"file $fileName has $size lines")
  5. } catch {
  6. case NonFatal(ex) => println(s"Non fatal exception! $ex")
  7. } finally {
  8. for (s <- source) {
  9. println(s"Closing $fileName...")
  10. s.close
  11. }
  12. }

这里是打开文件的操作,并且计算行数;NonFatal 非致命错误,如内存不足之类的非致命错误,将会被抛掉。finnaly 操作结束后,关闭文件。这是 Scala 模式匹配的又一大应用场景,你会发现倒是都是模式匹配:

  1. case NonFatal(ex) => ...

如果每条异常的处理语句只是单条的话,Scala 写起来应该会挺爽的。

在传统的异常处理,无法一次性汇总运行过程中的错误。如果我们在做表单验证的时候,我们就需要考虑到这个场景。而传统的做法就是通过一层层 try ... catch ...,把错误追加到一个列表中来实现,而 scalaz 中提供一个适用于这个场景的封装,直接看例子吧:

  1. import scalaz._, std.AllInstances._
  2. /* 验证用户名,非空和只包含字母 */
  3. def validName(key: String, name: String):
  4. Validation[List[String], List[(String,Any)]] = {
  5. val n = name.trim // remove whitespace
  6. if (n.length > 0 && n.matches("""^\p{Alpha}$""")) Success(List(key -> n))
  7. else Failure(List(s"Invalid $key <$n>"))
  8. }
  9. /* 验证数字,并且大于0 */
  10. def positive(key: String, n: String):
  11. Validation[List[String], List[(String,Any)]] = {
  12. try {
  13. val i = n.toInt
  14. if (i > 0) Success(List(key -> i))
  15. else Failure(List(s"Invalid $key $i"))
  16. } catch {
  17. case _: java.lang.NumberFormatException =>
  18. Failure(List(s"$n is not an integer"))
  19. }
  20. }
  21. /* 验证表单 */
  22. def validateForm(firstName: String, lastName: String, age: String):
  23. Validation[List[String], List[(String,Any)]] = {
  24. validName("first-name", firstName) +++ validName("last-name", lastName) +++
  25. positive("age", age)
  26. }
  27. validateForm("Dean", "Wampler", "29")
  28. validateForm("", "Wampler", "0")
  29. // Returns: Failure(List(Invalid first-name <>, Invalid age 0))
  30. //告知你名字和年龄填写有误
  31. validateForm("Dean", "", "0")
  32. // Returns: Failure(List(Invalid last-name <>, Invalid age 0))
  33. // 告知你姓氏和年龄填写有误
  34. validateForm("D e a n", "", "29")
  35. // Returns: Failure(List(Invalid first-name <D e a n>, Invalid last-name <>))
  36. // 告知你名字和姓氏填写错误
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注