为什么可空布尔值不允许 if(nullable),但允许 if(nullable == true)?

36

这段代码可以编译:

private static void Main(string[] args)
{
    bool? fred = true;

    if (fred == true)
        Console.WriteLine("fred is true");
    else if (fred == false)
         Console.WriteLine("fred is false");
    else Console.WriteLine("fred is null");
}

这段代码无法编译

private static void Main(string[] args)
{
    bool? fred = true;

    if (fred)
        Console.WriteLine("fred is true");
    else if (!fred)
         Console.WriteLine("fred is false");
    else Console.WriteLine("fred is null");
}

我以为if(booleanExpression == true)是一种多余的表达方式。为什么在这种情况下它不是多余的?

6个回答

61
Nullable<bool>不能隐式转换为bool。事实上,从boolNullable<bool>有一个隐式的转换,这也是第一种版本中每个布尔常量发生的事情(按语言术语)。然后应用bool operator==(Nullable<bool>, Nullable<bool>运算符。(这与其他抬升运算符不完全相同,结果只是bool,而不是Nullable<bool>。)
换句话说,表达式“fred == false”的类型是bool,而表达式“fred”的类型是Nullable<bool>,因此你不能将其用作“if”表达式。
编辑:回答评论,从Nullable<T>T从来没有隐式转换,原因充分-隐式转换不应该引发异常,除非你想让null被隐式转换为default(T),否则就几乎没别的办法了。
此外,如果两者之间存在隐式转换,像“nullable + nonNullable”这样的表达式会非常令人困惑(对于支持+的类型,如int)。根据哪个操作数进行转换,将同时可用+(T?,T?)和+(T,T)。但结果可能会非常不同!
我完全支持只从Nullable<T>T有显式的转换这个决定。

为什么从Nullable<bool>到bool的隐式转换不存在?有什么见解吗? - Scott Dorman
谢谢!:D。我还想知道为什么他们没有在这里给我们一个隐式转换。这肯定是“可修复的”..... - Quibblesome
@Scott Dorman:如果 Nullable<bool> 的 HasValue 等于 false,你会将它隐式转换为什么 bool 值? - jason
我猜你可以创建一些异常,将转换反过来(bool 转为可空 bool),或者至少在编译时将 if(fred) 转换为 if(fred == true)。以其目前的状态来看,这似乎有点愚蠢。 - Quibblesome
2
完全同意禁止从Nullable<T> -> T进行隐式转换。隐式转换不应该抛出异常。这保持了简单的规则:隐式=安全,显式=危险。 - JaredPar
@Jon Skeet,@Jason:请忽略我的问题...我完全没有考虑过,如果我已经知道答案,我就不会问了。 :) 谢谢。无论如何,我认为这个澄清使得答案更加完整。我不能再点赞了,否则我会的。 - Scott Dorman

8

因为fred不是布尔类型。它是一个结构体,其中有一个名为IsNull、HasValue或其他名称的布尔属性... 名为fred的对象是包含布尔值和值的复合对象,而不是原始布尔值本身...

下面是可空整数的实现示例。通用的Nullable几乎肯定是以类似的方式实现的(但是通用)。您可以在此处查看如何实现隐式和显式转换..

public struct DBInt
   {
       // The Null member represents an unknown DBInt value.
       public static readonly DBInt Null = new DBInt();
       // When the defined field is true, this DBInt represents a known value
       // which is stored in the value field. When the defined field is false,
       // this DBInt represents an unknown value, and the value field is 0.
       int value;
       bool defined;
       // Private instance constructor. Creates a DBInt with a known value.
       DBInt(int value) 
       {
              this.value = value;
              this.defined = true;
       }
       // The IsNull property is true if this DBInt represents an unknown value.
       public bool IsNull { get { return !defined; } }
       // The Value property is the known value of this DBInt, or 0 if this
       // DBInt represents an unknown value.
       public int Value { get { return value; } }
       // Implicit conversion from int to DBInt.
       public static implicit operator DBInt(int x) 
       { return new DBInt(x); }

       // Explicit conversion from DBInt to int. Throws an exception if the
       // given DBInt represents an unknown value.
       public static explicit operator int(DBInt x) 
       {
              if (!x.defined) throw new InvalidOperationException();
              return x.value;
       }
       public static DBInt operator +(DBInt x) 
       { return x; }
       public static DBInt operator -(DBInt x) 
       { return x.defined? -x.value: Null; }
       public static DBInt operator +(DBInt x, DBInt y) 
       {
              return x.defined && y.defined? 
                      x.value + y.value: Null;
       }
       public static DBInt operator -(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                      x.value - y.value: Null;
       }
       public static DBInt operator *(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                      x.value * y.value: Null;
       }
       public static DBInt operator /(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value / y.value: Null;
       }
       public static DBInt operator %(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                      x.value % y.value: Null;
       }
       public static DBBool operator ==(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value == y.value: DBBool.Null;
       }
       public static DBBool operator !=(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value != y.value: DBBool.Null;
       }
       public static DBBool operator >(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value > y.value: DBBool.Null;
       }
       public static DBBool operator <(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value < y.value: DBBool.Null;
       }
       public static DBBool operator >=(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                      x.value >= y.value: DBBool.Null;
       }
       public static DBBool operator <=(DBInt x, DBInt y) 
       {
              return x.defined && y.defined?  
                     x.value <= y.value: DBBool.Null;
       }
       public override bool Equals(object o) 
       {
              try { return (bool) (this == (DBInt) o); } 
              catch  { return false; }
       }
       public override int GetHashCode() 
       { return (defined)? value: 0; }   
       public override string ToString() 
       { return (defined)? .ToString(): "DBInt.Null"; }   
   }

是的,我感激这个,但“== true”应该是一种冗余。这意味着使用通常冗余的“== true”实际上比执行以下操作更简洁:if(fred != null && fred.Value){} - Quibblesome
只有当左侧为布尔值时,它才应该是冗余的。但在这种情况下,它不是。 - Jon Skeet
或者,如果你真的非常想写“if(fred)”,那么请编写自己的可空布尔值,公共结构体MyNullBool {},建议采用上面的DBInt模型(续) - Charles Bretana
...(续)...并添加一个隐式转换,例如public static implicit operator bool(MyNullBool insideVal) {if ( !insideVal.defined) throw InvalidOperationException(); return insideVal.value; } ----- 但我不建议这样做,因为它有点晦涩。 - Charles Bretana

3

语句Nullable<bool> == true隐式地检查Nullable<bool> == (Nullable<bool>)true

请注意,Nullable<bool>本身不是布尔值。它是一个包装布尔值的对象,也可以设置为null。


0
从技术上讲,如果您实现了 true 运算符,则裸条件测试不需要隐式转换为 bool 类型。
bool? nullableBool = null;
SqlBoolean sqlBoolean = SqlBoolean.Null;
bool plainBool = sqlBoolean; // won't compile, no implicit conversion
if (sqlBoolean) { } // will compile, SqlBoolean implements true operator

原问题是寻找一种类似于 SQL 风格的 null 实现,其中 null 更像未知,而 Nullable 实现更像将 null 添加为额外可能的值。例如,比较如下:
if (((int?)null) != 0) { } //block will execute since null is "different" from 0
if (SqlInt32.Null != 0) { }  // block won't execute since "unknown" might have value 0

System.Data.SqlTypes中的类型提供了更多类似于数据库的行为。


0
如果你将fred转换为布尔值,它将会编译通过:
  if (( bool )fred )
      (...)

我认为当你比较bool?和bool时,编译器会进行隐式转换,执行比较,然后返回true或false。结果:表达式求值为bool。

当你不将bool?与其他内容进行比较时,表达式的求值结果为bool?,这是不合法的。


0
实现问题可以完美地表述为: Fred 的类型为 Nullable<bool>,而 ! 运算符未定义为 Nullable<bool>。没有理由将 Nullable<bool> 上的 ! 运算符定义为基于 bool
引用微软的话:

当使用可空类型进行比较时,如果其中一个可空类型为 null,则比较始终被评估为 false。

该规则没有提及隐式转换。它只是一个任意的约定,旨在保证没有布尔表达式出现异常。一旦建立了这个规则,我们就知道如何编写代码。不幸的是,Microsoft错过了这个一元运算符。为了与二元运算符行为保持一致,以下代码应该有一个愉快的结局。
因此
static void Main(string[] args)
{
    bool? fred = null;

    if (!fred)
    {
        Console.WriteLine("you should not see this");
    }
    else
    {
        Console.WriteLine("Microsoft fixed this in 4.5!!!");
    }
}

我敢打赌,现在有些程序员正在写fred==false,而微软则修复这个看似最后的空值问题。


这是错误的。对于可空布尔值,! 是被定义的。尽管为什么要首先定义它是奇怪的。 - MgSam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接