空指针(Null Pointers),断言(Asserts)和警告(Warnings)

空指针

那么现在假设你在网站上放了个调查,询问读者的姓名和年龄。有一个问题是,如果由于某些原因一些读者不想给出他们的年龄——他们很顽固,拒绝填写这一栏。那么可怜的数据库管理员要怎么做呢?

假设年龄是由一个int来表示的,有两种方式来解决这个问题。最常见的方式(也是最错误的)是当没有收集到年龄信息的时候,为年龄假设某种“特殊”的值。所以,假设当没有收集到数据时,年龄为-1,否则当有数据时,则填入年龄(即使它是无效的!)。它暂时完成了工作,直到你要开始对年龄进行计算,例如,计算网站访客的平均年龄。由于你忘了考虑这个特殊值,最后可能访客年龄的平均值为7½,然后你就会雇佣设计师将长的单词去掉并到处使用简单的色块。

另一种方式,也就是正确的方式是将年龄存储在一个类型为“int或null”的字段中。下面是这个用于存储年龄的SQL表:

create table users
(
  userid serial,
  name text not null,
  age int             -- 可能为null
);

如果没有收集到年龄数据,那么存到数据库中的是一个特殊的SQL NULL值。当你要计算平均值或其他东西的时候,SQL会自动忽略这些空值。

编程语言也支持空值,虽然某些语言中的使用要比其他一些简单。在Perl中,任何量(即数字或者字符串)都可以是undef(空值在Perl中的说法)。这是导致很多警告的一个原因,但这些警告往往会被没有经验的程序员所忽略,即便这些警告可能表示严重的错误。在Java中,任何指向对象的引用都可以为空,所以在Java中可以将年龄存为Integer,就能让年龄的引用为null。在C语言中,指针当然能为空,但是如果要让一个简单的整数为空,首先要将其装箱为一个由malloc在堆上分配的对象。

OCaml则有其自己对空值问题的优雅解决方法,使用一个简单的多态变体类型,定义(在Pervasives中的)为:

type 'a option = None | Some of 'a

A "null pointer" is written None. The type of age in our example above (an int which can be null) is int option [remember: backwards like int list and int binary_tree].

# Some 3;;
- : int option = Some 3

What about a list of optional ints?

# [ None; Some 3; Some 6; None ];;
- : int option list = [None; Some 3; Some 6; None]

And what about an optional list of ints?

# Some [1; 2; 3];;
- : int list option = Some [1; 2; 3]

Assert, warnings, fatal errors, and printing to stderr

One great feature of Perl is the rich set of commands for debugging programs and handling unexpected errors, including the ability to print stack traces, throw and catch exceptions and the like. OCaml doesn't have quite such a rich set of debugging commands - better than Java, about the same as C, not nearly as good as Perl. (We'll talk about exceptions in more detail later on.)

First of all, assert takes an expression as an argument and throws an exception. Assuming that you don't catch this exception (it's probably unwise to catch this exception, particularly for beginners), this results in the program stopping and printing out the source file and line number where the error occurred. An example:

# assert (Sys.os_type = "Win32");;
Exception: Assert_failure ("", 0, 30).

(Running this on Win32, of course, won't throw an error).

You can also just call assert false to stop your program if things just aren't going well, but you're probably better to use ...

failwith "error message" throws a Failure exception, which again assuming you don't try to catch it, will stop the program with the given error message. failwith is often used during pattern matching, like this real example:

  match Sys.os_type with
    "Unix" | "Cygwin" ->   (* code omitted *)
  | "Win32" ->             (* code omitted *)
  | "MacOS" ->             (* code omitted *)
  | _ -> failwith "this system is not supported"

Note a couple of extra pattern matching features in this example too. A so-called "range pattern" is used to match either "Unix" or "Cygwin", and the special _ pattern which matches "anything else".

If you want to debug your program, but, like me, you have an aversion to debuggers which aren't gdb, then you'll probably want to print out a warning some way through your function. Here's an example (note the code highlighted in red):

open Graphics;;

open_graph " 640x480";;
for i = 12 downto 1 do
  let radius = i * 20 in
  prerr_endline ("radius is " ^ (string_of_int radius));
  set_color (if (i mod 2) = 0 then red else yellow);
  fill_circle 320 240 radius
done;;
read_line ();;

If you prefer C-style printf's then try using OCaml's Printf module instead:

open Graphics;;
open Printf;;

open_graph " 640x480";;
for i = 12 downto 1 do
  let radius = i * 20 in
  eprintf "radius is %d\n" radius;
  set_color (if (i mod 2) = 0 then red else yellow);
  fill_circle 320 240 radius
done;;
read_line ();;

Discuss this page