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与底层并发编程机制异同之探

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容