Scala代码编写中常见的十大陷阱

很多Java开发者在学习Scala语言的时候,往往觉得Scala的语法和用法有些过于复杂,充满语法糖,太“甜”了。在使用Scala编写代码时,由于语法和编写习惯的不同,很多开发者会犯相同或相似的错误。一位Scala狂热爱好者近日总结了十大这样的错误,以供参考。

【51CTO精选译文】对于支持并发和分布式处理、高可扩展、基于组件的应用程序来说,Scala的功能是很强大的。它利用了面向对象和函数式程序设计的优点。这种基于Java虚拟机的语言在宣布Twitter正使用它时受到了最多的冲击(相关51CTO评论:从Scala进驻Twitter看多语言混杂系统的前景)。如果使用正确,Scala可以大量减少应用程序对代码的需求。 对于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=<root/> 

这段代码真正的意思是:

val xml.$equal$less(root).$slash$greater  

这种情况的发生是由于操作符相当随意,而且scala采用这样一种事实:字母数字字符与非字母数字字符通过下划线可以结合成为一个有效的标识符。这也使得“x+y”这样的表达式不会被当成一个标识符。而应该注意 “x_+”是一个有效的标识符。所以,赋值标识符的写法应该是:

val xml = <root/> 

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的系统中,对你的主机进行了命名却没有在主机文件中声明。特别的,下面这条指令不会工作:

ping `hostname`  

在这种情况下,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   

【51CTO.com译稿,合作站点转载请注明原文译者和出处为51CTO.com ,且不得修改原文内容。】 原文:10 Scala Programming Pitfalls 作者:mitchp

Scala讲座:函数式语言的体验 Scala讲座:类型系统和相关功能 Adobe架构师谈Scala:功能强大但令人困惑 Scala 2.8的for表达式:性能与运行顺序的 Scala Actor与底层并发编程机制异同之探

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏about云

hadoop开发必读:认识Context类的作用

问题导读: 1.Context能干什么? 2.你对Context类了解多少? 3.Context在mapreduce中的作用是什么? 本文实在能够阅读源码...

35740
来自专栏斑斓

编程修炼 | Scala亮瞎Java的眼(二)

继续上一期的话题,介绍Scala有别于Java的特性。说些题外话,当我推荐Scala时,提出质疑最多的往往不是Java程序员,而是负责团队的管理者,尤其是略懂技...

38950
来自专栏皮皮之路

【JDK1.8】Java 8源码阅读汇总

40270
来自专栏申龙斌的程序人生

零基础学编程022:函数的世界

通过《零基础学编程021:获取股票实时行情数据》的学习,我们已经可以取出“谷歌”股票的开盘价,今天我们要取出GAFATA共6支股票的开盘价。 先回顾上次的代码:...

29160
来自专栏ACM算法日常

I Hate It!(线段树-超详细~)- HDU 1754

很多学校流行一种比较的习惯。老师们很喜欢询问,从某某到某某当中,分数最高的是多少。 这让很多学生很反感。 不管你喜不喜欢,现在需要你做的是,就是按照老师的要求...

10020
来自专栏鹅厂优文

Python 工匠:编写条件分支代码的技巧

我一直觉得编程某种意义是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。

1.2K100
来自专栏嵌入式程序猿

号外号外:无规矩不成方圆(4)

本文MISRA规则由嵌入式程序猿整理自网络,版权归原作者所有 不能使用三字母词 三字母词由2 个问号序列后跟1 个确定字符组成(如, ??- 代表“ ~”(非)...

27350
来自专栏Crossin的编程教室

【Python 第19课】 函数

数学上的函数,是指给定一个输入,就会有唯一输出的一种对应关系。编程语言里的函数跟这个意思差不多,但也有不同。函数就是一块语句,这块语句有个名字,你可以在需要时反...

30970
来自专栏Java帮帮-微信公众号-技术文章全总结

​图;代码轻松理解,代理

代理 代理是英文 Proxy 翻译过来的。我们在生活中见到过的代理,大概最常见的就是朋友圈中卖面膜的同学了。 她们从厂家拿货,然后在朋友圈中宣传,然后卖给熟人。...

31750
来自专栏顶级程序员

Python 工匠:编写条件分支代码的技巧

我一直觉得编程某种意义上是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。

10320

扫码关注云+社区

领取腾讯云代金券