对于Scala编程, 我们收集了这些常见代码编写中的陷阱。这些技巧来自于Daniel Sobral,一个曾参加过FreeBSD项目和Java软件开发工程的Scala狂热爱好者。
1. 语法错误
认为 “yield” 像 ”return” 一样。有人会这样写:
- for(i <- 0 to 10) {
- if (i % 2 == 0)
- yield i
- else
- yield -i
- }
正确的表示应该是:
- for(i <- 0 to 10)
- yield {
- if (i % 2 == 0)
- i
- else
- -i
- }
2. 误用和语法错误
滥用scala.xml.XML.loadXXX。这个的语法分析器试图访问外部的DTD、strip组件或类似的东西。在scala.xml.parsing.ConstructingParser.fromXXX中有另一个可选的语法分析器。同时,在处理XML时忘记了等号两端的空格。比如:
这段代码真正的意思是:
- val xml.$equal$less(root).$slash$greater
这种情况的发生是由于操作符相当随意,而且scala采用这样一种事实:字母数字字符与非字母数字字符通过下划线可以结合成为一个有效的标识符。这也使得“x+y”这样的表达式不会被当成一个标识符。而应该注意 “x_+”是一个有效的标识符。所以,赋值标识符的写法应该是:
3. 用法错误
为那些根本不是无关紧要的应用加入Application特征。
- object MyScalaApp extends Application {
- // ... body ...
- }
示例部分的问题在于,body部分在单元对象初始化时执行。首先,单元初始化的执行是异步的,因此你的整个程序不能与其它线程交互;其次,即时编译器(JIT)不会优化它,因此你的程序速度慢下来,这是没有必要的。
另外,不能与其它线程的交互也意味着你会忘记测试应用程序的GUI或者Actors。
4. 用法错误
试图模式匹配一个字符串的正则表达式,而又假定该正则表达式是无界的:
- val r = """(\d+)""".r
- val s = "--> 5 <---"
- s match {
- case r(n) => println("This won't match")
- case _ => println("This will")
- }
此处的问题在于, 当模式模式匹配时, Scala的正则表达式表现为如同开始于”^”,结束于”$”。使之工作的正确写法是:
- val r = """(\d+)""".r
- val s = "--> 5 <---"
- r findFirstIn s match {
- case Some(n) => println("Matches 5 to "+n)
- case _ => println("Won't match")
- }
或者确保模式能匹配任意前缀和后缀:
- val r = """.*(\d+).*""".r
- val s = "--> 5 <---"
- s match {
- case r(n) => println("This will match the first group of r, "+n+", to 5")
- case _ => println("Won't match")
- }
5. 用法错误
把var和val认为是字段(fields):
Scala强制使用统一访问准则(Uniform Access Principle),这使得我们无法直接引用一个字段。所有对任意字段的访问只能通过getters和setters。val和var事实上只是定义一个字段,getter作为val字段,对于var则定义一个setter。
Java程序员通常认为var和val是字段,而当发现在他们的方法中它们共享相同的命名空间时,常常觉得惊讶。因此,不能重复使用它们的名字。共享命名空间的是自动定义的getter和setter而不是字段本身。通常程序员们会试图寻找一种访问字段的方法,从而可以绕过限制——但这只是徒劳,统一访问准则是无法违背的。它的另一个后果是,当进行子类化时val会覆盖def。其它方法是行不通的,因为val增加了不变性保证,而def没有。
当你需要重载时,没有任何准则会指导你如何使用私有的getters和setters。Scala编译器和库代码常使用私有值的别名和缩写,反之公有的getters和setters则使用fullyCamelNamingConventions(一种命名规范)。其它的建议包括:重命名、实例中的单元化,甚至子类化。这些建议的例子如下:
重命名
- class User(val name: String, initialPassword: String) {
- private lazy var encryptedPassword = encrypt(initialPassword, salt)
- private lazy var salt = scala.util.Random.nextInt
-
- private def encrypt(plainText: String, salt: Int): String = { ... }
- private def decrypt(encryptedText: String, salt: Int): String = { ... }
-
- def password = decrypt(encryptedPassword, salt)
- def password_=(newPassword: String) = encrypt(newPassword, salt)
- }
单例模式(Singleton)
- class User(initialName: String, initialPassword: String) {
- private object fields {
- var name: String = initialName;
- var password: String = initialPassword;
- }
- def name = fields.name
- def name_=(newName: String) = fields.name = newName
- def password = fields.password
- def password_=(newPassword: String) = fields.password = newPassword
- }
或者,对于一个类来说,可以为相等关系或hashCode自动定义可被重用的方法
- class User(name0: String, password0: String) {
- private case class Fields(var name: String, var password0: String)
- private object fields extends Fields(name0, password0)
-
-
- def name = fields.name
- def name_=(newName: String) = fields.name = newName
- def password = fields.password
- def password_=(newPassword: String) = fields.password = newPassword
- }
子类化
- case class Customer(name: String)
- class ValidatingCustomer(name0: String) extends Customer(name0) {
- require(name0.length < 5)
-
- def name_=(newName : String) =
- if (newName.length < 5) error("too short")
- else super.name_=(newName)
- }
- val cust = new ValidatingCustomer("xyz123")
6. 用法错误
忘记类型擦除(type erasure)。当你声明了一个类C[A]、一个泛型T[A]或者一个函数或者方法m[A]后,A在运行时并不存在。这意味着,对于实例来讲,任何参数都将被编译成AnyRef,即使编译器能够保证在编译过程中类型不会被忽略掉。
这也意味着在编译时你不能使用类型参数A。例如,下面这些代码将不会工作:
- def checkList[A](l: List[A]) = l match {
- case _ : List[Int] => println("List of Ints")
- case _ : List[String] => println("List of Strings")
- case _ => println("Something else")
- }
在运行时,被传递的List没有类型参数。 而List[Int]和List[String]都将会变成List[_]. 因此只有第一种情况会被调用。
你也可以在一定范围内不使用这种方法,而采用实验性的特性Manifest, 像这样:
- def checkList[A](l: List[A])(implicit m: scala.reflect.Manifest[A]) = m.toString match {
- case "int" => println("List of Ints")
- case "java.lang.String" => println("List of Strings")
- case _ => println("Something else")
- }
7. 设计错误
Implicit关键字的使用不小心。Implicits非常强大,但要小心,普通类型不能使用隐式参数或者进行隐匿转换。
例如,下面一个implicit表达式:
- implicit def string2Int(s: String): Int = s.toInt
这是一个不好的做法,因为有人可能错误的使用了一个字符串来代替Int。对于上面的这种情况,更好的方法是使用一个类。
- case class Age(n: Int)
- implicit def string2Age(s: String) = Age(s.toInt)
- implicit def int2Age(n: Int) = new Age(n)
- implicit def age2Int(a: Age) = a.n
这将会使你很自由的将Age与String或者Int结合起来,而不是让String和Int结合。类似的,当使用隐式参数时,不要像这样做:
- case class Person(name: String)(implicit age: Int)
这不仅因为它容易在隐式参数间产生冲突,而且可能导致在毫无提示情况下传递一个隐式的age, 而接收者需要的只是隐式的Int或者其它类型。同样,解决办法是使用一个特定的类。
另一种可能导致implicit用法出问题的情况是有偏好的使用操作符。你可能认为”~”是字符串匹配时最好的操作符,而其他人可能会使用矩阵等价(matrix equivalence),分析器连接等(符号)。因此,如果你使用它们,请确保你能够很容易的分离其作用域。
8. 设计错误
设计不佳的等价方法。尤其是:
◆试着使用“==”代替“equals”(这让你可以使用“!=”)
◆使用这样的定义:
- def equals(other: MyClass): Boolean
而不是这样的:
- override def equals(other: Any): Boolean
◆忘记重载hashCode,以确保当a==b时a.hashCode==b.hashCode(反之不一定成立)。
◆不可以这样做交换: if a==b then b==a。特别地,当考虑子类化时,超类是否知道如何与一个子类进行对比,即使它不知道该子类是否存在。如果需要请查看canEquals的用法。
◆不可以这样做传递: if a==b and b ==c then a==c。
9. 用法错误
在Unix/Linux/*BSD的系统中,对你的主机进行了命名却没有在主机文���中声明。特别的,下面这条指令不会工作:
在这种情况下,fsc和scala都不会工作,而scalac则可以。这是因为fsc运行在背景模式下,通过TCP套接字监听连接来加速编译,而scala却用它来加快脚本的执行速度。
10.风格错误
使用while。虽然它有自己的用处,但大多数时候使用for往往更好。在谈到for时,用它们来产生索引不是一个好的做法。
避免这样的使用:
- def matchingChars(string: String, characters: String) = {
- var m = ""
- for(i <- 0 until string.length)
- if ((characters contains string(i)) && !(m contains string(i)))
- m += string(i)
- m
- }
而应该使用:
- def matchingChars(string: String, characters: String) = {
- var m = ""
- for(c <- string)
- if ((characters contains c) && !(m contains c))
- m += c
- m
- }
如果有人需要返回一个索引,可以使用下面的形式来代替按索引迭代的方法。如果对性能有要求,它可以较好的应用在投影(projection)(Scala 2.7)和视图(Scala 2.8)中。
- def indicesOf(s: String, c: Char) = for {
- (sc, index) <- s.zipWithIndex
- if c == sc
- } yield index