Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

Nullable types

Posted on 2004-07-08 11:31  Flier Lu  阅读(1326)  评论(0编辑  收藏  举报

http://www.blogcn.com/user8/flier_lu/index.html?id=2151342&run=.07A3756

 上周 MS 发布了最新的 C# 2.0 版本语言规范,其中一个很有趣的新增语法特性是 nullable types。通过这种语法,可以让一个普通内建类型的内容为空(NULL)。之所以新增这个类型,很大程度上应该是为了从语言一级对与关系型数据库的交互进行封装(估计八成这个 BT 的需求是 yukon 组提出来的,呵呵)
    与普通程序语言的内建类型不同,数据库的字段在有类型的同时,都可以单独指定是否能够为空。例如
以下为引用:

CREATE TABLE [dbo].[TestTable] (
[ID] [int] IDENTITY (1, 1) NOT NULL ,
[Name] [varchar] (20) NOT NULL ,
[Desc] [varchar] (255) NULL,
[Age] [int] NULL
}


    这样一段 TSQL 语法构造出来的表中,Desc 和 Age 字段可以存在一个空(NULL)的特殊状态。而在传统的 C++/Java 语言中,是无法通过内建类型直接表达这个语义的,而只能通过设计来模拟,如
以下为引用:

struct TestRecord
{
  int ID;
  std::string name;
  char *desc;
  int *age;

  TestRecord() : desc(NULL), age(NULL)
  {
  }

  ~TestRecord()
  {
    if(desc) delete[] desc;
    if(age) delete age;
  }

  // 其他实现函数
};

TestRecord rec = tblTest.GetRecord(...);

if(rec.desc) std::cout << rec.desc << std::endl;
if(rec.age) std::cout << *rec.age << std::endl;


    通过一个指向实际数据的指针,可以很容易模拟一个具有空语义的字段。但这种模拟需要太多薄记工作,如果要大规模使用,则需要做一个较为完整的包装模板类,如
以下为引用:

template <typename T>
class Nullable
{
private:
  T *m_value;
public:
  Nullable() : m_value(NULL)
  {
  }
  ~Nullable()
  {
    if(m_value) delete m_value;
  }
  bool hasValue(void) const { return m_value != NULL };
  operator T(void) const { return *m_value; }

  // 其他包装函数
};

// 需要针对内建类型做特化处理
template <typename T>
std::ostream& operator << (std::ostream& os, const Nullable<T>& value);

struct TestRecord
{
  int ID;
  std::string name;
  Nullable<std::string> desc;
  Nullable<int> age;
};

TestRecord rec = tblTest.GetRecord(...);

if(rec.desc.hasValue()) std::cout << rec.desc << std::endl;
if(rec.age.hasValue()) std::cout << rec.age << std::endl;


    而这种包装,对于不支持 C++ 如此灵活模板机制的 C#/Java 来说,要实现就更加复杂了。如 SourceForge 上有一个 C# 实现的 NullableTypes 项目,就是通过一个支持查询变量是否为空的 INullable 接口,和一系列底层支持类,完成类似的语义,如
以下为引用:

public sealed class TestRecord
{
  public int ID;
  public string name;
  public NullableString desc;
  public NullableInt32 age;
}

TestRecord rec = tblTest.GetRecord(...);

if(!rec.desc.IsNull) Console.WriteLn(rec.desc.Value);
if(!rec.age.IsNull) Console.WriteLn(rec.age.Value);


    语义上来说,这些模拟都是可行的,但语法上看起来实在太罗嗦了,而且也无法做到与现有类型系统的语法级无缝集成。因此 Anders Hejlsberg 决定增加倍受争议的 Nullable Types。这个语法要是搁 Java 里面,Java 社区非得为维护语法纯洁性闹翻天,呵呵,不过搁 MS 这儿则感觉是顺其自然,很能体现 MS 的实用化设计风格。毕竟 MS 系统的程序员大多已经习惯 MS 这样的修修补补。连 Java 和 C++ 都折腾出那么多所谓“增强”特性,自家的 C# 改改还用犹豫吗?呵呵
    既然 MS 在编译器一级提供了这种特性,语法上的简洁性肯定是能够很好得到保障的,如
以下为引用:

int? x = 123;
int? y = null;
if (x.HasValue) Console.WriteLine(x.Value);
if (y.HasValue) Console.WriteLine(y.Value);


    一个简单的 ? 号,表示此类型是一个 nullable type,例如 bool? 类型将有三种可能值,NULL, true 和 false,呵呵,传说中的三态变量啊 :P 而这一切实际上都是编译器做的障眼法,底层实现是通过一个泛型类型完成的,如
以下为引用:

struct Nullable<T>
{
    public bool HasValue;
    public T Value;
}


    前面那个例子实际上被编译器翻译成类似下面的伪代码
以下为引用:

Nullable<int> x = new Nullable<int>(125);
Nullable<int> y = Nullable<int>.NullValue;
Nullable<int> z =  (x.HasValue && y.HasValue) ? new Nullable<int>(x.Value + y.Value) : Nullable<int>.NullValue;


    比较幸福的是 MS 在编译器一级内建提供了 nullable 类型和普通内建类型之间的隐式转换,如
以下为引用:

int i = 123;
int? x = i;   // int --> int?
double? y = x; // int? --> double?
int? z = (int?)y; // double? --> int?
int j = (int)z;   // int? --> int


    因此在语法上这种 nullable 类型能够很轻松的融入现有 C# 语法中去,而将薄记工作交给编译器完成。

    但新增这种语法的代价,是必须要了解一些对于 nullable 的值进行计算的新规则,如
以下为引用:

int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x + y;


    对这种情况来说,x 和 y 都有可能为空,因此 z 只有在 x 和 y 都不为空的时候,才能计算得到非空值。也就是说 int? z = x + y 等价于
以下为引用:

int? z = x.HasValue && y.HasValue ? x.Value + y.Value : (int?)null;


    仔细考虑一下,实际上这种规定是非常有道理的。因为 null 这个值是一个非常特殊的值,他不是一个有效范围内的值,也不能等价于 0 或者长度为 0 的字符串,是一种特殊的存在。将一个数字与 null 相加是没有任何意义的,只能返回 null。故而上述规则成立。推广开来,对下面这种情况
以下为引用:

int? x = GetNullableInt();
int? y = x + 1;


    只有 x 非空的时候,y 才会计算得到一个非空值,否则都是 null。

    null 值带来的令人困惑的另外一个问题是 ==, !=, <, >, <=, >= 等比较操作符如何对各种组合进行操作?C# FAQ 里面有一篇文章解释了为什么比较操作符需要返回一个二值的 bool 而非三值的 bool?。

    Why don't nullable relational operators return bool? instead of bool? 

    如果要返回一个 bool? 则比较行为应该在一个三值 bool 的世界里面进行,而在这样一个世界里面,任何东西与 null 比较都会返回一个 null,也就是说 null 不等于任何东西。这就造成类似下面的语法无法发挥身处二值 bool 世界我们的预期语义
以下为引用:

void Process(int? p)
{
    if (p == null)
    {
        // do some processing...
    }
}


    必须将比较操作改成 if (!p.HasValue) 才能在三值 bool? 的世界里面正常运转,呵呵。因此设计者决定将比较操作直接返回二值 bool,使我们能够使用对引用类型进行比较相同的语法和语义对 nullable 类型进行比较。

    此外为了简化对 nullable 类型的操作,C# 2.0 还提供了一个新的 null coalescing operator (??) 运算符,用于比较值是否为空,为空则返回缺省值。使用方法如下:
以下为引用:

int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x ?? y;
int i = z ?? -1;


    通过这个运算符,可以很容易实现缺省值的语义,如
以下为引用:

string? s = GetStringValue();
Console.WriteLine(s ?? "Unspecified");


    可以看到 C# 做了相当多的工作来辅助 nullable 类型的简化使用。

    有兴趣进一步了解的朋友可以参考 C# 2.0 版本语言规范 以及下面一些讨论文章:

    Eric Gunnerson's Nullable types in C#

    TheServerSide's 3 Month Anniversary: An Auspicious Beginning

    李建忠的 Updated C# V2.0 Specifications,Nullable Types(空属类型),编程语言杂谈

btw: 上个月刚刚调了项目组,折腾一个月总算理清了大部分头绪,开始恢复更新 blog 了,呵呵