谜题1:
List(1, 2).map { i => i + 1 }
List(1, 2).map { i => println("Hi"); i + 1 }
i => println("Hi");这个是一个函数字面量表达式,每次调用i都会调用后面的println
List(1, 2).map { _ + 1 }
List(1, 2).map { println("Hi"); _ + 1 }
println("Hi"); _ + 1 是一个代码块,代码块作为参数传入时,只是在map传入参数评估时进行调用,一个代码块最终返回的结果决定于最后一个语句
执行结果:
List[Int] = List(2, 3)
Hi
Hi
List[Int] = List(2, 3)
List[Int] = List(2, 3)
Hi
List[Int] = List(2, 3)
解释:
使用占位符语法定义的匿名函数的范围只延伸到含有下划线(_)的表达式;常规的匿名函数的函数体是包含从箭头标识符(=>)一直到代码块结束的所有代码
谜题2:
var IJ: (Int, Int) = (3, 4)
var (I, J): (Int, Int) = (3, 4)
在scala中,多变量赋值实际是基于模式匹配的,大写字符开头的变量有特别的含义:它们是静态标识符,小写变量定义的是变量的模式,这个模式会给变量赋值
var 用小写,常量用大写《scala语言规范》
Things can be made extremely simple to understand:
- multiple variable assignments (inside parenthesis) are based on pattern match
- pattern match considers uppercase as 'constant' (or 'stable identifier')
- pattern match either succeeds or fails at runtime (if you were able to compile), no variables are actually assigned (unless it is regular lowercase)
执行结果:
IJ: (Int, Int) = (3, 4)
and the second fails to compile
谜题3:
trait A {
val audience: String
println("Hello " + audience)
}
class BMember(a: String = "World") extends A {
val audience = a
println("I repeat: Hello " + audience)
}
class BConstructor(val audience: String = "World") extends A {
println("I repeat: Hello " + audience)
}
new BMember("Readers")
new BConstructor("Readers")
执行结果:
Hello null
I repeat: Hello Readers
Hello Readers
I repeat: Hello Readers
解释:
在实例化超类或特征时,可以将父构造函数可视化为在子构造函数的第一行之前但在类定义之后执行
在继承trait时,A的调用构造函数在继承者之前,第一步 Readers 分配给构造器参数。当调用的构造
器参数时, audience还没有初始化,所以就打出缺省的字符串值null
可以将第一种方式改造成以下这样(通过使用一个早期字段定义从句不用在构造器参数列表中声明)
class BEarlyDef(a: String = ”World” ) extends {
val audience = a
} with A {
printl (repeat: Hello ” + audience)
首先 按照定义的顺序早定义;
然后 超类构造器;
最后 缺省构造器中语句。
一句话:超类和超特质初始化代码的执行是在参数评估和早期字段定义之
后,实例化类和特质的初始化语句之前。直接超类和混进特质是当它们出现
class trait object 定义中时按左到右的顺序初始化
trait A {
val audience : String
println (”Hello ” + audience )
trait AfterA {
val introduction : String
println(introduction)
class BEvery (val audience : String ) extends {
val introduction =
{ println (” Evaluating ea rl y de f ” ) ;”Are you there ?” }
} with A with AfterA {
println (” I repeat : ell audi e n ce )
scala> new BEvery ( { println ral uati ng pa ram” ) ;”Readers” } )
Evaluating param
Evaluating early def
Hello Readers
Are you there?
I repeat: He l lo Readers
谜题4:
trait A {
val foo: Int
val bar = 10
println("In A: foo: " + foo + ", bar: " + bar)
}
class B extends A {
val foo: Int = 25
println("In B: foo: " + foo + ", bar: " + bar)
}
class C extends B {
override val bar = 99
println("In C: foo: " + foo + ", bar: " + bar)
}
new C
执行结果:
In A: foo: 0, bar: 0
In B: foo: 25, bar: 0
In C: foo: 25, bar: 99
你应该记得每个 Scala 类都有一个非显式定义但与类定义交互 的主构造器 。类定义中的所有语句形成了主构造器的程序体,包
括字段定义(顺便说一下 ,这就是为什么 Scala 构造器类宇段和值之间井没有本质不同的原因)。因此,在特质 、类 和类 中的所有代码都属于构造器程序体。
以下规则控制 va 的初始化和重载行为
l. 超类会在子类之前初始化
2. 按照声明的顺序对成员初始化:
3. 当一个 va 被重载时,只能初始化一次:
4. 与抽象 va 类似,重载的 va 在超类构造期间会有一个缺省的初始值。
Scala Java 继承了初始化顺序规则。 Java 确保首先初始化超类,这样就
可以从子类构造器中安全使用超类字段,确保正确地初始化字段。特质会被编译
成接口和 体的(非抽象的) 所以也可应用同样的规则
你可能问, 使用个还没有初始化成非缺省值 的抽象字段时,编译器是
否能用某种方式发出警告呢。可惜 缺省情况下编译器不能报出这种有关未初始
化值的警告(只有测试才能捕获这样的信息〉。有个高级编辑选项可以用来检责未初始化
解决方法:
a、将val bar 改为def bar:int =10,改成方法体后,该方法就不属于主构造器,因此不参与类初始化
b、在变量名前加上lazy,尽可以延迟参数的初始化
最佳:采用预初始化字段
class c extends {
override val bar = 99
} with B {
println ("In C:foo" + foo +", bar: " + bar )
}
这段程序与原来程序的唯一差别, 就是bar在类的早期字段定义从句中初
始化。早 字段定义从句紧跟着 extends 关键字后的大括号,它是子类的
部分构造器之前运行 。这样就可确保bar在特质被构造之前即被
初始化
谜题5:
def sumSizes(collections: Iterable[TraversableOnce[_]]): Int = collections.map(_.size).sum
sumSizes(List(Set(1, 2), List(3, 4)))
sumSizes(Set(List(1, 2), Set(3, 4)))
答案:
Int = 4
Int = 2
解释:尽管 collections.map 似乎将一个可迭代对象映射到另一个“不错的”可迭代对象,但由于集合重新设计,返回的可迭代对象的类型(通常)将匹配输入类型。对于集合,这意味着......没有重复。是的, foldLeft 显然是一种更好的方法
操作符一般会保持输入的集合类型不变!
Java背景的开发者可能认为转换Iterable会产生一个遵从Iterable接口的结果,可能是任何实现类型。 Scala集合则更进一步,返回一个与输入类型一
样的Iterable。这意味着下面语旬的结果也是 set 类型,因此不能有重复
的实例。
Set(List(l, 2), Set(3,4).map(_.size)
解决方法:
1)如何才能避免这种意外结果呢?在本例中一个可能的选择是将外集合转换成
种己知的类型,例如使用toSeq
collections.map(_.size).sum 改为 collections.toSeq.map(_.size).sum
2)使用fold操作
collections.map(_.size).sum 改为 collections.foldLeft(0) { (sumOfsizes,collection) => sumOfsizes + collection.size}
要密切关注对集合进行操作的方法其输入类型是什么。如果你不是必需保留输入类型,也可以考虑用已知的特性构建自己的中间类型
谜题6:
def square[T : Numeric](n: T) = implicitly[Numeric[T]].times(n, n)
def twiceA[T](f: T => T, a: T) = f(f(a))
def twiceB[T](f: T => T)(a: T) = f(f(a))
def twiceC[T](a: T, f: T => T) = f(f(a))
def twiceD[T](a: T)(f: T => T) = f(f(a))
twiceA(square, 2)
twiceB(square)(2)
twiceC(2, square)
twiceD(2)(square)
执行结果:
第一个、第二个和第三个语句编译失败,第四个打印:
16
解释:
要使square可用作参数,编译器必须知道T绑定到(可以隐式转换为的类型)Numeric。尽管看起来 2 作为 Int 显然满足了这个条件,但编译器在多参数列表中无法获得该信息。只有在第四个版本中,T绑定“足够早”以允许平方计算
隐式转换函数有两种作用场景。
1 转换为期望类型:就是指一旦编译器看到X,但需要Y,就会检查从X到Y的隐式转换函数。
2 转换方法的调用者:简单来说,如obj.f(),如果obj对象没有f方法,则尝试将obj转换为拥有f方法的类型。
编译器只关心隐式转换函数的输入输出类型,不关心函数名,为避免歧义,同一个作用域中不能有输入输出类型相同的两个隐式转换函数
Scala2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。使用情况与隐式转换函数类似,可以看做将类的构造函数定义为隐式转换函数,返回类型就是这个类
1)当函数没有柯里化时,implicit关键字会作用于函数列表中的的所有参数。
2)隐式参数使用时要么全部不指定,要么全不指定,不能只指定部分。
3)同类型的隐式值只能在作用域内出现一次,即不能在同一个作用域中定义多个相同类型的隐式值。
4)在指定隐式参数时,implicit 关键字只能出现在参数开头。
5)如果想要实现参数的部分隐式参数,只能使用函数的柯里化,
如要实现这种形式的函数,def test(x:Int, implicit y: Double)的形式,必须使用柯里化实现:def test(x: Int)(implicit y: Double).
6)柯里化的函数, implicit 关键字只能作用于最后一个参数。否则,不合法。
7)implicit 关键字在隐式参数中只能出现一次,柯里化的函数也不例外!
上下文界定(context bounds)
def min[T: Ordering](x: T, y: T): T = {
val ord = implicitly[Ordering[T]]
if (ord.compare(x, y) >= 0) y else x
}
[T: Ordering]这种语法就叫上下文界定,含义是上下文中必须有一个Ordering[T]类型的隐式值,这个值会被传入min函数。但是由于这个隐式值并没有明确赋值给某个变量,没法直接使用它,所以需要一个implicitly函数把隐式值取出来
implicitly函数的定义非常简单,作用就是将T类型的隐含值返回:
@inline def implicitly[T](implicit e: T) = e
implicitly是在Predef.scala里定义的,它是一个特殊的方法,编译器会记录当前上下文里的隐式值,而这个方法则可以获得某种类型的隐式值
* 1,scala的类和方法、函数都可以是泛型。
* 2,关于对类型边界的限定分为上边界和下边界(对类进行限制)
* 上边界:表达了泛型的类型必须是"某种类型"或某种类型的"子类",语法为“<:”,
* 下边界:表达了泛型的类型必须是"某种类型"或某种类型的"父类",语法为“>:”,
* 3, "<%" :view bounds可以进行某种神秘的转换,把你的类型在没有知觉的情况下转换成目标类型,
* 其实你可以认为view bounds是上下边界的加强和补充,语法为:"<%",要用到implicit进行隐式转换(见下面例子)
* 4,"T:classTag":相当于动态类型,你使用时传入什么类型就是什么类型,(spark的程序的编译和运行是区分了Driver和Executor的,只有在运行的时候才知道完整的类型信息)
* 语法为:"[T:ClassTag]"下面有列子
* 5,逆变和协变:-T和+T(下面有具体例子)+T可以传入其子类和本身(与继承关系一至)-T可以传入其父类和本身(与继承的关系相反),
* 6,"T:Ordering" :表示将T变成Ordering[T],可以直接用其方法进行比大小,可完成排序等工作
一个类型参数的信息只对克里化调用中 后的参数列表是可用的,对同一列表中的其他参数是不可用的。当定义一个有参数的方法时,参数的类型约束只有相同方法的早参数绑定的类型是已知时才
能满足,这时要用克里化的方法来定义而不是单个参数列表的方法
谜题7:
import collection.mutable.Buffer
val funcs1 = Buffer[() => Int]()
val funcs2 = Buffer[() => Int]()
{
val values = Seq(100, 110, 120)
var j = 0
for (i <- 0 until values.length) {
funcs1 += (() => values(i))
funcs2 += (() => values(j))
j += 1
}
}
funcs1 foreach { f1 => println(f1()) }
funcs2 foreach { f2 => println(f2()) }
执行结果:
The first statement prints:
100
110
120
and the second throws an IndexOutOfBoundsException at runtime
当使用var而不是val时,函数关闭变量而不是值。因为i是在 for-comprehension 中定义的,所以它被定义为val。这意味着每次i存储在某个地方时,它的值都会被复制 - 因此它会打印预期的结果:
100 110 120
当j在循环内部发生变化时,所有三个闭包“看到”同一个变量j而不是它的副本。因为在循环j为3之后,会引发IndexOutOfBoundsException。可以通过“修复”要返回的值来避免异常:
import collection.mutable.Buffer
val funcs1 = Buffer[() => Int]()
val funcs2 = Buffer[() => Int]()
{
val values = Seq(100, 110, 120)
var j = 0
for (i <- 0 until values.length) {
funcs1 += (() => values(i))
val value = values(j)
funcs2 += (() => value)
j += 1
}
}
funcs1 foreach { f1 => println(f1()) }
funcs2 foreach { f2 => println(f2()) }
value的赋值是立即执行的,因此“捕获”了values的预期元素。
解释:
1、Scala 允许函数体引用没有显式函数参数但函数被构造时又在范围内的变量。
当调用这些函数时,为了在不同的范围内访问这些自由变量 Scala 将它封闭成
个闭包。
2、封闭一个自由变量并非是使用时取这个变量值的一个快照 而是将引用捕捉
变量 个字段加到函数对象中。对这个例子克里化, 捕获的 val 简单地由值表
示,而捕获 var 的结果是引用了 var 自己。
3、阻止这种问题发生的最鲁棒的方式是避免 var ,这也是较好的 Scala 样式
如果不能避免使用 var ,但仍然希望用闭包在创建时捕获它的值,可以将它的值
分配给 个临时 val 来“冻结” var
4、要避免在闭包里捕获引用任何可变var或可变对象的自由变量 如果你需要封闭任何可变的值,可以抽取
一个不变值赋给 val ,然后在函数中使用这个 val
谜题8:
val xs = Seq(Seq("a", "b", "c"), Seq("d", "e", "f"), Seq("g", "h"), Seq("i", "j", "k"))
val ys = for (Seq(x, y, z) <- xs) yield x + y + z
val zs = xs map { case Seq(x, y, z) => x + y + z }
执行结果:
ys evaluates to
Seq(abc, def, ijk)zs throws a MatchError
解释:
大多数人认为 for-yield-expression 直接转换为等效的map调用,但这仅在不使用模式匹配时才是正确的!如果使用模式匹配,也会应用过滤器。在上面的示例中,生成了以下代码:
xs withFilter { case Seq ( x , y , z ) => true ; case _ => false } map { case Seq ( x , y , z ) => x + y + z }
谜题9:
// start a 2.13.2 or newer REPL using "scala -Yrepl-class-based:false"
// see https://github.com/scala/scala/pull/8748
object XY {
object X {
val value: Int = Y.value + 1
}
object Y {
val value: Int = X.value + 1
}
}
println(if (math.random > 0.5) XY.X.value else XY.Y.value)
执行结果:
Prints:
2
解释:
两个字段X.value和Y.value的递归定义是有效的并且可以编译,但由于它是递归的,因此必须至少为X.value指定类型( Int ) 。
当对象X或Y被访问时,其字段值被初始化(对象在被访问之前不会被初始化)。如果先初始化对象X,则其字段值的初始化会触发对象Y的初始化。为了初始化字段Y.value,访问字段X.value。VM 注意到对象X的初始化已经在运行,并返回X.value的当前值,该值为零( Int字段的默认值),因此在运行时没有堆栈溢出。因此Y.value设置为0 + 1 = 1和X.value为1 + 1 = 2。但是,如果首先初始化对象Y,则Y.value用2初始化,X.value用1初始化。
在if语句中,我们访问X.value或Y.value并强制初始化X或Y。但是正如我们在上面看到的,首先访问哪个对象,其字段值被初始化为2,因此条件语句的结果始终为 2。
谜题10:
val xs = Seq(Seq("a", "b", "c"), Seq("d", "e", "f"), Seq("g", "h"), Seq("i", "j", "k"))
val ys = for (Seq(x, y, z) <- xs) yield x + y + z
val zs = xs map { case Seq(x, y, z) => x + y + z }
执行结果:
ys evaluates to
Seq(abc, def, ijk)zs throws a MatchError
解释:
1、大多数人认为 for-yield-expression 直接转换为等效的map调用,但这仅在不使用模式匹配时才是正确的!如果使用模式匹配,也会应用过滤器。在上面的示例中,生成了以下代码:
xs withFilter { case Seq ( x , y , z ) => true ; case _ => false } map { case Seq ( x , y , z ) => x + y + z }
注意这个静音过滤器!它可能会导致讨厌的错误。
2、在Scala 中,i <-0 to 的语法叫生成器。使用对一个简单变量赋值的生
成器,很容易让人忘记生成器左边不是一个简单变量 而是一个模式 如代码例
子中 Seq (x, y, z) <-xs 所示。 Scala 编译器用各种非凡模式(如对多个简
单变量赋值的模式)对生成器提糖。
但语言规范也规定非凡
模式是 withFilter 调用的附加 。因此下面的表达式
for (pattern <- expr) yield fun
实际上就变成:
expr withFilter {
case pattern => true
case => false
) map { case patterη => fun ) :
withFilter 调用透明地 “剔除”了不匹配的值,而在我们的提糖过程中
正是这些不匹配的值引起了 MatchError 异常。
谜题11:
trait SpyOnEquals {
override def equals(x: Any) = { println("DEBUG: In equals"); super.equals(x) }
}
case class CC()
case class CCSpy() extends SpyOnEquals
val cc1 = new CC() with SpyOnEquals
val cc2 = new CC() with SpyOnEquals
val ccspy1 = CCSpy()
val ccspy2 = CCSpy()
println(cc1 == cc2)
println(cc1.## == cc2.##) // null-safe hashCode()
println(ccspy1 == ccspy2)
println(ccspy1.## == ccspy2.##)
执行结果:
Prints:
DEBUG: In equals
true
true
DEBUG: In equals
false
true
解释:
1、对于 case 类,会生成equals、hashCode和 toString方法,对于具有相同元素的 case 类的两个实例,我们可以预期equals和 hashCode返回相同的结果(即答案 1 和 4)。
然而, 根据 SLS(第 5.3.2 节),案例类仅在案例类本身不提供这些方法之一的定义并且仅当具体案例类的某些基类中未给出定义(AnyRef除外)。
在我们的示例中,CCSpy 的基本特征SpyOnEquals提供了一个equals 方法,因此案例类不提供自己的定义,并且两个不同实例的比较返回false。
但是对于方法hashCode没有提供实现,无论是在CCSpy类中还是在某些基类中,所以这里使用了隐式覆盖的版本,它为相等的元素返回相同的值。
对于案例类CC,没有提供equals或hashCode的定义,因此这两种方法都使用了隐式覆盖的版本。在创建案例类的实例时混合SpyOnEquals不会影响这一点。
如果 case class 里没有定义这个方法,或者在不同于 AnyRef case class
某些基本类中也没有这个方法的具体定义,那么每个 case class 都会隐式地重载
ca la.AnyRef 类的方法定义。
所以 ,只有 case class 中没有方法的显式实现并且也没有从父类、父特质中
继承方法的实现时,编译器才会生成重载。此外,这个重载方法生成的条件(在
此例中是 equals hashCode )是彼此相互独立的,所以 equals hashCode
之间 致性留给开发者来处理。
在这个例子中 ,编译器为 ou ntryWithTrace equals 方法生成
载实现,所以比较由 newSwitzDecl 所创建的两个实例, newSwitzDecl ==
newS witzDecl ,评估结果为 true
然而, hashCode 方法并没有被重载,所 以在 TraceHashCode 里调用
super.hashCode 就调用了 AnyRef 的缺省实现,这与参考等式 致。因此,
newSwitzDecl. hashCode ==newSwi tzDecl. has hCod 返回 false ,所以
countries Deel 集合中并没有发现由 newSwitzDecl 创建的新实例。
至于 new Country (”CH " ) with TraceHashCod 这个例子,当声明 cas
class Country 的时候,生成的重载就被编译器加进来,此时 equals ha sh
Code 都还没有显式地实现。newSwitzinst 创建新实例期间混入了 TraceHas hCode 特质,此时 Country 己经有了 个基于结构等式的 equals 方法 从而
TraceHashCode 中的 super hashCode 就会调用 Country 中的编译器生成
hashCode 方法,这正符合我们的本意
2、在scala中有的时候用extends有的时候用with,到底该用什么呢?
如果是待声明的类没有扩展其他类只是或入了一些trait那么必须用extends关键字,并把它用在第一个trait之前而其它的trait之前用with关键字,就像Myclass3, 如果在实例化某一个类型的时候想要混入某一个特质,这个时候要用with关键字,如new Mycalss2这一个声明一样。
a、无论是继承abstract class或者混入trait,对于一个要实现的类来说,必须先用extends,剩下的用with,否则编译的时候就会出错
当同时实现abstract class和trait的时候,abstract class必须在前,而trait必须在后,如果仅实现其一的话,只可以用extends。
b、当直接new一个实现了trait的对象的时候的时候是可以使用with关键字的。
c、T with U可以是一个新的type,但是T extends U并不是一个新的类型。