Scala与Java代码的区别(2)

用Null还是不用,这是个问题

习以为常的null

价值千万的错误

Tony Hoare introduced Null references in ALGOL W back in 1965 “simply because it was so easy to implement”, says Mr. Hoare. He talks about that decision considering it “my billion-dollar mistake”.

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

为什么是个错误

TODO: 另开session

它的存在基于一些看似合理的假设

如果我们接受了这种设定,类型系统似乎也可以变得简单

总之

我们在尝试以“通过自己小心谨慎的努力避免出错”来换取“代码的简单”

一段Java代码

class Team {
    private List<String> members = null;

    public Team(List<String> members) {
        this.members = members;
    }

    public int getTeamSize() {
        return members.size();
    }

    public String getFirstMember() {
        return members.get(0);
    }
}

如何不抛NullPointerException

List<String> members = new ArrayList<String>();
members.add("Juan");
members.add("Chen");
members.add("...");

Team team = new Team(members);

System.out.println(team.getTeamSize());
System.out.println(team.getFirstMember());

如果我们的代码可以保证,构造这个Team对象时,一定传入一个非空的列表,则不会抛异常。

不可靠的保证

所以我们在使用这些允许null的语言写代码时,每次使用一个变量或者一个方法的返回值时,都会在心里纠结一下:

它到底会不会是null?

Workarounds

  1. NullObject pattern
  2. 编辑器的提示 (Idea)
  3. @NotNull Annotation (Idea)
  4. 语法层面:a?.b?.c (Groovy)
  5. 小幅改进的类型系统:var name: String? = null,使用前要if(name!=null),否则编译不过 (Kotlin)

完善彻底的解决方案

  1. Scala: Option[T], Some[T], None

对于所有可能为null的变量,或者返回值为null的方法/函数,都要声明为Option

当一个类型声明不是Option,我们可以放心的认为它不会为null

(Haskell: Maybe a, Just a, Nothing)

再强调一遍

对于所有可能为null的变量,或者返回值为null的方法/函数,都要声明为Option

如果一个类型不是Option,我们可以放心的认为它不会为null

这不是由编译器保证的,只能靠我们自己在写代码时注意。

(由于Scala需要跟Java互通,所以还要兼容null,Haskell里没有null

Scala版

class Team(members: List[String]) {
    def getTeamSize: Int = members.size()

    def getFirstMember: Option[String] = members.get(0)
}

为什么:

  1. members的类型不是Option[List[String]],而是List[String]?
  2. getTeamSize的返回类型是Int,不是Option[Int]?
  3. getFirstMember的返回类型是Option[String],而不是String?

Option的简化定义

sealed trait Option[+T]
case class Some[T](t:T) extends Option[T]
case object None extends Option[Nothing]

如何判断一个Option是Some还是None

(1) 命令式风格(通常不推荐):

val name: Option[String] = getNameFromSomeWhere()
if(name.isDefined) {
    println("The name is: " + name.get()) 
}
if(name.isEmpty) {
    println("The name is None")
}

因为其中的name.get()有可能抛出Exception in thread "main" java.util.NoSuchElementException: None.get,需要小心检查在它之前是否确保它肯定是Some,在函数式风格的代码中,我们不应该这么做

(2) 使用Patten match

这是一种比较推荐的方式,特别是需要针对不同的情况进行不同的处理的时候:

val name: Option[String] = getNameFromSomeWhere()
name match {
    case Some(n) => // do something with `n`
    case None =>  // do some other things
}

不方便之处,在于有点长。如果我们仅仅需要取值的话,这么写太累了:

name match {
    case Some(n) => n + "!!!"
    case None => "<empty name>"
}

如果只会这两种方式,你可能会痛恨Option

val name: Option[String] = getNameWhichShouldBeSome()

name match {
    case Some(n) => doSomethingWith(n)
    case None => shouldNotHappen()
}

哪怕我已经确定name的类型是Option[String],但为了得到其真实的值,我还要写三行代码!哪怕有两行我都不需要!

虽然我也可以使用option.get()直接获取值,写成doSomethingWith(name.get()),但前面说了,我们应该尽量避免使用它。

这是让人非常纠结的一件事。好痛恨Option

(3) 善用Option提供的方法

不要老想着先把东西从option里取出来再用,而要善用它提供的方法,在一个安全的环境中使用它里面的值:

val name: Option[String] = getNameFromSomeWhere()

name.map(_ + "!!!").getOrElse("<empty name>")

name.filter(_.length>10).getOrElse("<a short name>")

name.foreach(println)

name orElse Some("another name")

你不需要判断,不需要取值,只需要在合适的方法中,就可以安全而方便的使用它的值。

Option在整个scala库中都大量使用

list.headOption
list.lastOption
list.find
map.get
...

for语法的支持

因为Option中有map,所以我们可以用for yield

val name: Option[String] = getNameFromSomeWhere()

val newName: Option[String]= for {
    n <- name
} yield n + "!!!"

因为Option中有foreach,所以可以用for

for {
    n <- name
} println(n + "!!!")

但是如果Option太多。。。

就算我们使用上面的技巧,还是要写很长的代码:

class User(name: Option[String], age: Option[Int], phone: Option[String]) {
    def getProfile(): Option[String] = {
        for {
            n <- name
            a <- age
            p <- phone
        } yield n + " " + a + " " + p
    }
    def getProfile2(): String = (name, age, phone) match {
        case (Some(n), Some(a), Some(p)) => n + " " + a + " " + p
        case _ => "some information is missing"
    }
}

怎么办?

将在以后Manod那里详细说明,敬请期待