当程序员经常抱怨null错误/异常时,有人会问没有null我们该怎么办。

我对选项类型的酷有一些基本的概念,但我没有最好地表达它的知识或语言技能。对于下面的内容,什么样的解释对普通程序员来说是可以理解的呢?

默认情况下,不希望引用/指针为空 选项类型如何工作,包括简化检查空情况的策略,如 模式匹配和 单细胞生物的理解 替代解决方案,如消息吃nil (我错过的其他方面)


当前回答

微软研究院有个有趣的项目叫做

spec#

它是一个c#扩展,具有非空类型和一些机制来检查你的对象是否为空,尽管,恕我直言,应用契约设计原则可能更合适,对许多由空引用引起的麻烦情况更有帮助。

其他回答

我总是把Null(或nil)看作是一个值的缺失。

有时你想这样,有时又不想。这取决于您正在处理的域。如果缺少中间名是有意义的:没有中间名,那么您的应用程序可以相应地执行。另一方面,如果不应该有空值:第一个名字是空的,那么开发人员会在凌晨2点接到电话。

我还见过代码因检查null而超载和过于复杂。对我来说,这意味着两件事之一: A)在应用程序树中更高的错误 B)糟糕的/不完整的设计

从积极的方面来看——Null可能是检查是否缺少某些东西的更有用的概念之一,没有Null概念的语言在进行数据验证时最终会使事情过于复杂。在这种情况下,如果新变量没有初始化,所述语言通常会将变量设置为空字符串0或空集合。但是,如果空字符串或0或空集合是应用程序的有效值——那么就有问题了。

有时,通过为字段创建特殊/奇怪的值来表示未初始化的状态,可以避免这种情况。但是当一个好心的用户输入特殊值时会发生什么呢?让我们不要陷入数据验证例程的混乱。 如果语言支持空概念,那么所有的关注点都将消失。

默认情况下,不希望引用/指针为空。

我不认为这是null的主要问题,null的主要问题是它们可能意味着两件事:

引用/指针是未初始化的:这里的问题与一般的可变性相同。首先,它使分析代码变得更加困难。 变量为空实际上意味着一些事情:这是Option类型实际形式化的情况。

支持Option类型的语言通常也禁止或不鼓励使用未初始化的变量。

选项类型的工作方式包括简化检查null情况的策略,例如模式匹配。

为了有效,需要在语言中直接支持Option类型。否则就需要大量样板代码来模拟它们。模式匹配和类型推断是使Option类型易于使用的两个关键语言特性。例如:

在f#:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

然而,在像Java这样没有直接支持Option类型的语言中,我们会有这样的东西:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

替代解决方案,如消息吃nil

Objective-C's "message eating nil" is not so much a solution as an attempt to lighten the head-ache of null checking. Basically, instead of throwing a runtime exception when trying to invoke a method on a null object, the expression instead evaluates to null itself. Suspending disbelief, it's as if each instance method begins with if (this == null) return null;. But then there is information loss: you don't know whether the method returned null because it is valid return value, or because the object is actually null. It's a lot like exception swallowing, and doesn't make any progress addressing the issues with null outlined before.

到目前为止,所有的答案都集中在为什么null是一个坏东西,以及如果一种语言可以保证某些值永远不会为null,那么它是多么方便。

然后他们继续建议,如果对所有值强制执行非空性,这将是一个非常棒的想法,如果您添加一个像Option或Maybe这样的概念来表示可能不总是有定义值的类型,就可以做到这一点。这就是Haskell所采用的方法。

这些都是好东西!但它并不排除使用显式可空/非空类型来实现相同的效果。那么,为什么Option仍然是一个好东西呢?毕竟,Scala支持可空值(这是必须的,所以它可以与Java库一起工作),但也支持选项。

问:除了能够从语言中完全删除空值之外,还有什么好处呢?

答:作文

如果从支持null的代码进行朴素转换

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

选项感知代码

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

没有太大的区别!但这也是一种糟糕的使用选项的方式……这种方法更简洁:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

甚至:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

当你开始处理List of Options时,它会变得更好。假设List people本身是可选的:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

这是如何工作的呢?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

对应的带有空检查(甚至是elvis ?:操作符)的代码将非常长。这里真正的技巧是flatMap操作,它允许以一种可空值永远无法实现的方式嵌套理解Options和集合。

Robert Nystrom在这里提供了一篇不错的文章:

http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/

描述了他在为Magpie编程语言添加缺席和失败支持时的思维过程。

微软研究院有个有趣的项目叫做

spec#

它是一个c#扩展,具有非空类型和一些机制来检查你的对象是否为空,尽管,恕我直言,应用契约设计原则可能更合适,对许多由空引用引起的麻烦情况更有帮助。