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

第16章 使用列表

  1. 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]={
	...
}
  1. 所以可以理解 class C[T]def func[T]处的类型参数T是用来帮助类型推断的,看情况可写可不写。

第19章 类型参数化

  1. 类型参数化能让我们编写泛型的类和特质。例如:Set[T]的具体实例可以是Set[String],Set[Int]。P385
  2. 推论:不带类型参数的类和特质,可以直接做类型参数。如下面的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]=..
  1. 带类型参数的类或特质又叫泛型。给定类型参数后,就可当做类型参数了。例如 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)。
  2. 型变的作用在于,可以方便的使用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]
  3. Scala中数组是不变的,但可以通过类型转换,实现需要协变时的场景。P397
val a1=Array("abc")
val a2:Array[Any]=a1 //报错
val a2:Array[Any]=a1.asInstanceOf[Array[Any]] //成功

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

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

    • 方法参数的位置和值参数的位置都是逆变点。如def func[T](a:S)中的T和S处。
    • 方法返回值的位置是协变点。如def func[T](a:S):Q={}中的Q处。
    • FunctionN有N+1个参数,前N个都是逆变点,最后一个是协变点。
  3. scala中函数参数的一个非常重要的结论:函数的参数必须是逆变的,而返回值必须是协变的

  4. 当我们写下函数类型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) //也成立,里氏替换规则

  1. 下界:逆变点位置是不能使用协变参数的,要想解决这个问题,可通过下界方案。在逆变点用[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]={..} //不报错
}
  1. 上界。[T<:U]`表示T有个上界U,即U必须是T的超类。理解为下界的一种相反的写法即可。P409
  2. private[this]protect[this]可以使scala跳过型变检查,而且这种跳过检查不会造成问题,因为型变只会在运行或者编译时对象类型改变发生,如果对象的字段是private或者protect的,这种情况就不会发生。具体原因可参考:参考1参考2。P407

第20章 抽象成员

  1. 抽象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-=
}

  1. 抽象类/特质第具体化的新语法:使用匿名类。 解释a的创建过程:
      1. Abs的初始化代码被执行,抽象字段都初始化为对应类型的默认值。
      • 容易得到错误结果
      • Abs(exp1,exp2)这样第初始化会先计算exp1和exp2的结果,new Abs{val v=exp1,m=exp2}初始化匿名类过程不会先计算exp1和exp2。
      1. 由new表达式定义的匿名子类的主构造方法给执行。包括用1初始化v和用4初始化m。
      1. 解释器调用被构造对象第toString方法,以便打印出结果
      1. 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
  1. 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
}
  1. 抽象类型可定义上下界 P424
class Food
abstract class Animal{
	type SFood <:Food
	def eat(food:SFood)
}
//等价
abstract class Animal{
	def eat[SFood <:Food](food:SFood)
}
  1. 枚举。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章 隐式转换和隐式参数

  1. 会发生隐式换的三种情况: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
  1. 隐式转换的规则约束
    • 标记规则:只有标记位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,不需要再程序中单独引入这个转化

  1. 关于隐式参数中使用的Odering[T],哪里引入的类似implicit val odr=new Ordering[T]这样的变量的呢?
    • scala默认引入三个包:java.lang._,scala._,scala.Predef._ implicit val odr=new Ordering[T]scala._中引入,scala.math.Orderingimplicit 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))
}
  1. 上下文界定。[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 //标准库中的方法
  }
}
  1. 多个转换可选时。P464

    • 大部分场合会报错
    • 如果某个转换比其他的更具体,则用这个更具体的。以下任意一条满足,则表示a更具体。没有更具体的时,就报错。
      • a的入参是b入参的子类型 a(x:String),b(x:Any)
      • a、b都是方法,且a所在的类扩展自b所在的类,A extends BA.aB.b
  2. 隐式类的调试方法。P466

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值