第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放在最后。
- 方法首先会看方法的类型参数是否传递了,如果传递了(如
- 对于实例m的方法调用m(args),首先检查m的类型是否已知。如果类型已知,那么这个类型就用于推断入参的预期类型。例如,
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)。
- 协变(convariant):如果S是T的子类型,则C1[S]也是C1[T]的子类型,则称C1在类型参数T上是协变的。通过定义时再T前面加上+实现。
- 型变的作用在于,可以方便的使用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个都是逆变点,最后一个是协变点。
- 方法参数的位置和值参数的位置都是逆变点。如
-
当我们写下函数类型
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
第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,编译器会自动生成一个从类的构造方法参数到类本身的隐式转换。
- 隐式类必须存在于另一个对象、特征或类里面
- 隐式类不能是样例类,并且其构造方法必须有且仅有一个参数。
- 隐式类一般作为富包装类给某个已有的类添加方法。
- 隐式类:以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都满足。
- scala默认引入三个包:
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
- a的入参是b入参的子类型
-
隐式类的调试方法。P466
- 显示写出
scalac -Xprint:typer xxx.scala
方法打印添加了隐式值的代码示例