from Scala向导中文版
第1章基本类型和操作
本章里,你会获得Scala基本类型的概观,包括String和值类型Int,Long,Short,Byte,Float,Double,Char还有Boolean。你会学到可以在这些类型上执行的操作,包括Scala表达式里的操作符优先级是如何工作的。你还会学到隐式转换是如何“丰富”这些基本类型的变体,并带给你那些由Java提供支持之外的附加操作。
1.1 一些基本类型
表格5.1显示了Scala的许多基本的类型和其实例值域范围。总体来说,类型Byte,Short,Int,Long和Char被称为整数类型:integral type。整数类型加上Float和Double被称为数类型:numeric type。
表格 5.1 一些基本类型
值类型 | 范围 |
Byte | 8位有符号补码整数(-27~27-1) |
Short | 16位有符号补码整数(-215~215-1) |
Int | 32位有符号补码整数(-231~231-1) |
Long | 64位有符号补码整数(-263~263-1) |
Char | 16位无符号Unicode字符(0~216-1) |
String | 字符序列 |
Float | 32位IEEE754单精度浮点数 |
Double | 64位IEEE754单精度浮点数 |
Boolean | true或false |
除了String归于java.lang包之外,其余所有的基本类型都是包scala的成员。[1]如,Int的全名是scala.Int。然而,由于包scala和java.lang的所有成员都被每个Scala源文件自动引用,你可以在任何地方只用简化名(就是说,像Boolean,或Char,或String这样的名字)。
注意
目前实际上你可以使用与Java的原始类型相一致的Scala值类型的小写化名。比如,Scala程序里你可以用int替代Int。但请记住它们都是一回事:scala.Int。Scala社区实践提出的推荐风格是一直使用大写形式,这也是我们在这本书里做的。为了纪念这个社区推动的选择,将来Scala的版本可能不再支持乃至移除小写变体,因此跟随社区的大流,在你的Scala代码中使用Int而非int才是明智之举。
敏锐的Java开发者会注意到Scala的基本类型与Java的对应类型范围完全一样。这让Scala编译器能直接把Scala的值类型:value type实例,如Int或Double,在它产生的字节码里转译成Java原始类型。
1.2 文本
所有在表5.1里列出的基本类型都可以写成文本:literal。文本是直接在代码里写常量值的一种方式。
Java程序员的快速通道
本节里多数文本的语法和在Java里完全一致,因此如果你是一位Java大师,你可以安心地跳过本节的多数内容。你应该看得两个差异分别是Scala的原字串和符号文本,将在第71页描述。
整数文本
类型Int,Long,Short和Byte的整数文本有三种格式:十进制,十六进制和八进制。整数文本的开头方式说明了数字的基。如果数开始于0x或0X,那它是十六进制(基于16),并且可能包含从0到9,及大写或小写的从A到F的数字。举例如下:
scala> val hex = 0x5
hex: Int = 5
scala> val hex2 = 0x00FF
hex2: Int = 255
scala> val magic = 0xcafebabe
magic: Int = -889275714
请注意,不论你用什么形式的整数文本初始化,Scala的shell始终打印输出基于10的整数值。因此解释器会把你用文本0x00FF初始化的hex2变量的值显示为十进制的255。(当然,你也可以不采信我们的话。开始感受语言的好方法是你一边读本章的时候一边在解释器里试试这些语句。)如果数开始于零,就是八进制(基于8)的,并且只可以包含数字0到7。下面是一些例子:
scala> val oct = 035 // (八进制35是十进制29)
oct: Int = 29
scala> val nov = 0777
nov: Int = 511
scala> val dec = 0321
dec: Int = 209
如果数开始于非零数字,并且没有被修饰过,就是十进制(基于10)的。例如:
scala> val dec1 = 31
dec1: Int = 31
scala> val dec2 = 255
dec2: Int = 255
scala> val dec3 = 20
dec3: Int = 20
如果整数文本结束于L或者l,就是Long类型,否则就是Int类型。一些Long类型的整数文本有:
scala> val prog = 0XCAFEBABEL
prog: Long = 3405691582
scala> val tower = 35L
tower: Long = 35
scala> val of = 31l
of: Long = 31
如果Int类型的文本被赋值给Short或者Byte类型的变量,文本就会被看作是能让文本值在那个类型有效范围内那么长的Short或者Byte类型。如:
scala> val little: Short = 367
little: Short = 367
scala> val littler: Byte = 38
littler: Byte = 38
浮点数文本
浮点数文本是由十进制数字,可选的小数点和可选的E或e及指数部分组成的。下面是一些浮点数文本的例子:
scala> val big = 1.2345
big: Double = 1.2345
scala> val bigger = 1.2345e1
bigger: Double = 12.345
scala> val biggerStill = 123E45
biggerStill: Double = 1.23E47
请注意指数部分表示的是乘上以10为底的幂次数。因此,1.2345e1就是1.2345乘以101,等于12.345。如果浮点数文本以F或f结束,就是Float类型的,否则就是Double类型的。可选的,Double浮点数文本也可以D或d结尾。Float文本举例如下:
scala> val little = 1.2345F
little: Float = 1.2345
scala> val littleBigger = 3e5f
littleBigger: Float = 300000.0
最后一个值可以用以下(或其他)格式表示为Double类型:
scala> val anotherDouble = 3e5
anotherDouble: Double = 300000.0
scala> val yetAnother = 3e5D
yetAnother: Double = 300000.0
字符文本
字符文本可以是在单引号之间的任何Unicode字符,如:
scala> val a = 'A'
a: Char = A
除了在单引号之间显式地提供字符之外,你还可以提供一个表示字符代码点的前缀反斜杠的八进制或者十六进制数字。八进制数必须在'\0'和'\377'之间。例如字母A的Unicode字符代码点是八进制101。因此:
scala> val c = '\101'
c: Char = A
字符文本同样可以以前缀\u的四位十六进制数字的通用Unicode字符方式给出,如:
scala> val d = '\u0041'
d: Char = A
scala> val f = '\u0044'
f: Char = D
实际上,这种unicode字符可以出现在Scala程序的任何地方。例如你可以这样写一个标识符:
scala> val B\u0041\u0044 = 1
BAD: Int = 1
这个标识符被当作BAD,上面代码里的两个unicode字符扩展之后的结果。通常,这样命名标识符是个坏主意,因为它太难读。然而,这种语法能够允许含非ASCII的Unicode字符的Scala源文件用ASCII来代表。
表格 5.2 特殊字符文本转义序列
文本 | 含义 |
\n | 换行(\u000A) |
\b | 回退(\u0008) |
\t | 制表符(\u0009) |
\f | 换页(\u000C) |
\r | 回车(\u000D) |
\" | 双引号(\u0022) |
\' | 单引号(\u0027) |
\\ | 反斜杠(\u005C) |
最终,还有一些字符文本被表示成特殊的转义序列,参见表格5.2。例如:
scala> val backslash = '\\'
backslash: Char = \
字串文本
字串文本由双引号(")环绕的字符组成:
scala> val hello = "hello"
hello: java.lang.String = hello
引号内的字符语法与字符文本相同,如:
scala> val escapes = "\\\"\'"
escapes: java.lang.String = \"'
由于这种语法对于包含大量转义序列或跨越若干行的字串很笨拙。因此Scala为原始字串:raw String引入了一种特殊的语法。以同一行里的三个引号(""")开始和结束一条原始字串。内部的原始字串可以包含无论何种任意字符,包括新行,引号和特殊字符,当然同一行的三个引号除外。举例来说,下面的程序使用了原始字串打印输出一条消息:
println("""Welcome to Ultamix 3000.
Type "HELP" for help.""")
运行这段代码不会产生完全符合所需的东西,而是:
Welcome to Ultamix 3000.
Type "HELP" for help.
原因是第二行前导的空格被包含在了字串里。为了解决这个常见情况,字串类引入了stripMargin方法。使用的方式是,把管道符号(|)放在每行前面,然后在整个字串上调用stripMargin:
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin)
这样,输出结果就令人满意了:
Welcome to Ultamix 3000.
Type "HELP" for help.
符号文本
符号文本被写成'<标识符>,这里<标识符>可以是任何字母或数字的标识符。这种文本被映射成预定义类scala.Symbol的实例。特别是,文本'cymbal将被编译器扩展为工厂方法调用:Symbol("cymbal")。符号文本典型的应用场景是你在动态类型语言中使用一个标识符。比方说,或许想要定义个更新数据库记录的方法:
scala> def updateRecordByName(r: Symbol, value: Any) {
// code goes here
}
updateRecordByName: (Symbol,Any)Unit
方法带了一个符号参数指明记录的字段名和一个字段应该更新进记录的值。在动态类型语言中,你可以通过传入一个未声明的字段标识符给方法调用这个操作,但Scala里这样会编译不过:
scala> updateRecordByName(favoriteAlbum, "OK Computer")
<console>:6: error: not found: value favoriteAlbum
updateRecordByName(favoriteAlbum, "OK Computer")
基本同样简洁的替代方案是,你可以传递一个符号文本:
scala> updateRecordByName('favoriteAlbum, "OK Computer")
除了发现它的名字之外,没有太多能对符号做的事情:
scala> val s = 'aSymbol
s: Symbol = 'aSymbol
scala> s.name
res20: String = aSymbol
另一件值得注意的事情是符号是被拘禁:interned的。如果你把同一个符号文本写两次,那么两个表达式将指向同一个Symbol对象。
布尔型文本
布尔类型有两个文本,true和false:
scala> val bool = true
bool: Boolean = true
scala> val fool = false
fool: Boolean = false
就这些东西了。现在你简直(literally)[2]可以称为Scala的专家了。
1.3 操作符和方法
Scala为它的基本类型提供了丰富的操作符集。如前几章里描述的,这些操作符实际只是作用在普通方法调用上华丽的语法。例如,1 + 2与(1).+(2)其实是一回事。换句话说,就是Int类包含了叫做+的方法,它带一个Int参数并返回一个Int结果。这个+方法在两个Int相加时被调用:
scala> val sum = 1 + 2 // Scala调用了(1).+(2)
sum: Int = 3
想要证实这点,可以把表达式显式地写成方法调用:
scala> val sumMore = (1).+(2)
sumMore: Int = 3
而真正的事实是,Int包含了许多带不同的参数类型的重载:overload的+方法。[3]例如,Int还有另一个也叫+的方法参数和返回类型为Long。如果你把Long加到Int上,这个替换的+方法就将被调用:
scala> val longSum = 1 + 2L // Scala调用了(1).+(2L)
longSum: Long = 3
符号+是操作符——更明确地说,是中缀操作符。操作符标注不仅限于像+这种其他语言里看上去像操作符一样的东西。你可以把任何方法都当作操作符来标注。例如,类String有一个方法indexOf带一个Char参数。indexOf方法搜索String里第一次出现的指定字符,并返回它的索引或-1如果没有找到。你可以把indexOf当作中缀操作符使用,就像这样:
scala> val s = "Hello, world!"
s: java.lang.String = Hello, world!
scala> s indexOf 'o' // Scala调用了s.indexOf(’o’)
res0: Int = 4
另外,String提供一个重载的indexOf方法,带两个参数,分别是要搜索的字符和从哪个索引开始搜索。(前一个indexOf方法开始于索引零,也就是String开始的地方。)尽管这个indexOf方法带两个参数,你仍然可以用操作符标注的方式使用它。不过当你用操作符标注方式调用带多个参数的方法时,这些参数必须放在括号内。例如,以下是如何把另一种形式的indexOf当作操作符使用的例子(接前例):
scala> s indexOf ('o', 5) // Scala调用了s.indexOf(’o’, 5)
res1: Int = 8
任何方法都可以是操作符
Scala里的操作符不是特殊的语言语法:任何方法都可以是操作符。使用方法的方式使它成为操作符。如果写成s.indexOf('o'),indexOf就不是操作符。不过如果写成,s indexOf 'o',那么indexOf就是操作符了,因为你以操作符标注方式使用它。
目前为止,你已经看到了中缀:infix操作符标注的例子,也就是说调用的方法位于对象和传递给方法的参数或若干参数之间,如“7 + 2”。Scala还有另外两种操作符标注:前缀和后缀。前缀标注中,方法名被放在调用的对象之前,如,-7里的‘-’。后缀标注中,方法放在对象之后,如,“7 toLong”里的“toLong”。
与中缀操作符——操作符带后两个操作数,一个在左一个在右——相反,前缀和后缀操作符都是一元:unary的:它们仅带一个操作数。前缀方式中,操作数在操作符的右边。前缀操作符的例子有-2.0,!found和~0xFF。与中缀操作符一致,这些前缀操作符是在值类型对象上调用方法的简写方式。然而这种情况下,方法名在操作符字符上前缀了“unary_”。例如,Scala会把表达式-2.0转换成方法调用“(2.0).unary_-”。你可以输入通过操作符和显式方法名两种方式对方法的调用来演示这一点:
scala> -2.0 // Scala调用了(2.0).unary_-
res2: Double = -2.0
scala> (2.0).unary_-
res3: Double = -2.0
可以当作前缀操作符用的标识符只有+,-,!和~。因此,如果你定义了名为unary_!的方法,就可以像!p这样在合适的类型值或变量上用前缀操作符方式调用这个方法。但是如果你定义了名为unary_*的方法,就没办法用成前缀操作符了,因为*不是四种可以当作前缀操作符用的标识符之一。你可以像平常那用调用它,如p.unary_*,但如果尝试像*p这么调用,Scala就会把它理解为*.p,这或许就不会是你想当然的了![4]
后缀操作符是不用点或括号调用的不带任何参数的方法。Scala里,你可以舍弃方法调用的空括号。例外就是如果方法带有副作用就加上括号,如println(),不过如果方法没有副作用就可以去掉括号,如String上调用的toLowerCase:
scala> val s = "Hello, world!"
s: java.lang.String = Hello, world!
scala> s.toLowerCase
res4: java.lang.String = hello, world!
后面的这个例子里,方法没带参数,或者还可以去掉点,采用后缀操作符标注方式:
scala> s toLowerCase
res5: java.lang.String = hello, world!
例子里,toLowerCase被当作操作数s上的后缀操作符。
因此要想知道Scala的值类型里你可以用哪些操作符,所有需要做的就是在Scala的API文档里查询定义在值类型上的方法。不过由于本书是Scala的教程,我们会在后续几段里带您快速浏览这些方法中的大部分。
Java程序员的快速通道
本章后续部分描述的Scala的很多方面与Java相同。如果你是一个匆忙的Java牛人,你可以安心地跳到第77页的5.7节,那里描述了在对象相等性方面Scala与Java的差异。
1.4 数学运算
你可以通过中缀操作符,加号(+),减号(-),乘号(*),除号(/)和余数(%),在任何数类型上调用数学方法。以下是一些例子:
scala> 1.2 + 2.3
res6: Double = 3.5
scala> 3 - 1
res7: Int = 2
scala> 'b' - 'a'
res8: Int = 1
scala> 2L * 3L
res9: Long = 6
scala> 11 / 4
res10: Int = 2
scala> 11 % 4
res11: Int = 3
scala> 11.0f / 4.0f
res12: Float = 2.75
scala> 11.0 % 4.0
res13: Double = 3.0
当左右两个操作数都是整数类型时(Int,Long,Byte,Short,或Char),/操作符将返回给你商的整数部分,去掉余数部分。%操作符指明它的余数。
用%符号得到的浮点数余数部分并不遵循IEEE754标准的定义。IEEE754在计算余数时使用四舍五入除法,而不是截尾除法,因此余数的计算与整数的余数操作会有很大的不同。如果你的确想要IEEE754的余数,可以调用scala.Math里的IEEEremainder,例如:
scala> Math.IEEEremainder(11.0, 4.0)
res14: Double = -1.0
数类型还提供了一元前缀+和-操作符(方法unary_+和unary_-),允许你指示文本数是正的还是负的,如-3或+4.0。如果你没有指定一元的+或-,文本数被解释为正的。一元符号+也存在只是为了与一元符号-相协调,不过没有任何效果。一元符号-还可以用来使变量变成负值。举例如下:
scala> val neg = 1 + -3
neg: Int = -2
scala> val y = +3
y: Int = 3
scala> -neg
res15: Int = 2
1.5 关系和逻辑操作
你可以用关系方法:大于(>),小于(<),大于等于(>=)和小于等于(<=)比较数类型,像等号操作符那样,产生一个Boolean结果。另外,你可以使用一元操作符!(unary_!方法)改变Boolean值。以下是一些例子:
scala> 1 > 2
res16: Boolean = false
scala> 1 < 2
res17: Boolean = true
scala> 1.0 <= 1.0
res18: Boolean = true
scala> 3.5f >= 3.6f
res19: Boolean = false
scala> 'a' >= 'A'
res20: Boolean = true
scala> val thisIsBoring = !true
thisIsBoring: Boolean = false
scala> !thisIsBoring
res21: Boolean = true
逻辑方法,逻辑与(&&)和逻辑或(||),以中缀方式带Boolean操作数并产生Boolean结果。如:
scala> val toBe = true
toBe: Boolean = true
scala> val question = toBe || !toBe
question: Boolean = true
scala> val paradox = toBe && !toBe
paradox: Boolean = false
与Java里一样,逻辑与和逻辑或有短路:short-circuit的概念:用这些操作符建造的表达式仅评估最少能决定结果的部分。换句话说,逻辑与和逻辑或表达式的右手侧部分在左手侧部分能决定结果时就不再被评估了。举个例子,如果逻辑与表达式的左手侧计算结果为false,那么表达式的结果将注定是false,因此右手侧部分不再做评估。与之类似,如果逻辑或表达式的左手侧部分计算结果为true,那么表达式的结果将必然是true,于是右手侧部分不再被计算。下面是一些例子:
scala> def salt() = { println("salt"); false }
salt: ()Boolean
scala> def pepper() = { println("pepper"); true }
pepper: ()Boolean
scala> pepper() && salt()
pepper
salt
res22: Boolean = false
scala> salt() && pepper()
salt
res23: Boolean = false
第一个表达式中,pepper和salt都被调用,但第二个里,只有salt被调用。因为salt返回false,所以就没必要调用pepper了。
注意
或许你会想知道如果操作符都只是方法的话短路机制是怎么工作的呢。通常,进入方法之前所有的参数都会被评估,因此方法怎么可能选择不评估他的第二个参数呢?答案是因为所有的Scala方法都有延迟其参数评估乃至取消评估的设置。这个设置被称为叫名参数:by-name parameter,将在9.5节中讨论。
1.6 位操作符
Scala让你能够使用若干位方法对整数类型的单个位执行操作。有:按位与运算(&),按位或运算(|)和按位异或运算(^)。[5]一元按位取补操作符(~,方法unary_~),反转它的操作数的每一位。例如:
scala> 1 & 2
res24: Int = 0
scala> 1 | 2
res25: Int = 3
scala> 1 ˆ 3
res26: Int = 2
scala> ~1
res27: Int = -2
第一个表达式,1 & 2,与运算了1(0001)和2(0010)的每一个位,并产生了0(0000)。第二个表达式,1 | 2,对同样的操作数的每一个位执行或运算,并产生3(0011)。第三个表达式,1 ^ 3,异或1(0001)和3(0011)的每一个位,产生2(0010)。最后的表达式,~1,转换了1(0001)的每一个位,产生了-2,二进制看起来是1111 1111 1111 1111 1111 1111 1111 1111 1111 1110。
Scala整数类型还提供了三个位移方法:左移(<<),右移(>>)和无符号右移(>>>)。使用在中缀操作符方式时,位移方法会按照右侧指定的整数值次数逐位移动左侧的整数。左移和无符号右移在移动的时候填入零。右移则在移动时填入左侧整数的最高位(符号位)。举例如下:
scala> -1 >> 31
res38: Int = -1
scala> -1 >>> 31
res39: Int = 1
scala> 1 << 2
res40: Int = 4
二进制的-1是1111 1111 1111 1111 1111 1111 1111 1111。第一个例子里,-1 >> 31,-1被右移了31个位。由于Int包括32位,这个操作实际就是把最左侧的一位移到了最右侧。[6]由于>>方法在不断右移的时候填入的是1,-1最左侧的一位是1,导致结果与原来左侧的数字一模一样,32位个1,或者说是-1。第二个例子里,-1 >>> 31,最左侧的位再一次不断向右移直至最右侧的位置,但是这次填入的是0。因此这次的结果是二进制的0000 0000 0000 0000 0000 0000 0000 0001,或者说是1。最后一个例子里,1 << 2,左操作数,1,被向左移动2个位置(填入0),产生结果是二进制的0000 0000 0000 0000 0000 0000 0000 0100,或者说是4。
1.7 对象相等性
如果你想比较一下看看两个对象是否相等,可以使用或者==,或它的反义!=。下面举几个例子:
scala> 1 == 2
res24: Boolean = false
scala> 1 != 2
res25: Boolean = true
scala> 2 == 2
res26: Boolean = true
这些操作对所有对象都起作用,而不仅仅是基本类型。例如,你可以用他比较列表:
scala> List(1, 2, 3) == List(1, 2, 3)
res27: Boolean = true
scala> List(1, 2, 3) == List(4, 5, 6)
res28: Boolean = false
进一步,你还可以比较不同类型的两个对象:
scala> 1 == 1.0
res29: Boolean = true
scala> List(1, 2, 3) == "hello"
res30: Boolean = false
你甚至可以比较null,或任何可能是null的东西。不会有任何异常被抛出:
scala> List(1, 2, 3) == null
res31: Boolean = false
scala> null == List(1, 2, 3)
res32: Boolean = false
如你所见,==已经被仔细地加工过,因此在许多情况下你都可以得到你想要的相等性的比较。这只是用了一个非常简单的规则:首先检查左侧是否为null,如果不是,调用equals方法。由于equals是一个方法,因此比较的精度取决于左手边的参数。又由于已经有一个自动的null检查,因此你不需要手动再检查一次了。[7]
这种类型的比较对于不同的对象也会产生true,只要他们的内容是相同的并且它们的equals方法是基于内容编写的。例如,以下是恰好都有五个同样字母的两个字串的比较:
scala> ("he" + "llo") == "hello"
res33: Boolean = true
Scala的==与Java的有何差别
Java里的既可以比较原始类型也可以比较参考类型。对于原始类型,Java的==比较值的相等性,如Scala。然而对于参考类型,Java的==比较了参考相等性:reference equality,也就是说这两个变量是否都指向于JVM堆里的同一个对象。Scala也提供了这种机制,名字是eq。不过,eq和它的反义词,ne,仅仅应用于可以直接映射到Java的对象。eq和ne的全部细节将在11.1节和11.2节给出。还有,可以看一下第二十八章,了解如何编写好的equals方法。
1.8 操作符的优先级和关联性
操作符的优先级决定了表达式的哪个部分先于其他部分被评估。举例来说,表达式2 + 2 * 7计算得16,而不是28,因为*操作符比+操作符有更高的优先级。因此表达式的乘法部分先于加法部分被评估。当然你还可以在表达式里使用括号来厘清评估次序或覆盖优先级。例如,如果你实际上希望上面表达式的值是28,你可以这么写表达式:
(2 + 2) * 7
由于Scala没有操作符,实际上,是以操作符的格式使用方法的一个途径,你或许想知道操作符优先级是怎么做到的。Scala基于操作符格式里方法的第一个字符决定优先级(这个规则有一个例外,稍后再说)。比方说,如果方法名开始于*,那么就比开始于+的方法有更高的优先级。因此2 + 2 * 7将被评估为2 + (2 * 7),而a +++ b *** c(这里a,b和c是值或变量,而+++和***是方法)将被看作是a +++ (b *** c),因为***方法比+++方法有更高的优先级。
表格 5.3 操作符优先级
(所有其他的特殊字符) |
* / % |
+ - |
: |
= ! |
< > |
& |
^ |
| |
(所有字母) |
表格5.3以降序方式展示了根据方法第一个字符指定的优先级,同一行的字符具有同样的优先级。表格中字符的位置越高,以这个字符开始的方法具有的优先级就越高。举例如下:
scala> 2 << 2 + 2
res41: Int = 32
<<方法开始于字符<,在表格5.3里的位置比+(+方法的第一个也是唯一的一个字符)要低。因此<<比+的优先级低,表达式也要在先调用了+方法之后再调用<<方法,如2 << (2 + 2)。我们可以算一下,2 + 2得4,2 << 4得32。下面给出另一个例子:
scala> 2 + 2 << 2
res42: Int = 16
由于第一个字符与前面的例子里一样,因此调用的方法顺序也没有不同。首先+方法被调用,然后是<<方法。因此2 + 2得4,4 << 2得16。
上面提到的优先级规则的一个例外,有关于以等号结束的赋值操作符:assignment operator。如果操作符以等号字符(=)结束,且操作符并非比较操作符<=,>=,==,或=,那么这个操作符的优先级与赋值符(=)相同。也就是说,它比任何其他操作符的优先级都低。例如:
x *= y + 1
与下面的相同:
x *= (y + 1)
因为*=被当作赋值操作符,它的优先级低于+,尽管操作符的第一个字符是*,似乎其优先级高于+。
当同样优先级的多个操作符肩并肩地出现在表达式里,操作符的关联性:associativity决定了操作符分组的方式。Scala里操作符的关联性取决于它的最后一个字符。正如第3章里47页提到的,任何以‘:’字符结尾的方法由它的右手侧操作数调用,并传入左操作数。以其他字符结尾的方法有其他的说法。它们都是被左操作数调用,并传入右操作数。因此a * b变成a.*(b),但是a:::b变成b.:::(a)。然而,不论操作符具有什么样的关联性,它的操作数总是从左到右评估的。因此如果b是一个表达式而不仅仅是一个不可变值的指针的话,那么更精确的意义上说,a:::b将会当作是:
{ val x = a; b.:::(x) }
这个代码块中,a仍然在b之前被评估,然后评估结果被当作操作数传给b的:::方法。
这种关联性规则在同时使用多个具有同优先级的操作符时也会起作用。如果方法结束于:,它们就被自右向左分组;反过来,就是自左向右分组。例如,a ::: b ::: c会被当作a ::: (b ::: c)。而a * b * c被当作(a * b) * c。
操作符优先级也是Scala语言的一部分。你不用怕它。但无论如何,使用括号去厘清什么操作符作用在哪个表达式上都是好的风格。或许你唯一可以确信其他人不用查书就知道的优先级关系就是乘除法操作符(*,/和%),比加减法(+和-)的要高。因此即使a + b << c不用括号也能产生你想要的结果,写成(a + b) << c而得到的简洁性也可能会减少你的同事为了表示不满在操作符注释里写你名字的频率,“bills!*&^%~code!”[8]
1.9 富包装器
你可以在Scala基本类型上调用的方法远多于前几段里面讲到过的。表格5.4里罗列了几个例子。这些方法的使用要通过隐式转换:implicit conversion,一种将在第二十一章描述其细节的技术。现在所有要知道的就是本章介绍过的每个基本类型,都有一个“富包装器”可以提供许多额外的方法。因此,想要看到基本类型的所有可用方法,你还应该查看一下每个基本类型的富包装器的API文档。这些类可参见表格5.5。
表格 5.4 一些富操作
代码 | 结果 |
0 max 5 | 5 |
0 min 5 | 0 |
-2.7 abs | 2.7 |
-2.7 round | -3L |
1.5 isInfinity | false |
(1.0 / 0) isInfinity | true |
4 to 6 | Range(4, 5, 6) |
"bob" capitalize | "Bob" |
"robert" drop 2 | "bert" |
表格 5.5 富包装类
基本类型 | 富包装 |
Byte | scala.runtime.RichByte |
Short | scala.runtime.RichShort |
Int | scala.runtime.RichInt |
Long | scala.runtime.RichLong |
Char | scala.runtime.RichChar |
String | scala.runtime.RichString |
Float | scala.runtime.RichFloat |
Double | scala.runtime.RichDouble |
Boolean | scala.runtime.RichBoolean |
[1] 包的概念在第2章的第二步作了简要说明,将在第十三章做更深入介绍。
[2] 象征意义的说法。(似乎是双关语)
[3] 重载的方法有同样的名称和不同的参数类型。第6章会对方法重载做更多说明。
[4] 然而,不是一点儿希望都没有。仍然有极微弱的机会,让你的带有*p的程序或许能像C++那样被编译。
[5] 按位异或方法对它的操作数执行互斥或:exclusive or操作。一致的位产生0。差异的位产生1。因此0011 ^ 0101产生0110。
[6] 数字类型的最左侧位是符号位。如果最左侧位是1,数字就是负的,如果是0,数字就是正的。
[7] 自动检查机制不会检查右手侧的参数,但是任何合理的equals方法都应在参数为null的时候返回false。
[8] 到目前为止,你应该能指出给出的这段代码,Scala编译器会调用成(bills.!*&^%~(code)).!()。