《scala 编程(第3版)》学习笔记

持续更新中…

第2章 入门1

函数式编程的核心一,函数是一等的。P9函数式编程的核心二,程序操作应当将输入映射成输出。不要有副作用。副作用是指改变了环境量。P10。原因:函数相互不纠缠,更可靠,更容易复用。P38函数是递归的,必须显示给出函数的结果类型。P25函数字面量: 参数列表=> 函数体,加参数类型时,必须用括号。(x:Int)=>2*x。 P31。若类型可靠类型推断实现,则可省略括号。a.foreach(x=>x+2)。P147。函数字面量只接收单个参数时,可不写参数。arr.forecch(println)。P31

第3章 入门2

new用来创建对象或类的实例。实例可参数化可接收类型和参数:val a=new 对象名[类型](参数),如 val a=new Array[Int](3)。P33通用规则:如果方法只接收一个参数,则调用他它的时候,可不用.和(),如 0 to 2为 (0).to(2)。P35 这叫操作符表示法,任何方法都可以是操作符,若有多个参数,则可把多参数写在()里,如a fun (c,d)。P79通用规则:a(i)实际上是a.apply(i)的简写。i可换成多值。P36通用规则:a(i)="hello"实际上是a.update(i,"hello")。P36注意区分类的方法和对象的方法。P37Array可变,List不可变。a(1)=2可以,l(1)=2不可以。P38List操作:1)拼接 l1_2=l1:::l22)前面添加l2=3::l1,常量时间完成3)追加: l2=l1:+3少用!列表越长追加时间越长。用追加的话可用ListBuffer再转List。P40右操作元:方法名一冒号:结尾,是右操作元,3::l1//l1.::(3)P39Nil可表示空列表,即List(),l1=1::2::3::Nil等价于’''l1=List(1,2,3)```P40List方法:count,init,tail, forall,sort。fill, tabulate。 P42元组:也不可变,和List不同的是可装不同元素。P42元组一个用处是函数返回多个对象时使用。P42元组用._N取值,原因:t(N)形式时t.apply(),apply要求返回一致的类型,而元组支持不同类型返回。P43+=: a+="1",有的是简写a=a+"1",有的是方法a.+=("1")。P45->: a -> b 实际上是 a.->(b),返回的是元组(a,b)。scala允许任何对象调用->,通过隐式转换实现。P47用reduce方法来减少var的使用,如 arr.reduceLeft((a,b)=>b)。P53

第4章 类和对象

(第10章 组合和继承 会有更多详细的介绍)

类的成员包括 字段(field)和方法(method) P57private 指明私有字段,只能被类内方法访问。P58不当做断句:1)行末是句点或中缀操作符 (+,-,*等) 2)下一行以不能作为语句开头的词开头 3)行尾在()和[]内。P62单例对象用来实现类似java类的静态成员能力。和类的定义很像,把class换成object。当有类名和单例对象同名时,单例对象叫伴生对象,类叫伴生类。他们可以相互访问对方私有成员,且必须在同一个源码文件中定义。P63类的实例,无法访问伴生对象中的方法。P63单例对象不接受参数,不能被new实例化,只在代码首次访问时才被初始化。P65没有伴生类的单例对象叫孤立对象,孤立对象一般用于把方法归集到一起或者定义应用程序入口。P65所有应用程序的对象,需要有main函数,且结果输出为Unit。P65脚本需要以计算结果表达式结尾,否则以脚本方式跑脚本会报错。P66fsc来编译非脚本文件。编译后,可直接 scala 类名 参数来调用scala应用程序。P67App特质可省略main外壳。直接args使用参数。P68类的定义可以为空,即class A和 class A{}都可以,是一样的。P272

// 类定义

class 类名[(参数列表)]{//注意,区别于函数,没有=

//类的定义

}

// 单例对象

object 类名{ //注意,不接受参数

//类的定义

}

//例子

class A{val a=1}

// 继承

class A(a:Int,..) extends B {

...

}

// 类的参数问题

//定义时带参数

class A(a:Int,b:Int)={

//直接使用a,b

}

val c1=new A(1,2)

val c2= new A //报错

//通过辅助构造函数

class A={

def this(a:Int,b:Int)={

this()//必须先调用主构造函数

//直接使用a,b

}

}

val c1= new A(1,2)

val c2= new A //也存在

第5章 基础类型和操作

Int,Long,Short,Byte整数字面量有两种,0xca(或0Xca十六进制),123(十进制)或123L。P71shell总是以十进制打印整数。P71赋值给Short和Byte,只要在合法取值内即可。val a:Byte=23。P72浮点字面量:十进制数+小数点(+e/E+F/D)如:12.34e13D。P72字符字面量:'原字符'或'\uUnicode码'。P73Unicode码可出现在scala程序的任何位置,本意时让非ASCII的Unicode字符在源文件中也能用ASCII码表示。val B\u0041\u0044=1等价于val BAD=1。P74"""|原生字符串“”“.stripMargin有较多坑,不同环境调用的换行符不同(\n或\r\n),导致预期不符。P75符号字面量(略)P75布尔字面量 true,false。P76字符串插值。P77

s: s" abc\ndtef${表达式}"。raw: 和s一样,只不过里面的\不转义,raw" abc\ndtef${表达式}"。f: java.util.Formatter中给出的语法。例f"${math.Pi}%.5f"。默认为%s。 任何方法都可以是操作符。P79前缀操作符是unary_操作符的简写,unary(一元的),如-7 为 7.unary_-。仅-、+、!、~可用作前缀操作符。自定义前缀操作符也只能选这四个,如自定义一个unary_!方法。P80后缀操作符是不接受参数,且在调用时不用括号和句点的方法。如7 toLong。P81约定俗成:如果方法有副作用,则要保留括号。如println()。如果没有副作用,则可用后缀操作符法表示。如s toLowerCase。P82&&和||是短路的,&和|是非短路的。P83位操作:按位与&,按位或|,按位异或^,按位取反~ P85位移操作:<<左移,>>右移,>>>无符号右移。<<和>>>自动填充0,>>用左侧最高位填充。P85。相等性:==,!=可放心用于所有对象,已做好了null值处理,如a == null。P87操作符优先级:为保证程序可读性,建议老实用括号标出优先级。复杂难懂的直接查资料。P89富操作:scala的基础类都可自动隐式转换成富类型,从而获得更多方法。如0 max 5就是Int类型隐式转换成了scala.runtime.RichInt。P92

第6章 函数式对象

本章主要讨论不可变对象。

不可变对象优势P95:

容易推理,时不变。自由传递。可变对象则需做保护式拷贝。可做哈希表里的键。 劣势P95:

不能做局部更新,每次都要完整拷贝。 主构造器、辅助构造器与私有构造器Scala中,每个类都有主构造器,但并不以this定义。主构造器会执行类定义中的所有语句,即定义时{}中的所有内容。

class YourClass{

...

}

辅助构造方法以def this(...)开始。每个辅助构造器都必须以一个对先前已定义的其他辅助构造器或主构造器的调用开始。P102

// 构造器

class Student {

private var name = " "

private var age = 0

def this(name: String){ //辅助构造器1

this() //调用主构造器

this.name = name

}

def this(name: String,age: Int){ //辅助构造器2

this(name) //调用前一个辅助构造器

this.age = age

}

}

require函数的使用。P98类的类参数不能直接引用,想要实例引用参数,则要把它们做成字段。P99

scala> class Rational(n:Int,d:Int)

defined class Rational

scala> val a=new Rational(1,2)

a: Rational = Rational@5c669da8

scala> a.n

:14: error: value n is not a member of Rational

a.n

自引用。关键字this指向当前执行方法的调用对象。有时this可省略,有时不可省。P100私有化。private字段标识,只能在类定义内部访问,外部是访问不到的(博主注:即通过实例或者类名无法访问)。P103。复习:伴生对象和伴生类可相互访问私有成员。 私有成员不能被继承。定义操作符。def 操作符,如def +(that: Cls){},定义出来的操作符,任遵守操作符优先级。如a+b*c,还是a+(b*c)。Scala的常量一般用首字母大写驼峰命名。方法与函数:Scala 方法是类的一部分,而函数是一个对象可以赋值给一个变量。换句话来说在类中定义的函数即是方法。

//定义,:,=都不是必须的

def functionName ([参数列表]) [: [return type]] [[=][方法实现]

//例:

def a: Int={1+1}

def a={1+1}//等价

def a{1+1}//等价

第7章 内建的控制结构

Scal所有控制结构都会返回某种值。P114if语句:

if(布尔表达式 1){

// 如果布尔表达式 1 为 true 则执行该语句块

}else if(布尔表达式 2){

// 如果布尔表达式 2 为 true 则执行该语句块

}else {

// 如果以上条件都为 false 执行该语句块

}

if子句中的返回值类型必须一致或为继承关系(最终返回父类类型)。P215while语句,返回单元值Unit,写做()P117:

//while

while(condition)

{

statement(s);

}

//do...while

do {

statement(s);

} while( condition );

赋值语句也返回单元值。如a='1'返回(),单元值和所有值用==比较都返回true。P118

var a=1

() == (a=3)

:13: warning: comparing values of types Unit and Unit using `==' will always yield true

() == (a=3)

^

res4: Boolean = true

//try表达式能返回子句值

val k=try{val c=1 }

k: Unit = ()

for语句知识点。

生成器:i<- 集合 叫生成器P120过滤器:可包含多个过滤器,过滤器由 if condition(i)组成。P121多个生成器一起使用,可得到嵌套循环。P122中途变量,使用和val一样。P123可用{}代替(),实现自动;推断,不然()要手工用;来断句。P122if过滤器前的;可省略(很奇怪)。如 for(a<- arr if a>1)。yield产生新的集合,元素类型由yield后的代码体自有确定。for 子句 yield 代码体。P124完整子句包含生成器;定义(中途变量);过滤器。用{}可不写分号。P483

for{

a<- arr1

m1=a+10

if a>1

b<- arr2

m2=b+10

if b<2

} yield {

(a,m1,b,m2)

}

//用括号

for(

a<- arr1; m1=a+10; if a>1;

b<- arr2; m2=b+10; if b<2 ;

) yield {

(a,m1,b,m2)

}

//推荐-用大括号-自动加分号功能只能在最后加

for{

a<- arr1; m1=a+10; if a>1

b<- arr2; m2=b+10; if b<2

} yield {

(a,m1,b,m2)

}

异常捕获:

scala异常清单throw 表达式会返回Nothing。P126try-catch返回值:无异常,返回try子句结果;有异常且被捕获,返回catch子句结果;有异常没被捕获,则无结果,因为异常向上扩散,该处无法继续执行下去。P128正常情况finally返回的值会被丢弃,除非显示的在finnally里写了return。则会覆盖try-catch的返回值。P129捕获所有异常

try {

val f = new FileReader("input.txt")

} catch {

case ex: FileNotFoundException =>{

println("Missing file exception")

}

case ex: Execpion=> {

println("通常这个Exception就能捕获几乎所有异常了")

}

} finally {

println("做些副作用操作,如清理数据,关闭文件等")

//通常不要return value

}

"""

Exception is programmatically recoverable. Its subclass RuntimeException indicates a programming error and is usually not to be caught as well. Throwable is super class of Exception as well as Error. In normal cases we should always catch sub-classes of Exception, so that the root cause doesn't get lost.

如果下面的写法捕获不到异常,可以使用case e:Throwable尝试捕获所有异常

catch {

case e:Exception => {

e.printStackTrace()

}

}

Thowable catches really everything even ThreadDeath which gets thrown by default to stop a thread from the now deprecated Thread.stop() method. So by catching Throwable you can be sure that you'll never leave the try block without at least going through your catch block, but you should be prepared to also handle OutOfMemoryError and InternalError or StackOverflowError.

Catching Throwable is most useful for outer server loops that delegate all sorts of requests to outside code but may itself never terminate to keep the service alive.

"""

match表达式:

break隐含。P130返回匹配值后面的子句值。P130通配符(_)用于表示任意值。P130如果各子句值类型不一样,返回的是各子句值的父类型(很可能是Any)多个条件在一个case中,可用|分开

it match{

case "a" => dosometing1()

case "b" => dosometing2()

case 123 | "str1" => "ok"

case _ => dosometing3()

}

一定要用break时,也可break foreach函数。

// 导入以下包

import scala.util.control._

// 创建 Breaks 对象

val loop = new Breaks;

// 在 breakable 中循环

loop.breakable{

// 循环

for(...){

....

// 循环中断

loop.break;

}

arr.foreach{

loop.break;

}

}

花括号会引入一个新的作用域。P136没有副作用的函数,更容易进行单元测试。P138

第8章 函数与闭包

对象内的函数叫方法。P140函数式编程设计原则是尽量将程序分解成多个小函数,每个小函数完成特定功能。我们称这些小函数为助手函数。P142助手函数会污染命名空间。解决方法1是用私有函数。方法2是用局部函数。P143

def func1(a,..)={

def local_func1(b,..)={a}

}

private修饰符只用在类成员上,局部函数前不需要。P143局部函数可直接访问父函数的参数。P144函数字面量在运行时,被实例化成函数值。函数字面量支持多条语句,用{}即可。P144

(x:Int)=>{

...

}

函数字面量在实例化时,会实例化成FunctionN特质的类。N表示参数个数。P144下划线"_"占位符,在函数字面量中可用来表示多个参数。只有当参数在函数字面量中仅出现一次时才能使用。第一个下划线表示第一个参数,第二个表示第二个参数,依此类推。当无法自动推断类型时,需手动标注类型。P148

a.filter(x=>x>0)

a.filter(_>0) //等价于

val f = (_:Int)+(_:Int)

部分应用函数:

Scala中的偏函数(patial function)与部分应用函数(partial applied function)是不同的概念,偏函数(patial function)在模式匹配一章有介绍,简而言之,偏函数是在某些值没有定义的函数;部分应用函数是一个函数有N个参数,我们固定某些参数(或不固定而是替换整个参数列表),提供小于N个参数的函数。部分应用函数是一个表达式,这个表达式中部给出函数需要的所有参数,只给出部分或完全不给。部分应用函数返回的是一个函数,如sum _ 或sum(1,_: Int,2)“_”也可替换整个参数列表。P149注意理解,为何a.foreach(println)可这么精简。博主认为不用从部分应用函数角度理解,因为在需要函数作为参数的地方,就可以直接填一个函数。P151

scala> def sum= (_:Int) + (_:Int) + (_:Int)

sum: (Int, Int, Int) => Int

scala> def sum1(a:Int,b:Int,c:Int)=a+b+c

sum1: (a: Int, b: Int, c: Int)Int

//注意,定义函数时,不要用部分应用函数,不然相当于包一层sum()

scala> val sum2= (_:Int) + (_:Int) + (_:Int)

sum: (Int, Int, Int) => Int =

scala> val a =sum _

a: () => (Int, Int, Int) => Int =

//注意这个差异

scala> val a1=sum1 _

a1: (Int, Int, Int) => Int =

scala> a(1,2,3)

:14: error: too many arguments for method apply: ()(Int, Int, Int) => Int in trait Function0

a(1,2,3)

^

scala> a()(1,2,3)

res4: Int = 6

scala> a1(1,2,3)

res5: Int = 6

//以下,sum和sum1表现一致

scala> val b= sum(1,_:Int,3)

b: Int => Int =

scala> b(2)

res3: Int = 6

scala> val c=sum(1,_)

^

:12: error: not enough arguments for method apply: (v1: Int, v2: Int, v3: Int)Int in trait Function3.

Unspecified value parameter v3.

val c=sum(1,_)

^

scala> val c=sum(1,_:Int,_:Int)

c: (Int, Int) => Int =

scala> val d=sum(_:Int,2,_:Int)

d: (Int, Int) => Int =

函数作为一等的,只有在明确需要函数作为参数的地方可以直接用函数名func,其他地方都要写成 func _ 或者 func(_)的形式,因为这样返回的是一个部分应用函数。P151闭包:

运行时,会引入自有变量的函数字面量创建出来的函数值,叫做闭包。P154闭包能感知自由变量的变化,对自由变量的改动也会反应到闭包外。P154闭包引用的自由变量的值,为闭包被创建时变量活跃的值。P155被返回的闭包,会使得自由变量在堆上继续存活。P155博主认为,直接用函数的方式,也能实现闭包。见如下代码。

var more=3

//闭包

val f=(x:Int)=> x+more

f(1) //4

more=10

f(1) //11

//函数方式

var more=3

def f4(x:Int)= x+more

f4(1) //4

more=10

f4(1) //11

//闭包引用的自由变量的值,为闭包被创建时变量活跃的值。

def f2(more:Int)= (x:Int)=> x+more //该函数每调用一次,就会返回一个闭包

val c1=f2(1)

val c999=f2(999)

c1(1) //2

c999(1) //1000

//局部函数方式

def f3(more:Int)={

def closure(x:Int)={

x+more

}

closure(_) //只写closure会报错,因为只有明确需要一个函数参数的地方,参能只写函数。

//Unapplied methods are only converted to functions when a function type is expected.

//You can make this conversion explicit by writing `closure _` or `closure(_)` instead of `closure`.

}

val c1=f3(1)

val c999=f3(999)

c1(1) //2

c999(1) //1000

重复类型参数。类型后加*,如下示例。实际上再func函数内部,args的类型是Array[T]。但又不能直接把Array类型的变量,如arr当做参数给到函数,会报错。应该要 func(arr:_*)解开。

def func(args:String*)={

...

}

尾递归。只有在最后一步调用自己的函数,才能享受scala的尾递归优化。优化位类似使用了while的优化,使得所有调用都在同一个栈帧中执行,不会像其他语言每次调用都构建一个新的栈帧。P160

//如下两个函数,在编译后,字节码相同。

def approximate(gusess:Double):Double={

if(isGoodEnough(guess)) guess

else approximate(improve(guess))

}

def approximateLoop(initialGuess:Double):Double={

var guess=initialGuess

while(!isGoodEnough(guess))

guess=improve(guess)

guess

}

第9章 减少代码重复

利用高阶函数(即接收函数作为参数的函数)减少代码。P164函数参数的表示法:

func:(Sting,String)=>Int //有参数

func: () => Int //无参数

func: => Int // 传名函数

函数柯里化。func(a:Int,b:Int)->func(a:Int)(b:Int) 柯里化实际是做了两次传统函数调用。P171

def func(a:Int,b:Int)= a+b

//柯里化定义

def func(a:Int)(b:Int)= a+b

//第一次

def first(a:Int)= a + (_ : Int)

val second = first(1)

//第二次

second(2) // 3

//从func中引用“第二步”函数

val second = func(1)_

柯里化的作用:隐式转化,更方便的组装函数参数为单个参数时,scala允许用{}代替(),目的是为了方便程序员在{}中编写函数字面量。P175传名参数用于替代函数参数无参数的表示。func:()=>Int 表示成 func:=>Int。传名参数完全可用传统参数替代,如funcValue:Int。但这种表达在调用函数前会被先求值,只有在明确不希望调用前参数先求值的场景下,传名参数才有用。P177

补充: 由于函数和字段在同一个命名空间,所以可以把无参函数和字段相互替换使用。即val a=”3”,可以把a当作一个无参函数使用。说明这点的作用是在做scala测试的时候,ScalaTest框架,会把测试代码当作无参函数传入test函数中。这样也利用了传名函数不会马上计算结果的特征。P260

//验证:

scala> def strToInt(s: => String) = {s.toInt}

strToInt: (s: => String)Int

scala> val a ="3"

a: String = 3

scala> def f:String="3"

f: String

scala> strToInt(a)

res9: Int = 3

scala> strToInt(f)

res10: Int = 3

scala> strToInt("3") //可以看到,字面量”3“也可当作无参函数使用。

res11: Int = 3

第10章 组合和继承

组合是指一个类包含对另一个类的引用,并头通过引用类来帮助完成任务。继承只超类/子类的关系。P179一个方法只要没有实现(没有等号和方法体),它就是抽象的。P181包含抽象成员的类,要声明为抽象类。abstract class 类名 P181抽象类不能被实例化。P181

scala> class A{def func:Int } // abstract class A{def func:Int } 才对

:11: error: class A needs to be abstract, since method func is not defined

class A{def func:Int }

//用了空方法体也不是抽象类

scala> class A{def func:Unit={} }

defined class A

无参方法、空括号方法、字段:

无参方法:调用时只能用无参形式,建议无副作用的方法定义成无参方法。P182空括号方法:调用时可用无参形式和空括号形式,建议有副作用的调用使用空括号。P184字段:访问比方法调用快,但会为每个对象分配额外内存空间。用字段还是方法,取决于类的用法。P183

scala> def func:Int= 7

func: Int

scala> func

res0: Int = 7

scala> func()

:13: error: Int does not take parameters

func()

^

scala> def func1():Int= 7

func1: ()Int

scala> func1

res2: Int = 7

scala> func1()

res3: Int = 7

类的创建

class 类名[(参数列表)]{构造函数主体}//注意,区别于函数,没有=

//例子

class A{val a=1}

继承

class A(a:Int,..) extends B {

...

}

类的参数问题

//定义时带参数

class A(a:Int,b:Int)={

//直接使用a,b

}

val c1=new A(1,2)

val c2= new A //报错

//通过辅助构造函数

class A={

def this(a:Int,b:Int)={

this()//必须先调用主构造函数

//直接使用a,b

}

}

val c1= new A(1,2)

val c2= new A //也存在

子类的指可以被用在任何需要超类的值的地方。P186

val b:B_super=new B_subtype(1,2,3)//b的表现通过多态自动匹配

scala命名空间有两个P187:

值(字段,方法,包和单例对象):所以可用字段重写无参方法。类型(类和特质名)

class A{

def a:Int

}

class B extends A{

val a=2//将方法重写成字段

//override val a =2 //若A中a非抽象

}

参数化字段,类的参数中带val和var 前缀的字段,是同时定义参数和同名字段的简写方式。参数化字段前还可以加修饰符,如overide,private。P188

// 使用参数化字段

class A(val a:Int){

...

}

//等价于:

class A(e1:Int){

val a=e1

...

}

要调用超类的构造方法,秩序将你打算传入的入参放在超类名称后的括号里即可。P190

class A1(a:Int) extends A(a-1){

...

}

//若超类没有入参,则不传参,也自动调用其构造方法?

override的使用:只要超类成员不是抽象的,子类在重写该成员时都要加上override,否则会报错。P190多态和动态绑定。

多态是指超类的变量可以指向子类的对象。这一类多态也叫子类型多态。动态绑定:变量调用的方法是随运行时的状态来定的。具体来说:如果子类对象有定义,则调用子类的方法,如果无定义,则向上寻找父类的方法。P193

//A有实现func_a

val a1:A=new A1//A1有实现func_a1

val a2:A=new A2//A2有实现func_a2

a1.func_a1

a1.func_a2//报错

a1.func_a

final:声明成员不被子类重写(还是可以被继承),或者类不能被继承。P195

和private的区别:private不能被继承,且类的实例无法访问。而final声明的成员,可被继承,且实例可以访问。

scala> class A{final val a=10}

scala> class A1 extends A{val a1=2}

scala> val c=new A1

c: A1 = A1@6fd1660

scala> c.a

res2: Int = 10

scala> c.a1

res3: Int = 2

组合 or 继承?P196

追求代码复用,选组合。a. is-a的关系,即A1是一个A。 b. 要把子类当作超类来用(使用多态能力) ,选继承。 工厂对象:可以将创建对象的逻辑集中起来,不用new 来创建对象,而是用方法将new 包起来。工厂对象一般用伴生对象实现。

object A {

def elem(a:Int):A={

new A1(a)

}

def elem(a:Array[String]):A={

new A2(a)

}

}

//使用:

import A.elem

val a1=elem(2)

val a2=elem(Array("a","b"))

第11章 Scala的继承关系

顶部的Any类定义了:==,!=,equals,##,hashCode,toString等方法P208,有两个子类AnyVal和AnyRef。P210AnyVal包括Byte,Short…Boolean,Unit这9个内建类,前八个的实例在scala中写作字面量,运行时用java基本类的值来表示。不能用new创建(类定义为final抽象类实现)。P210Unit只有一个实例值,写作()。P210对象的相等性。P210值类空间时扁平的(即相互之间并没有子类关系)。不同的值类类型之间存在隐式转换(21章继续讨论)。AnyRef是所有引用类的基类。P212 补充:值类型(AnyVal)和引用类型(AnyRef)的不同。 底部类:scala.Null和scala.Nothing。

Null的实例是null,所有引用类的子类。不能赋值给值类。 val i:Int=null报错。P215Nothing是所有类的子类,包括值类和引用类。用来兜底。P215 自定义值类:P216

有且仅有一个参数在内部除了def之外,没有任何其他东西。不能重新定义equals或hashCode。 自定义值类可用来防止参数顺序错误,错了会报错。P218

class Anchor(val v:String) extends Anyval

class Style(val v:String) extends Anyval

class Text(val v:String) extends Anyval

class Html(val v:String) extends Anyval

第12章 特质

特质的特点:

默认超类为AnyRef。P221可以用extends或with混入: I. extends混入,则类隐式集成特质的超类。P221 II. with混人,对特质超类有要求吗?有,被混入的特质的超类,必须是要混入特质的类的超类(可以是其往上几层的超类)。P231总结:混入特质的类,特质的超类也必须是其超类(可以是其往上几层的超类)。定义特质的同时也定义了一个类型,所以特质也可以当做类型来使用。P221多个特质有相同方法时,会发生什么?会冲突报错,可通过在新类中重写冲突函数解决。

trait Trait1{

def echo()={

println("echo Trait1")

}

}

class Frog extends Trait{

...

}

val t:Trait1=new Frog //其中Frog混入了特质Trait1,且t可以由任何混入了特质Trait1的类的初始化

//超类要求:

scala> class A{def echo()=println("echo A")}

scala> trait T1 extends A{override def echo()=println("echo T1")}

scala> class A1(val a1:Int) extends A

scala> class AA1(val a2:Int) extends A1(2)

scala> class AA1(val a2:Int) extends A1(2) with T1 //T1的超类是AA1往上两层的超类

scala> val aa1=new AA1(2)

scala> aa1.echo

echo T1

// 相同函数冲突,会报错

scala> trait T1 extends A{def echo1()=println("echo T1")}

scala> trait T2 extends A{def echo1()=println("echo T2")}

scala> class AT extends A with T1 with T2

:14: error: class AT inherits conflicting members:

method echo1 in trait T1 of type ()Unit and

method echo1 in trait T2 of type ()Unit

(Note: this can be resolved by declaring an override in class AT.)

class AT extends A with T1 with T2

scala> class AT extends A with T1 with T2{override def echo1()=println("echo AT")} //在新类中重写冲突函数即可。

scala> val at=new AT

scala> at.echo

echo A

特质 vs 类:几乎一样,除了以下亮点:

特质不能有"类"参数(但有办法绕过)P223类的super是静态绑定,而特质的super是动态绑定的。P223

class A(a:Int,b:Int)

trait T(a:Int,b:Int)//报错,20.5节有怎样绕过这个限制

Ordered特质,实现compare即可让混入的类使用>,<,>=,<=,但不会定义equals方法。

class YourClass extends Ordered[YourClass]{

//...

def compare(that:YourClass)= this.num-that.num//该函数返回结果:负数表示this小于that,0表示相等,正数表示this大于that

}

特质可叠加修改:

super调用是动态绑定的可叠加修改的特质,必须将叠加修改的方法标记位abstract override(仅限特质使用该标记),含义是该特质必须混入某个拥有该方法具体定义的类中。P231可在定义或者new 实例化时混入特质。P232混入顺序很重要,最右边的方法先被调用,然后依次往左。P233线性化:同级特质,按定义先后顺序决定线性化顺序。P237

abstract class A{

def get()

def put(x:Int)

}

trait Doubleing extends A {

abstract override def put(x:Int)= {super.put(2*x)}

}

trait Incrementing extends A {

abstract override def put(x:Int)= {super.put(x+1)}

}

trait Filtering extends A {

abstract override def put(x:Int)= {if(x>=0) super.put(x)}

}

class MyA extends A {

val buf=new ArrayBuffer[Int]

def get()=buf.remove(0)

def put(x:Int)= {buf+=x}

}

val i= new MyA with Doubleling //创建时混入特质

i.put(10)

i.get()//20

val i1= new MyA with Incrementing with Filtering

i1.put(-1)

i1.get()//None

val i1= new MyA with Filtering with Incrementing

i1.put(-1)

i1.get()//0

用不用特质?P238

如果某个行为不会被服用,用具体的类。如果某个行为可能被多个不同的类服用,则用特质如果要从java代码中继承某个行为,则用抽象类。如果计划将某个行为以编译好的方式分发,且预期会有外部的组织编写继承它的类,则倾向于用抽象类。如果考虑完后没答案,从使用特质开始。

第13 章 包和引入

将代码放入包里有两种方式。P240

文件顶部放一个package子句。package子句后加{}代码块。这种方法可方便地把多个包放在一个文件中。所有包的顶层包都为__root__idea里面代码经过build之后,会在out文件夹下面按层级建立不同的文件夹(文件夹对应包)和.class文件

//pack1.scala

package level1.level2.packagename

class A

//pack2.scala 等价于pack1.scala

package level1.level2.packagename{

class A

}

// pack3.scala 包含多个包

package p0{

class P0A

package p10{

class P10A

class P10B{

// 同一个包内,可不带前缀访问包内其他成员。不需要前缀不用p0.p10.P10A

val cb= new P10A

val c2=new P0A //也可见

}

package p20 {

class P20A{

val c1=new P10A//不在同一个包,但包外作用域可访问的名称,内层可用同样精简方式访问,不用p10.P10A

val c2=new P0A

}

}

}

}

//包内的简洁使用规则,需要名称被{}打包在同一作用域内。

package p0.p11{

class P11A{

//报错,P0A不在作用域内

val c=new P0A

}

}

//支持减少一直引用使得包定义过于往右

//跟在pack3.scala后面或者放在新建的pack4.scala里都可以

package p0

package p11

class P11A{

//注意,不报错,这是这种表达的一种作用

val c=new P0A

}

//pack5.scala 包名相互遮挡,则需指明前缀,没前缀为包内的子包。顶层包用__root__指明

package launch{

class A3

}

package p0{

package p10{

package launch{

class A1

}

class Task{

val a1=new launch.A1 //没前缀为包内的子包

val a2=new p0.launch.A2 //需指名前缀

val a3=new __root__.launch.A3 //所有包的顶层包都为__root__

}

}

package launch{

class A2

}

}

引入包,import:

可出现在任何位置,作用域由位置决定。P247除包外,还可引用对象(包括单例和常规对象)可重命名或隐藏某些被引入的成员可引入的是包本身(即可以把包名当作成员用),而不止是其中的成员

//全量引入

import Fruit._

//部分引入

import Fruit.{Apple,Orange}

//重命名:

import Fruit.{Apple=>MyLove,Orange}//要使用Apple可写成:Fruit.Apple或MyLove

import Fruit.{Apple=>MyLove,_}//引入全部成员,并将Apple重命名

//隐藏(即不引入某类)

import Fruit.{Apple=>_,_} //不引入Apple

//引入相同名称成员,使用时不区分的话,会出错

class test3 {

import p0.P0A

import p0.p11.P0A

val c=new P0A//无法编译,不知道是哪个P0A,需要加前缀指明

}

隐式引入,".scala"的源码文件都默认在顶部添加了如下三行引入: a. 这三个引入子句做了特殊处理,引入相同名称成员时,不会报错,而是后面的覆盖前面的。例如,scala.StringBuilder会覆盖java.lang.StringBuilder。P250

import java.lang._ //包含Thread等成员

import scala._ //包含List等成员

import Predef._ //包含assert等成员

包、类和对象的成员都可以标上访问修饰符“private”和“protected”。用“private”修饰的成员是私有的,只能被包含它的包、类或对象的内部代码访问。P253用“protected”修饰的成员是受保护的,除了能被包含它的包、类或对象的内部代码访问,还能被子类访问(只有类才有子类)。P253

class Diet {

private val time = "0:00"

protected val food = "Nothing"

}

class Breakfast extends Diet {

override val time = "8:00" // error

override val food = "Apple" // OK

}

(new Diet).time //error

假设X指代某个包、类或对象,那么如果在C类中使用private[X]和protected[X]就是在不加限定词的基础上(即包括C类内部也可用),把访问权限扩大到X的内部。即对于C和X外都是private或protected。

package A {

package B {

private[A] class JustA

}

class MakeA {

val a = new B.JustA // OK

}

}

package C {

class Error {

val a = new A.B.JustA // error

}

X还能是自引用关键字“this”。private[this]比private更严格,不仅只能由内部代码访问,还必须是调用方法的对象或构造方法正在构造的对象来访问;protected[this]则在private[this]的基础上扩展到定义时的子类。作用:保证成员不被该类的其他对象看到,对于文档或者型变注解有意义(当前不许要了解)。P254

class MyInt1(x: Int) {

private val mi1 = x

def add(m: MyInt1) = mi1 + m.mi1 //OK

}

class MyInt2(x: Int) {

private[this] val mi2 = x

def add(m: MyInt2) = mi2 + m.mi2 //m.mi2非法,无法访问,第一个mi2也可形成this.mi2

}

伴生对象和伴生类共享访问权限,即两者可以互访对方的所有私有成员。在伴生对象里使用“protected”没有意义,因为伴生对象没有子类。特质使用“private”和“protected”修饰成员也没有意义。包对象。P256

包里可直接包含的元素有类、特质和单例对象,但其实类内可定义的元素都能放在包级别。每个包都可以有一个包对象,任何被放在包对象中的定义都会被当作这个包本身的成员。包对象用关键字组合package object为开头来定义,其名称与关联的包名相同,有点类似伴生类与伴生对象的关系。包对象会被编译成名为“package.class”(无论你定义包对象的脚本叫什么)的文件,该文件位于与它关联的包的对应文件夹里。为了保持路径同步,建议定义包对象的文件命名为“package.scala”,并和定义关联包的文件放在同一个目录下。包对象不能定义package object p0.p10这样的多层包,所以包对象是对于package.scala所放位置决定的,放最顶层则对应的是__root__下的包而言的。放p10目录下,则对应的是p10下的包。且包对象和包应该有享有同样的成员使用权。(该结论待验证)包对象的作用:用于包级别的类型别名(第20章)和隐式转换(第21章)

//package.scala 放在p0层

package object p0{

def echo(c:P0A)={}//ok

}

package object p10{

def echo(c:P0A)={}//错误,访问不到P0A的定义,除非加import

}

package object p10{

def echo(c:P10A)={}//错误,访问不到P10A的定义,除非加import

}

//原因是p10理应为p0.p10,但包对象不允许这样的定义,如上相当于定义了__root__.p10的包对象

参考: https://blog.csdn.net/qq_34291505/article/details/86777542

第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(: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导入的教程)

org.scalatest

scalatest_2.11

3.2.0

test

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表达式。

变量定义,val Row(x,y)=scase序列

{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 =

> signal(1)

res0: Int = 0

> signal(0)

scala.MatchError: 0 (of class java.lang.Integer)

at $anonfun$1.apply$mcII$sp(: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] =

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(:11)

at $anonfun$1.applyOrElse(: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。每当模式匹配遇到引用提取器对象的模式时,他都会用选择表达式来调用提取器的unappliy方法。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)

//每当模式匹配遇到引用提取器对象的模式时,他都会用选择表达式来调用提取器的unappliy方法。上述过程会发生如下调用

EMail.unapply(x)

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),先启用Email的unapply,再启用Twice的unapply,最后启用UpperCase的unapply。注意UpperCase的无参数匹配一定要带()。 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

}

> val Domain(a, b @ _*)="www.baidu.com"

a: String = com

b: Seq[String] = WrappedArray(baidu, www)

提取器 vs 样例类P600

样例类的缺点:把数据的具体类型暴露给使用方。如case C()中的C。如果样例类被别人使用并包含了模式匹配,重命名或者改名样例类的继承关系都会影响使用方。提取器则没有这个问题。如果要暴露给别人使用,则考虑从提取器开始、如果看不出哪个好,从样例类开始,因为样例类更简单。 正则表达式。P603

raw字符串表示:"""any string"""正则表达式的类型:scala.util.matching.Regex。两种创建方法:P602

val r=new Regex("""any string""")"""val r=any string""".r 正则表达式定义了一些常用方法。findFristIn,finddAllIn findPrefixOf等,返回Option或者Iterator类型。每个正则表达式都定义了一个提取器。正则表达式的group对应提取器unapplySeq返回的Seq的每个元素。

> val R="""(-)?(\d+)(\.\d*)?""".r

> val R(s,i,d)="-1.23" //使用提取器的模式匹配

s: String = -

i: String = 1

d: String = .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: _*)

第16章 使用列表

scala的类型推断算法P336-P339:

对于实例m的方法调用m(args),首先检查m的类型是否已知。如果类型已知,那么这个类型就用于推断入参的预期类型。例如,l1.sortWith(_>_),l1为List[Char],入参类型可推导为 (Char,Char)=>Boolean。对于函数直接调用,如msort(_>_)(l1)。

方法首先会看方法的类型参数是否传递了,如果传递了(如msort[Char](_>_)(l1)),则可用Char直接推断后面的入参类型。如果没有传递方法的类型参数,则会检查入参的类型决定反复的正确实例类型。如msort(_:Char>_:Char)(l1)或msort2(l1)(_>_)。由上例子可知,scala的类型推断,对于柯里化的函数,会只考虑第一个函数参数列表里的所有入参类型,不会管后面函数的入参。所以建议将函数类型的入参放到最后,如msort2中的less放在最后。

val l1=List('a','b','c','d','e')

def msort[T](less:(T,T)=>Boolean)(xs:List[T]):List[T]={

...

}//msort[T]中的T一旦确定,后面所有入参的类型就确定了

//交换顺序

def msort2[T](xs:List[T])(less:(T,T)=>Boolean):List[T]={

...

}

所以可以理解 class C[T] 和 def func[T]处的类型参数T是用来帮助类型推断的,看情况可写可不写。

第19章 类型参数化

类型参数化能让我们编写泛型的类和特质。例如:Set[T]的具体实例可以是Set[String],Set[Int]。P385推论:不带类型参数的类和特质,可以直接做类型参数。如下面的C2和常见的String,Int等。P392

class C1[T](val l1:List[T])//trait C1[T](val l1:List[T]) 也一样

class C2(val l1:List[String])//trait C2(val l1:List[String]) 也一样

//做类型参数

val c2:C2=...

val c1:C1=.. (报错)

val c1:C1[String]=..

val c1:C1[Int]=..

带类型参数的类或特质又叫泛型。给定类型参数后,就可当做类型参数了。例如 C1[String],C1[Int]。有几个概念:

协变(convariant):如果S是T的子类型,则C1[S]也是C1[T]的子类型,则称C1在类型参数T上是协变的。通过定义时再T前面加上+实现。class C1[+T]。P393不变(nonvariant):不同类型的队列之间永远不存在子类型关系。泛型默认是不变的。 P394逆变(contravariance):如果S是T的子类型,则C1[T]是C1[S]的子类型。通过定义时再T前面加上-实现。class C1[-T]。P394+,-叫做型变注解(variance annotation)。协变,不变,逆变叫做类型参数的型变(variance)。 型变的作用在于,可以方便的使用scala的特性:子类的值可以被用在任何需要超类的值的地方(即子类可以当做超类使用,这也是里氏替换原则)。P186

对于协变来说,如果S是T的子类型,可用C1[T]的地方,就能用C1[S],例:String是Any的子类,可用C1[Any]的地方,就能用C1[String]对于逆变来说,如果S是T的子类型,可用C1[S]的地方,就能用C1[T],例:String是Any的子类,可用C1[String]的地方,就能用C1[Any] Scala中数组是不变的,但可以通过类型转换,实现需要协变时的场景。P397

val a1=Array("abc")

val a2:Array[Any]=a1 //报错

val a2:Array[Any]=a1.asInstanceOf[Array[Any]] //成功

所有需要类型参数的地方,都被认为是一个“点”,这些点可分为协变点、逆变点、不变点。P399

用+注解的类型参数只能用在协变点。用-注解的类型参数只能用在逆变点。不用注解的类型参数,可以用再任意点。(也是唯一能用在不变点的类型参数) 怎么确认点位类型?参考基本原理,如下是一些快速确认方法:

方法参数的位置和值参数的位置都是逆变点。如def func[T](a:S)中的T和S处。方法返回值的位置是协变点。如def func[T](a:S):Q={}中的Q处。FunctionN有N+1个参数,前N个都是逆变点,最后一个是协变点。 scala中函数参数的一个非常重要的结论:函数的参数必须是逆变的,而返回值必须是协变的 当我们写下函数类型A=>B,scala会展开成Function1[A,B],Function1是逆变和协变同时存在的。P403

我们在使用函数类型参数的时候,会默认带上逆变和协变在里面,有比较有趣的应用,见如下示例 P404

trait Function1[-S,+T]{

def apply(x:S):T

}

// 函数类型参数默认带上逆变和协变

def func1(f:A1=>Any)={}

// 等价于

def func1(f:Function1[-A1,+Any])={}

// 假如A1为A的子类,String是Any的子类,则 A=>String 就为 A1=>Any的子类

def f1(a:A1):Any={}

def f2(a:A):String={}

func(f1) //成立

func(f2) //也成立,里氏替换规则

下界:逆变点位置是不能使用协变参数的,要想解决这个问题,可通过下界方案。在逆变点用[U>:T]的U类型表示。这个语法表示U必须是T的超类。P401

举例,假定有一个Fruit类和两个子类Apple、Orange,且func的功能是新增元素。则按照C1的新定义,可以对C1[Apple]追加一个Orange,返回一个C1[Fruit]。P401

class C1[+T]{

def func(x:T):C1[T]={..} //报错,x处为逆变点,不能用协变参数

}

//修改

class C1[+T]{

def func[U>:T](x:U):C1[U]={..} //不报错

}

上界。[T<:U]`表示T有个上界U,即U必须是T的超类。理解为下界的一种相反的写法即可。P409 private[this]和protect[this]可以使scala跳过型变检查,而且这种跳过检查不会造成问题,因为型变只会在运行或者编译时对象类型改变发生,如果对象的字段是private或者protect的,这种情况就不会发生。具体原因可参考:参考1,参考2。P407 多重界定

/*

// 表示:A和B为T上界,and的关系

T <: A with B

// 表示:A和B为T上界,or的关系

T <: Eihter[A,B]

// 表示:A和B为T下界

T >: A with B

// 表示:同时拥有上界和下界,并且A为下界,B为上界,A为B的子类,顺序不能颠倒。

T >: A <: B

// 表示:类型变量界定,即同时满足AT这种隐式值和BT这种隐式值

T:A:B

// 表示:视图界定,即同时能够满足隐式转换的A和隐式转换的B

T <% A <% B

*/

第20章 抽象成员

抽象trait可以不加abstract 限定字。

抽象的val不能再重新定义成def 或var抽象的def可重新定义成val

trait Abstract{

type T

def transform(x:T):T

val initial:T

var current:T

}

class Concrete extends Abstract {

type T = String //相当于给String一个别名

def transform(x: T)= x +x

val initial = "hi"

var current = initial

}

abstract class B extends Abstract{

val transform:T //可以,抽象的def可重新定义成val

def initial:T //报错,抽象的val不能再重新定义成def 或var-=

}

抽象类/特质第具体化的新语法:使用匿名类。 解释a的创建过程:

Abs的初始化代码被执行,抽象字段都初始化为对应类型的默认值。

容易得到错误结果Abs(exp1,exp2)这样第初始化会先计算exp1和exp2的结果,new Abs{val v=exp1,m=exp2}初始化匿名类过程不会先计算exp1和exp2。

由new表达式定义的匿名子类的主构造方法给执行。包括用1初始化v和用4初始化m。

解释器调用被构造对象第toString方法,以便打印出结果

toString方法从头到尾依次访问对象字段。(如果有lazy字段,则lazy字段在被访问时开始被求值)

abstract class Abs { //或trait Abs

val v:Int

val m:Int

}

val a = new Abs{ //

val v=1

val m=4

}//这里中间过程产生了一个匿名类

println(a.v) //输出1

val x=2

val b = new Abs{ //

val v=1*x //初始化匿名类过程不会先计算表达式

val m=4*x//初始化匿名类过程不会先计算表达式

}

println(b.v) //输出0

new Abs{val v=exp1,m=exp2}解决这类产生默认值第问题:P417

//预初始化字段

val c = new { //

val v=1*x //初始化匿名类过程 先计算表达式

val m=4*x//初始化匿名类过程 先计算表达式

} with Abs

// lazy的val

abstract class Abs2 { //或trait Abs2

lazy val v:Int

laze val m:Int

val t=v+m

}

抽象类型可定义上下界 P424

class Food

abstract class Animal{

type SFood <:Food

def eat(food:SFood)

}

//等价

abstract class Animal{

def eat[SFood <:Food](food:SFood)

}

枚举。P429

Enumeration类中有Value函数

object Color extends Enumeration{

val Red = Value //Value函数创建 new Val(nextId,nextNameOrNull) 对象,Val 对象 继承自和Value函数同名的Value类

val Green = Value

val Blue = Value("Blue")//带参数第方法可以给枚举值关联特定名字

// 或直接一行 val Red, Green, Blue= Value

}

for(a<- Color.values) print(a +" ") //Red Green Blue

println(Color.Blue.id)//2

println(Color(2))//Blue

第21章 隐式转换和隐式参数

会发生隐式换的三种情况:P448

转换到一个预期类型:当编译器看到一个X,单需要一个Y时。就会查找一个能将X转化成Y的隐式转换。对某个(成员)选择接收端(即字段、方法调用等)。obj.doIt,如果obj没有doId这个成员,那么编译器再放弃前,会尝试将obj转换成有doIt成员的类型。这里obj就是接收端。

隐式类:以implicit打头的class,编译器会自动生成一个从类的构造方法参数到类本身的隐式转换。

隐式类必须存在于另一个对象、特征或类里面隐式类不能是样例类,并且其构造方法必须有且仅有一个参数。隐式类一般作为富包装类给某个已有的类添加方法。 隐式参数:someCall(a)替换成someCall(a)(b) 或 new A(a)替换成 new A(a)(b)。P454

提供整个最后一组柯里化的参数列表传入的参数b需要在定义时标记位implicit。且需要在当前作用域内定义或者import。定义需要在调用之前。

隐式object等价于隐式变量或隐式方法。 更多参考:https://stackoverflow.com/questions/22592456/what-are-implicit-objects 调用的方法someCall和A的最后一个参数列表页需要标记位implicit

//预期类型

implicit def doubleToInt(x:Double) = x.toInt

val i:Int=3.5 //可行,调用doubleToInt(3.5)

// 转换接收端

implicit def intToRtational(x:Int)= new Rational(x,1)

1+ new Rational(1,2)//就不会报错了,因为1先转换成Rational(1,1)

//隐式类

case class Rec(w:Int, h:Int)

object App2 extends App {

implicit class RecMaker(w:Int){

def x(h:Int)=Rec(w,h)

}

//编译器自动生成如下转换

//implicit def RecMacker(w:Int)=new RecMaker(w)

val a= 3 x 4 //就不会报错了,3会转换成new RecMaker(3)

println(a)//输出Rec(3,4)

}

//隐式参数

case class B(prefer:String)

case class C(prefer:String)

object A{

def greet(name:String)(implicit job:B,drink:C)={//注意:implicit是针对整个参数列表

println("欢迎"+name+", 系统已ok")

println(job.prefer)

}

}

object App2 extends App {

val b=B("bbb")

val c=C("ccc")

A.greet("张三")(b,c) //显示调用ok

A.greet("张三")//报错

implicit val b1=B("b111") //隐式定义

implicit val c1=C("c111") //隐式定义

A.greet("张三")//不报错

}

//关于隐式object

object NumberLike {

implicit object NumberLikeDouble extends NumberLike[Double]{...}

//等价1

//implicit val NumberLikeDouble: NumberLike[Double] = new NumberLike[Double] { ...}

//等价2

//implicit def NumberLikeDouble: NumberLike[Double] = new NumberLike[Double] { ...}

}

//隐式object的使用

import NumberLike

def sum[A](x: A, y: A)(implicit nl: NumberLike[A]) = nl.plus(x, y)

sum(4.0, 5.0) // 找到了NumberLikeDouble

隐式转换的规则约束

标记规则:只有标记位implicit的定义才可用。implicit可以标记任何变量、函数或对象定义。P445作用域规则:被插入的隐式转换必须是当前作用域的单个标识符。只能convert(x),不能a.convert(x)

单个标识符的例外:跟隐式转换的源类型或者目标类型有关联。把A对象传递给接收B的对象,源类型就是A,目标类型就是B。使用见下面的例子。P446 每次一个规则:只会convert(x)不会convert1(convert2(x)) P447显示优先规则:只要代码能按编写的样子通过检查,就不会尝试隐式定义。P447

//单标识符例外

object A{

implicit def aToB(x:A):B=...

}

class A{...}

val b:B=new A //这里会自动调用A.aToB,不需要再程序中单独引入这个转化

关于隐式参数中使用的Odering[T],哪里引入的类似implicit val odr=new Ordering[T]这样的变量的呢?

scala默认引入三个包:java.lang._,scala._,scala.Predef._ ,implicit val odr=new Ordering[T]在scala._中引入,scala.math.Ordering的implicit object中,并且利用了单个标识符的例外规则。只要理解,假定有implicit val odr=new Ordering[T]这样的变量引入了即可。基本类型的T都满足。

object App2 extends App {

def maxList[T](l:List[T])(implicit odering:Ordering[T]):T= l match {

case List()=> throw new IllegalArgumentException("空列表")

case List(x) => x

case x::rest=>

val maxRest=maxList(rest)//这里会隐式添加ordering

if(odering.gteq(x,maxRest)) x else maxRest //这里依然显示给出odering,可通过上下文界定省略

}

val l=List(3,8,5)

println(maxList(l))//实际调用的是scala.this.Predef.println(App2.this.maxList[Int](App2.this.l)(math.this.Ordering.Int))

}

上下文界定。[T:Ordering]这样的语法,是一个上下文界定的语法,相当于可省略了(implicit anyName:Ordering[T]) P460

[T:Ordering]引入一个参数类型T[T:Ordering]自动添加了一个类型为Ordering[T]的隐式参数对比上界,下界 [T<:Ordered[T]], [T>:Ordered[T]]表示的继承关系, [T:Ordering]仅表示T带有某种形式的Ordering

//标准库中定义如下方法

def implicityly[T](implicit t:T)=t

//使用上下文界定

object App2 extends App {

def maxList[T](l:List[T])(implicit odering:Ordering[T]):T= l match {//可以看到ordering并不是必须的,可修改为等价的上下文界定形式

case List()=> throw new IllegalArgumentException("空列表")

case List(x) => x

case x::rest=>

val maxRest=maxList(rest)//这里会隐式添加ordering

if(implicityly[Ordering[T]].gteq(x,maxRest)) x else maxRest //标准库中的方法

}

}

//等价上下文界定

object App2 extends App {

def maxList[T: Ordering](l:List[T]):T= l match {//等价的上下文界定形式

case List()=> throw new IllegalArgumentException("空列表")

case List(x) => x

case x::rest=>

val maxRest=maxList(rest)//这里会隐式添加隐式参数 anyName

if(implicityly[Ordering[T]].gteq(x,maxRest)) x else maxRest //标准库中的方法

}

}

多个转换可选时。P464

大部分场合会报错如果某个转换比其他的更具体,则用这个更具体的。以下任意一条满足,则表示a更具体。没有更具体的时,就报错。

a的入参是b入参的子类型 a(x:String),b(x:Any)a、b都是方法,且a所在的类扩展自b所在的类,A extends B,A.a和B.b 隐式类的调试方法。P466

显示写出scalac -Xprint:typer xxx.scala方法打印添加了隐式值的代码示例

参考文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。