Scala编码规范与最佳实践-编码风格

编码风格

  1. 尽可能直接在函数定义的地方使用模式匹配。例如,在下面的写法中,match应该被折叠起来(collapse):
list map { item =>   
     item match {     
          case Some(x) => x     
          case None => default   
     } 
}

用下面的写法替代:

list map {
   case Some(x) => x
   case None => default 
}

它很清晰的表达了 list中的元素都被映射,间接的方式让人不容易明白。此时,传入map的函数实则为partial function。

2)避免使用null,而应该使用Option的None。

import java.io._

object CopyBytes extends App {
     var in = None: Option[FileInputStream]
     var out = None: Option[FileOutputStream]
     try {
          in = Some(new FileInputStream("/tmp/Test.class"))
          out = Some(new FileOutputStream("/tmp/Test.class.copy"))
          var c = 0
          while ({c = in.get.read; c !=1}) {
             out.get.write(c)
    }
     } catch {
          case e: IOException => e.printStackTrace
     } finally {
          println("entered finally ...")
          if (in.isDefined) in.get.close
          if (out.isDefined) out.get.close
     }
} 

方法的返回值也要避免返回Null。应考虑返回Option,Either,或者Try。例如:

import scala.util.{Try, Success, Failure} 

def readTextFile(filename: String): Try[List[String]] = { 
    Try(io.Source.fromFile(filename).getLines.toList
)

val filename = "/etc/passwd" 
readTextFile(filename) match {
    case Success(lines) => lines.foreach(println)
    case Failure(f) => println(f) 
}

3)若在Class中需要定义常量,应将其定义为val,并将其放在该类的伴生对象中:

class Pizza (var crustSize: Int, var crustType: String) {
     def this(crustSize: Int) {
          this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
     }
   
     def this(crustType: String) {
          this(Pizza.DEFAULT_CRUST_SIZE, crustType)
     }
   
     def this() {
          this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
     }
     override def toString = s"A $crustSize inch pizza with a $crustType crust"
}

object Pizza {
     val DEFAULT_CRUST_SIZE = 12
     val DEFAULT_CRUST_TYPE = "THIN"
}

4)合理为构造函数或方法提供默认值。例如:

class Socket (val timeout: Int = 10000)

5)如果需要返回多个值时,应返回tuple。

def getStockInfo = {
     //
     ("NFLX", 100.00, 101.00)
}
  1. 作为访问器的方法,如果没有副作用,在声明时建议定义为没有括号。

例如,Scala集合库提供的scala.collection.immutable.Queue中,dequeue方法没有副作用,声明时就没有括号:

import scala.collection.immutable.Queue

val q = Queue(1, 2, 3, 4)
val value = q.dequeue
  1. 将包的公有代码(常量、枚举、类型定义、隐式转换等)放到package object中。
package com.agiledon.myapp

package object model {
     // field
     val MAGIC_NUM = 42 182 | Chapter 6: Objects

     // method
     def echo(a: Any) { println(a) }
   
    // enumeration
     object Margin extends Enumeration {
          type Margin = Value
          val TOP, BOTTOM, LEFT, RIGHT = Value
     }
   
    // type definition
     type MutableMap[K, V] = scala.collection.mutable.Map[K, V]
     val MutableMap = scala.collection.mutable.Map
}
  1. 建议将package object放到与包对象命名空间一致的目录下,并命名为package.scala。以model为例,package.scala文件应放在:
    ±- com
    ±- agiledon
    ±- myapp
    ±- model
    ±- package.scala

  2. 若有多个样例类属于同一类型,应共同继承自一个sealed trait。

sealed trait Message
case class GetCustomers extends Message
case class GetOrders extends Message

:这里的sealed,表示trait的所有实现都必须声明在定义trait的文件中。

  1. 考虑使用renaming clause来简化代码。例如,替换被频繁使用的长名称方法:
import System.out.{println => p}

p("hallo scala")
p("input") 
  1. 在遍历Map对象或者Tuple的List时,且需要访问map的key和value值时,优先考虑采用Partial Function,而非使用_1和_2的形式。例如:
val dollar = Map("China" -> "CNY", "US" -> "DOL")

//perfer
dollar.foreach {
     case (country, currency) => println(s"$country -> $currency")
}

//avoid
dollar.foreach ( x => println(s"$x._1 -> $x._2") )

或者,考虑使用for comprehension:

for ((country, currency) <- dollar) println(s"$country -> $currency")
  1. 遍历集合对象时,如果需要获得并操作集合对象的下标,不要使用如下方式:
val l = List("zero", "one", "two", "three")

for (i <- 0 until l.length) yield (i, l(i))

而应该使用zipWithIndex方法:

for ((number, index) <- l.zipWithIndex) yield (index, number)

或者:

l.zipWithIndex.map(x => (x._2, x._1))

当然,如果需要将索引值放在Tuple的第二个元素,就更方便了。直接使用zipWithIndex即可。

zipWithIndex的索引初始值为0,如果想指定索引的初始值,可以使用zip:

l.zip(Stream from 1)
  1. 应尽量定义小粒度的trait,然后再以混入的方式继承多个trait。例如ScalaTest中的FlatSpec:
class FlatSpec extends FlatSpecLike ...

trait FlatSpecLike extends Suite with ShouldVerb with MustVerb with CanVerb with Informing …

小粒度的trait既有利于重用,同时还有利于对业务逻辑进行单元测试,尤其是当一部分逻辑需要依赖外部环境时,可以运用“关注点分离”的原则,将不依赖于外部环境的逻辑分离到单独的trait中。

  1. 优先使用不可变集合。如果确定要使用可变集合,应明确的引用可变集合的命名空间。不要用使用import scala.collection.mutable._;然后引用 Set,应该用下面的方式替代:
import scala.collections.mutable
val set = mutable.Set()

这样更明确在使用一个可变集合。

  1. 在自己定义的方法和构造函数里,应适当的接受最宽泛的集合类型。通常可以归结为一个: Iterable, Seq, Set, 或 Map。如果你的方法需要一个 sequence,使用 Seq[T],而不是List[T]。这样可以分离集合与它的实现,从而达成更好的可扩展性。

  2. 应谨慎使用流水线转换的形式。当流水线转换的逻辑比较复杂时,应充分考虑代码的可读性,准确地表达开发者的意图,而不过分追求函数式编程的流水线转换风格。例如,我们想要从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:

val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
val orderedVotes = votes
   .groupBy(_._1)
   .map { case (which, counts) =>
     (which, counts.foldLeft(0)(_ + _._2))
   }.toSeq
   .sortBy(_._2)
   .reverse

上面的代码简洁并且正确,但几乎每个读者都不好理解作者的原本意图。一个策略是声明中间结果和参数:

val votesByLang = votes groupBy { case (lang, _) => lang }
val sumByLang = votesByLang map { 
     case (lang, counts) =>
          val countsOnly = counts map { case (_, count) => count }
          (lang, countsOnly.sum)
}
val orderedVotes = sumByLang.toSeq
   .sortBy { case (_, count) => count }
   .reverse

代码也同样简洁,但更清晰的表达了转换的发生(通过命名中间值),和正在操作的数据的结构(通过命名参数)。

  1. 对于Options对象,如果getOrElse能够表达业务逻辑,就应避免对其使用模式匹配。许多集合的操作都提供了返回Options的方法。例如headOption等。
val x = list.headOption getOrElse 0

这要比模式匹配更清楚:

val x = list match 
     case head::_ => head
     case Nil: => 0
  1. 当需要对两个或两个以上的集合进行操作时,应优先考虑使用for表达式,而非map,flatMap等操作。此时,for comprehension会更简洁易读。例如,获取两个字符的所有排列,相同的字符不能出现两次。使用flatMap的代码为:
 val chars = 'a' to 'z'
 val perms = chars flatMap { a =>
   chars flatMap { b =>
     if (a != b) Seq("%c%c".format(a, b))
     else Seq()
   }
 }

使用for comprehension会更易懂:

 val perms = for {
   a <- chars
   b <- chars
   if a != b
 } yield "%c%c".format(a, b)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值