null is not false

null is not false

Rate This
  • Comments 38

The way you typically represent a "missing" or "invalid" value in C# is to use the "null" value of the type. Every reference type has a "null" value; that is, the reference that does not actually refer to anything. And every "normal" value type has a corresponding "nullable" value type which has a null value.

The way these concepts are implemented is completely different. A reference is typically implemented behind the scenes as a 32 or 64 bit number. As we've discussed previously in this space, that number should logically be treated as an "opaque" handle that only the garbage collector knows about, but in practice that number is the offset into the virtual memory space of the process that the referred-to object lives at, inside the managed heap. The number zero is reserved as the representation of null because the operating system reserves the first few pages of virtual memory as invalid, always. There is no chance that by some accident, the zero address is going to be a valid address in the heap.

By contrast, a nullable value type is simply an instance of the value type plus a Boolean that indicates whether the value is to be treated as a value, or as null. It's just a syntactic sugar for passing around a flag. This is because value types need not have any "special" value that has no other meaning; a byte has 256 possible values and every one of them is valid, so a nullable byte has to have some additional storage.

Some languages allow null values of value types or reference types, or both, to be implicitly treated as Booleans. In C, you can say:

int* x = whatever();
if (x) ...

and that is treated as if you'd said "if (x != null)". And similarly for nullable value types; in some languages a null value type is implicitly treated as "false".

The designers of C# considered those features and rejected them. First, because treating references or nullable value types as Booleans is a confusing idiom and a potential rich source of bugs. And second, because semantically it seems presumptuous to automatically translate null -- which should mean "this value is missing" or "this value is unknown" -- to "this value is logically false".

In particular, we want to treat nullable bools as having three states: true, false and null, and not as having three states: true, false and different-kind-of-false. Treating null nullable Booleans as false leads to a number of oddities. Suppose we did, and suppose x is a nullable bool that is equal to null:

if (x)
  Foo();
if (!x)
  Bar();

Neither Foo nor Bar is executed because "not null" is of course also null. (The answer to "what is the opposite of this unknown value?" is "an unknown value".) Does it not seem strange that x and !x are both treated as false? Similarly, if (x | !x) would also be treated as false, which also seems bizarre.

The solution to the problem of these oddities is to avoid the problem in the first place, and not make nulls behave as though they were false.

Next time we'll look at a different aspect of truth-determining: just what is up with those "true" and "false" user-defined operators?

  • Doesn't a byte have 256 possible values (with 255 being the highest of them)?  Or have I been doing it wrong.

  • Enumerable.All has a similar issue, except its tri-state is true, false, and did-not-check-the-predicate-true, which leads to this interesting contradiction:

    var numbers = new int[0];

    bool all = numbers.All(i => false);

    if (all)

        throw new Exception("All items match an unmatchable predicate.");

    Empty sequences always matching the predicate has bitten me on more than one occasion.

  • @Chris B That's expected logically though. If you have an empty sequence, it's true that all the items in it match any predicate.

    In mathematics, this is called a "vacuous statement" (e.g., all members of the empty set are equal to 3).

    I wouldn't call that three-valued logic, as it's the expected behavior of regular boolean logic.

  • Chris B: That's not the same at all. In fact, it's completely correct: all elements in the array match the predicate. All items are also even, and odd.

  • Mr Lippert -

    On the subject of nulls, why the 'struct' constraint on Nullable?  I appreciate references already have a null representation, but would there be any harm in additionally allowing Nullable<ClassName> as an alternative?  I ask because it seems it might be useful sometimes to return a Nullable value to indicate a potential return type of 'not available'.  However, this does not generalise to classes.  What were the factors in making the design decision?

    For example, I would like to do something like this, except allow class values as well as structs:

    interface ICollection<TKey, TValue> where TValue : struct

     {

         TValue? this[TKey key]

         {

             get;

         }

     }

    Thank-you -

  • @Crosbie or just simply allow T? for non-constrained generic parameters [which will become int? for int, int? for int?, and string for string]

  • So, you've explained why implicit boolean treatment of T? is a bad idea.  But you haven't really given any rationale against implicit boolean treatment of T*.  It might also be worth mentioning that C uses the integer literal 0 to mean "null", and so the longhand of `int* p; if (p)` is `if (p != 0)`.  So it seems more likely that rejection of `if (p)` in C# is related to `if (i)` for integral i, which likewise in C means `if (i != 0)` and is rejected in C#, rather than related to System.Nullable, which didn't even exist yet.

  • @Ben: C doesn't have the concept of NULL, or BOOL even. An if statement just means "Is the evaluation of my expression non-zero?" So, it makes sense that

       //C

       if((int*)0) { ... }

    would be legal, since 0 is 0, no matter what form it takes.

    In C#, boolean is a distinct type from integer. If doesn't mean "is my expression non-zero", it means "is my expression non-false". As int* is not convertible to boolean, we cannot adequately answer that question.

    (It could be argued that int* /should/ be convertible to boolean, but as I try to avoid using pointers as much as possible, I have no opinion)

  • @Crosbie: Consider this:

       int?? myInt = null;

       Console.WriteLine(myInt == null); //???

    What should that output? true, because the inner int? is null, or false because the outer int? has a value?

  • I actually enjoy using this in JavaScript and I think it's really handy! I tend to think about it as a contextual operations that checks if a value is null or not, rather than a direct mapping between true-false/null-not null and unfortunately 0-1. In JS context, I don't see it awkward any more to think that !(null) != null and in that realm I don't have to worry about reference vs. value types as well.

    That has been said, I don't see my self comfortable using this when I put my C# hat and I strongly agree that this does not match the style of C# or the culture of their audience. The nutshell of my argument is although language features are transferable, what might look awkward (or wrong if there is such) in one language, could be justifiable in other.

  • Good post, looking forward for the next one about "true" and "false", I always wanted to know what useful cases they can enable.

    In the meanwhile, there's a typo:

    "[...] In particular, we want to treat nullable bools has having three states [...]"

    That "has" should be "as".

  • '(The answer to "what is the opposite of this unknown value?" is "an unknown value".) Does it not seem strange that x and !x are both treated as false?'

    Your decision in parentheses of how to define '!' leads the the apparent contradiction in the second sentence.

    However, if we take an actual language which actually implicitly treats null as false (e.g. Python), we actually find a different definition. When x if null, then !x is treated as !false, which is true.

    (Or, equivalently in Python syntax: not None == True)

  • Sorry. There's a bug in my one line of Python. I forgot to consider operation precedence, so it doesn't prove my point.

    What I wrote is equivalent to:

    not (None == True)

    What I meant was:

    (not None) == True

  • "treating references or nullable value types as null is a confusing idiom"

    Shouldn't this be:

    "treating references or nullable value types as Booleans is a confusing idiom"

  • I don't understand last example. Consider this sample (c/c++):

    int main() {

    bool *pSomeBool = NULL;

    if(pSomeBool) {

    foo();

    }

    if(!pSomeBool) {

    bar();

    }

    }

    it enters bar of course!

Page 1 of 3 (38 items) 123