第14章 断言和测试
- Predef的
assert(condition)
或assert(condition,explanation)
,condition如果不满足,则抛出AssertionError。explanation类型为Any,可以传入任何对象,将调用对象的toString打印输出。P258
scala> assert(false,"test error")
java.lang.AssertionError: assertion failed: test error
at scala.Predef$.assert(Predef.scala:170)
... 32 elided
- Predef的
ensuring(condition)
,这里的condition为结果类型参数并返回Boolean的前提条件函数(即condition为函数)。使用方法为{代码块}ensuring(condition(x))
,ensuring
将{代码块}
的结果x
传递给condition(x)
函数,如果condition(x)
为true,则ensuring
返回结果x
,否则抛出AssertionError。P259
def func(a:Int):String={
a.toString
}ensuring(_.length>2,"msg: 位数小于3")
scala> func(2)
java.lang.AssertionError: assertion failed: msg: 位数小于3
at scala.Predef$Ensuring$.ensuring$extension3(Predef.scala:261)
at .func(<console>:14)
... 32 elided
- ensuring一般用于内部测试,断言(assert, ensuring)可通过
JVM -ea -da
来分别打开或关闭。P260 - 外部测试,ScalaTest框架(官方指导),FunSuite风格。
- scalatest涉及三方jar包导入,idea的三方jar包导入方式(单个,批量),导入后即可正常
org.scalatest.FunSuite
(3.2.0版开始已经没有Funsuit
了,而是用funsuite.AnyFunSuite
(官方指导))。当然,也可以使用构建maven工程+pom.xml方式导入。(todo:有时间单独出一期直接导入和pom导入的教程)
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
import org.scalatest.funsuite
class MyTest extends funsuite.AnyFunSuite {
val s = "a b c d"
//assert 一般断言
test("长度大于4"){
assert(s.length > 4)
}
test("长度大于8"){
assert(s.length >8)
}
//assertResult 将预期与实际值区分开来;
val a = 5
val b = 2
assertResult(2) {
a - b
}
//assertThrows 以确保一些代码抛出预期的异常。捕获正确异常,则返回succeeded的Assertion,否则,返回TestFailedException
val s2 = "hi"
assertThrows[IndexOutOfBoundsException] { // Result type: Assertion
s2.charAt(-1)
}
//intercept和assertThrows 表现一样,但若捕获正常异常的话,则返回IndexOutOfBoundsException。
val caught =
intercept[IndexOutOfBoundsException] { // Result type: IndexOutOfBoundsException
s2.charAt(-1)
}
assert(caught.getMessage.indexOf("-1") != -1)
//带clue版本
assert(1 + 1 === 3, "this is a clue")
assertResult(3, "this is a clue") { 1 + 1 }
withClue("this is a clue") {
assertThrows[IndexOutOfBoundsException] {
"hi".charAt(-1)
}
}
}
第15章 样例类和模式匹配
- 样例类(case classes)和模式匹配(pattern matching)为我们编写规则的、未封装的数据结构提供支持。对于表达树形的递归数据尤为有用。P271
- 样例类的定义和普通类一样,只是在class前面增加了case:
// 类定义
case class 类名[(参数列表)]{//注意,区别于函数,没有=
//类的定义
}
case class A(name:String)
val a=A("test") //实例化
- 样例类case的使用,会添加一些语法上的便利:
- 会添加一个跟类同名的工厂方法。这样,创建实例的时候,就可以直接用
类名(参数)
,而不需要new 类名(参数)
P272- 实际上是自动创建了伴生对象,并添加了apply,用于不用new创建对象
- 伴生对象中添加了unapply方法,用于样例类的模式匹配
- 类参数都隐式获得了一个val前缀,直接变成了字段。也可指明为var型。可直接引用
a.name
。P273 - 编译器会帮我们以“自然"的方式实现toString、hashCode和equals方法(比较包含类及入参的整棵树)。可简单的认为,对于引用类型,
==
即equals,所以==
也被很好的实现了。P273 - 编译器会添加一个copy方法用于可修改部分内容的拷贝(通过
a.cpoy(arg1="新值")
实现),如果不修改任何东西,则用老对象中的原值。P273 - 最大的好处是他们支持模式匹配。
- 会添加一个跟类同名的工厂方法。这样,创建实例的时候,就可以直接用
- 模式匹配P274
选择器 match { 可选分支 }
//可选分支
case 模式 => 表达式
- 模式匹配中的模式:P275-286
- 常量模式,例如
"str1"
,123
,按照==
要求跟他们相等的值 - 变量模式,例如
case e => 表达式中可利用e
中的e,变量e可匹配任何值,主要是为了利用它(todo: 在使用时e的类型怎么保证?)- 任何字面量都可以当做常量(模式)使用
- 任何val和单例对象也可当做常量(模式)使用
- 例如Nil这个单例对象,能且仅能匹配空list
- 通配模式,即
_
,匹配任何值,但不利用这些值。 - 构造方法模式,例如
类名(参数)
,这样参数也可以是模式,从而减少繁琐的判断。例case ClassA("str1",123, e)=>对e做操作
。这样要求选择器是ClassA类型,而且第1个参数是"str1",第二个参数时123。 - 序列模式,如List或Array
- 元组模式
- 带类型的模式-可用于代替冗长的类型测试和类型转换。
- 类型擦除:类型参数信息不会被scala运行时保留,所以Map[Int,Int]和Map[,]是一样的,匹配不了里面的类型信息P285
- 类型擦除特例:Array是能保留的,因为scala对Array做了特殊处理P285
- 通配模式保底是必要的(当然也可用变量模式保底,只不过是给任何值取了个变量名),因为如果有没有匹配上的模式,会报MatchError错误。P278
- 常量模式,例如
exp match{
case "str1" | 123 => println("...") //常量模式
case e => println("...") //变量模式
case ClassA("str1",123, e)=> println(e) //构造方法模式
case List(0,_,_) | List(1,_*) => ... //序列模式
case (a,b,c) => ... //元组模式
case s:String | m:Map[_,_] | a:Array[String]=> ... //带类型的模式
case _ => //通配模式
}
- 常量模式和变量模式的冲突P279
- 为了防止冲突,scala会把以小写字母开头的名称当做模式变量处理(不取值,做新命名),其他的都是模式常量(可以理解成会先取值,再匹配)
- 小写字母开头的名称怎么当做模式常量处理呢?
- 加限定词 如
obj.pi
,this.pi
- 用反引号,如`pi`。
- 加限定词 如
- 变量绑定:除变量模式外,其他所有模式都可在匹配成功后绑定给一个变量。格式
e @ 模式
。 - scala要求模式都是线性的:同一个模式变量在模式中只能出现一次。P287
- 模式守卫:
if 变量相关条件表达式
,除了模式要匹配,同时还要满足if后面的表达式结果为true,才能匹配成功。P287 - 密封类。没有缺省行为,如何保证自己的枚举覆盖了所有场景?使用密封类。P289
- 密封类除了同一个文件里面定义的子类(用样例类case class实现)之外,不能添加新的子类
- 密封类的样例类做匹配时,如果遗漏了,编译器会给出警告
- 如果类打算被用于模式匹配,则推荐使用密封类
- 可以用缺省行为跳过部分样例类的检查,但是不是推荐方案,推荐方案是用
@unchecked
注解。编译器对后续模式分支的覆盖完整性检查就会跳过。
sealed abstract class Expr
case class Var(name:String) extends Expr
case class Number(num:String) extends Expr
def func1(e:Expr):String =(e:@unchecked) match{
case Var(_)=>"a number"
}//不会警告
- Option类型。有两种形式 Some(x)和None。用于防止处理无值时的报错情况。scala中的null是Null类型,是引用类AnyRef的子类,不是值类的子类。值类无值的时候(可用Unit的
()
表示),就不好统一处理。Option的None提供了一种统一处理方案。P292
//Option类的常用提取方法
def getValue(x:Option[String])=x match{
case Some(s) => s
case None => "?"
}
- 一切皆模式。并不仅限于match表达式。
- 变量定义
- case序列
{case ...;case ...}
这样的语句叫做case序列。可以用在任何允许出现函数字面量的地方,即当做函数使用。- case序列本质上就是一个函数字面量。
- case序列的每个case都是一个入口,每个入口都有自己的参数列表。
- case序列是一个偏函数(partial function),偏函数是指在某些值没有定义的函数,参数为这些值时会报错。简单理解偏函数和部分应用函数。偏函数定义为PartialFunction,则会自动添加是否有定义的函数isDefinedAt,使用前先判断是否有定义。
- for表达式
- 不能匹配给定模式的值会被直接丢弃(相当于省略了普通for语句的if子句)P297
// 变量定义中
val (numb,str)=(123,"abc")
val BinOp(op,left,right)=new BinOp("*",Number(5),Number(1)) //样例类解开
//作为偏函数的case序列
val signal: Int=> Int = {
//case 0 => 0
case x if x > 0 => x - 1
case x if x < 0 => x + 1
}
signal: Int => Int = <function1>
> signal(1)
res0: Int = 0
> signal(0)
scala.MatchError: 0 (of class java.lang.Integer)
at $anonfun$1.apply$mcII$sp(<console>:13)
... 32 elided
// 定义为PartialFunction,则会自动添加是否有定义的函数isDefinedAt
val signal: PartialFunction[Int, Int] = {
//case 0 => 0
case x if x > 0 => x - 1
case x if x < 0 => x + 1
}
signal: PartialFunction[Int,Int] = <function1>
scala> signal(0)
scala.MatchError: 0 (of class java.lang.Integer)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
at $anonfun$1.applyOrElse(<console>:11)
at $anonfun$1.applyOrElse(<console>:11)
at scala.runtime.AbstractPartialFunction$mcII$sp.apply$mcII$sp(AbstractPartialFunction.scala:36)
... 32 elided
scala> signal.isDefinedAt(0) //使用前先判断
res3: Boolean = false
// for 表达式
val a=List(Some("a"),None,Some("b"))
> for( Some(x)<- a) println(x) //None被自动跳过
a
b
第26章 提取器
本章相当于15章 的进阶版本,所以放在这里先讲。
- 提取器是拥有名为unapply成员方法的对象,即提取器是对象Object。P591
- 通常还会定义一个apply方法对应unapply的逆过程,非必选。
- unapply方法返回Option类型或Boolean类型(博主目前看到这两种,或许还有其他类型)。
- 该对象可以是伴生对象,那样的话,如果编写了apply方法,则不需要用new来创建对象了。
- 通过样例类(case classs)的学习,可以知道样例类实际上就是已经实现了apply和unaply伴生类
//实例1
class Student10 {
var name:String = _ // 姓名
var age:Int = _ // 年龄
// 实现一个辅助构造器
def this(name:String, age:Int) = {
this()
this.name = name
this.age = age
}
}
object Student10 {
def apply(name:String, age:Int): Student10 = new Student10(name, age)
// 实现一个解构器
def unapply(arg: Student10): Option[(String, Int)] = Some((arg.name, arg.age))
}
object App2 extends App {
val zhangsan = Student10("张三", 20) //因为apply方法的存在,不需要用new
zhangsan match {
case Student10(name, age) => println(s"姓名:$name 年龄:$age")//自动调用unapply方法解构后做模式匹配
case _ => println("未匹配")
}
}
//实例2,通过样例类的学习,可以知道样例类实际上就是已经实现了apply和unaply伴生类
case class Student10( name:String,age:Int)
object Student10 { //再用伴生对象就会和样例类自动生成的伴生对象冲突
def apply(name:String, age:Int): Student10 = Student10(name, age)
// 实现一个解构器
def unapply(arg: Student10): Option[(String, Int)] = Some((arg.name, arg.age))
}
object App2 extends App {
val zhangsan = Student10("张三", 20)
zhangsan match {
case Student10(name, age) => println(s"姓名:$name 年龄:$age")
case _ => println("未匹配")
}
}
报错:
ambiguous reference to overloaded definition,
both method apply in object Student10 of type (name: String, age: Int)org.example.Student10
and method apply in object Student10 of type (name: String, age: Int)org.example.Student10
报错:
method unapply is defined twice
conflicting symbols both originated in file 'D:\git\scala_test\pon_test\src\main\scala\org\example\Student10.scala'
case class Student10( name:String,age:Int)
// 实例3,样例类等价形式:
case class Student10( name:String,age:Int)
object App2 extends App {
val zhangsan = Student10("张三", 20)
zhangsan match {
case Student10(name, age) => println(s"姓名:$name 年龄:$age")
case _ => println("未匹配")
}
}
- Some(a,b)跟Some((a,b))是同一个意思P592。
- 一个更直观更单纯的提取器。P592
- 待匹配值和unapply的参数类型一致并不是必须的 P593
- 模式匹配逻辑首先检查给定值是否符合unapply的参数类型的要求,符合要求则转换成unapply的参数类型(博主推测是通过多态实现),不符合的话模式匹配就会失败。
object EMail{
def unapply(str:String):Option[(String,String)]={
val parts= str split "@"
if (parts.length == 2) {
Some(parts(0),parts(1))
}else None
}
}
//应用
val x: Any ="xlt@1243.com" //可转换为String
x match{
case EMail(user,domain)=> println(user,domain)
}
(xlt,1243.com)
scala> val x:Any=123//类型无法转换,匹配失败
scala> x match{
| case EMail(user,domain)=> println(user,domain)
| }
scala.MatchError: 123 (of class java.lang.Integer)
... 34 elided
- 提取0个参数的模式。
- unapply返回False表示匹配失败
- 不带参数的模式需要加上
()
,否则变成比较对象的相等性了 P595
- 多个提取器嵌套时,从外层匹配到内层。如
case Email(Twice(x @UpperCase()),domain)
P595 - 变长参数匹配。定义
unapplySeq
方法,返回类型必须是Option[Seq[T]]
类型。
object Domain{
def unapplySeq(whole:String):Option[Seq[String]]={
Some(whole.split("\\.").reverse)
}
}
//使用
"www.baidu.com" match {
case Domain("com", x @ _*)=>true //取到的x是WrappedArray[T] 类型
case _=> false
}
- 提取器 vs 样例类P600
- 样例类的缺点:把数据的具体类型暴露给使用方。如
case C()
中的C。如果样例类被别人使用并包含了模式匹配,重命名或者改名样例类的继承关系都会影响使用方。提取器则没有这个问题。 - 如果要暴露给别人使用,则考虑从提取器开始、
- 如果看不出哪个好,从样例类开始,因为样例类更简单。
- 样例类的缺点:把数据的具体类型暴露给使用方。如
- 正则表达式。P603
- raw字符串表示:
"""any string"""
- 正则表达式的类型:
scala.util.matching.Regex
。两种创建方法:P602val r=new Regex("""any string""")
"""val r=any string""".r
- 正则表达式定义了一些常用方法。findFristIn,finddAllIn findPrefixOf等,返回Option或者Iterator类型。
- 每个正则表达式都定义了一个提取器。正则表达式的group对应提取器
unapplySeq
返回的Seq的每个元素。
- raw字符串表示:
val R="""(-)?(\d+)(\.\d*)?""".r
val R(s,i,d)="-1.23" //使用提取器的模式匹配
其他
1、存在性类型:Existential types
def foo(l: List[Option[_]]) = ...
2、高阶类型参数:Higher kinded type parameters
case class A[K[_],T](a: K[T])
3、临时变量:Ignored variables
val _ = 5
4、临时参数:Ignored parameters
List(1, 2, 3) foreach { _ => println("Hi") }
5、通配模式:Wildcard patterns
Some(5) match { case Some(_) => println("Yes") }
match {
case List(1,_,_) => " a list with three element and the first element is 1"
case List(_*) => " a list with zero or more elements "
case Map[_,_] => " matches a map with any key type and any value type "
case _ =>
}
val (a, _) = (1, 2)
for (_ <- 1 to 10)
6、通配导入:Wildcard imports
import java.util._
7、隐藏导入:Hiding imports
// Imports all the members of the object Fun but renames Foo to Bar
import com.test.Fun.{ Foo => Bar , _ }
// Imports all the members except Foo. To exclude a member rename it to _
import com.test.Fun.{ Foo => _ , _ }
8、连接字母和标点符号:Joining letters to punctuation
def bang_!(x: Int) = 5
9、占位符语法:Placeholder syntax
List(1, 2, 3) map (_ + 2)
_ + _
( (_: Int) + (_: Int) )(2,3)
val nums = List(1,2,3,4,5,6,7,8,9,10)
nums map (_ + 2)
nums sortWith(_>_)
nums filter (_ % 2 == 0)
nums reduceLeft(_+_)
nums reduce (_ + _)
nums reduceLeft(_ max _)
nums.exists(_ > 5)
nums.takeWhile(_ < 8)
10、偏应用函数:Partially applied functions
def fun = {
// Some code
}
val funLike = fun _
List(1, 2, 3) foreach println _
1 to 5 map (10 * _)
//List("foo", "bar", "baz").map(_.toUpperCase())
List("foo", "bar", "baz").map(n => n.toUpperCase())
11、初始化默认值:default value
var i: Int = _
12、作为参数名:
//访问map
var m3 = Map((1,100), (2,200))
for(e<-m3) println(e._1 + ": " + e._2)
m3 filter (e=>e._1>1)
m3 filterKeys (_>1)
m3.map(e=>(e._1*10, e._2))
m3 map (e=>e._2)
//访问元组:tuple getters
(1,2)._2
13、参数序列:parameters Sequence
_*作为一个整体,告诉编译器你希望将某个参数当作参数序列处理。例如val s = sum(1 to 5:_*)就是将1 to 5当作参数序列处理。
//Range转换为List
List(1 to 5:_*)
//Range转换为Vector
Vector(1 to 5: _*)
//可变参数中
def capitalizeAll(args: String*) = {
args.map { arg =>
arg.capitalize
}
}
val arr = Array("what's", "up", "doc?")
capitalizeAll(arr: _*)