17

I was eating lunch with a friend yesterday and they were complaining about null in C#. He stated that null was illogical. I decided to test his claims, so I tested some simple logical propositions:

Console.WriteLine(null == null); //True
//Console.WriteLine(null == !!null); //BOOM

Console.WriteLine(10 >= null); //False
Console.WriteLine(10 <= null); //False

Console.WriteLine(!(10 >= null)); //True
Console.WriteLine(!(10 <= null)); //True

Checking equality seems straightforward and this is what I would expect. The greater/less than statements however are logical contradictions which I find really confusing! Shouldn't these throw? The negation operation throws as you would expect.

If I try to run comparisons (aside from equality) using null in Ruby or Python I get a type error along the lines of "cannot compare a number to nil". Why doesn't C# do this?

Veedrac
  • 58,273
  • 15
  • 112
  • 169
Pookchop
  • 211
  • 1
  • 4
  • 1
    No it isn't illogical it's just dangerous. It isn't illogical because all types that can hold the value are actually nullable types. That is to say pointers, references, or nullable value types. It's perfectly sound, just not ideal – Aluan Haddad Apr 03 '18 at 18:07
  • 2
    Thanks Aluan! I don't see what nullable types have to do with a logical proposition. What do you mean? – Pookchop Apr 03 '18 at 18:09
  • Is C# just doing an implicit conversion of null? – Trevor Apr 03 '18 at 18:09
  • I thought that too, but `10 + null` returning null really stumped me. – Pookchop Apr 03 '18 at 18:11
  • @Pookchop I mentioned nullable types because they explicitly include `null` in their domain. when you compare a non nullable type to `null` it gets lifted to the nullable wrapper type. The same is true with addition. It works because `int?` is valid super type, so it's at widening conversion – Aluan Haddad Apr 03 '18 at 18:11
  • Just to avoid funny implicit conversions, if you do `null + 10` do you get a different result? – Trevor Apr 03 '18 at 18:12
  • This brings me back to highschool vb.net, where booleans all had 3 possible values, true, false, and nothing. That never made any sense to me why a language would even waste the memory to allow this strange tristate boolean. – Trevor Apr 03 '18 at 18:15
  • @TrevorD yes it's still null. – Pookchop Apr 03 '18 at 18:15
  • 2
    Related, over at software engineering [Are null references really a bad thing](https://softwareengineering.stackexchange.com/questions/12777/are-null-references-really-a-bad-thing) – hatchet - done with SOverflow Apr 03 '18 at 18:17
  • 3
    This is standard behaviour, please see doc about nullable types: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/nullable-types/ They were introduced to ease interactions with SQL database (in SQL you have 3-value logic: true, false and null) – csharpfolk Apr 03 '18 at 18:17
  • 4
    Just don't tell your friend about how JavaScript has `undefined` as well as `null`, or how `NaN` is not equal to itself. – krillgar Apr 03 '18 at 18:18
  • Yeah javascript makes a weird result. In order it gets: true, true, false, 10. Also, null == undefined but doesn't === undefined. So even worse – Trevor Apr 03 '18 at 18:19
  • @AluanHaddad I'm not sure. You're saying that 10 is turned into `int?` and that somehow leads to a return of null for addition? – Pookchop Apr 03 '18 at 18:23
  • @Pookchop that's exactly what I'm saying. That's the conversion that happens, and the reason the result is `null` - because that's the only logical sound thing it could be. – Aluan Haddad Apr 03 '18 at 18:24
  • 7
    A lot of the interpretation depends on what you think null means. There are multiple interpretations which are self-consistent. C# tends to take null as meaning "unknown". What is 10 + unkown? Well, that's also unknown, so 10 + null == null makes sense. C# also maps unknown to false. Is 10 < unknown? That's unknown which is mapped to false. – Mike Zboray Apr 03 '18 at 18:29
  • Programming is fun and so is philosophy. But they must no be mixed. Ill-defined semantics have always been pretty common in philosophy; they just won't do in IT. So, imo the whole question, all the upvotes nonwithstanding, is moot. – TaW Apr 03 '18 at 19:26
  • @mikez Strictly speaking, the [documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null) defines it as a pointer to nothing (rather than "unknown"). – EJoshuaS - Stand with Ukraine Apr 03 '18 at 19:34
  • 8
    Excellent first question by the way - welcome to the site. – EJoshuaS - Stand with Ukraine Apr 03 '18 at 19:44
  • @mikez thanks! I like the `unknown` angle, however `null==null` being true kind of counters that point :). – Pookchop Apr 03 '18 at 19:49
  • 2
    @Pookchop Eric Lippert (a former member of the C# compiler team) has discussed the issue around the meaning of null and nullable comparisons [here](https://blogs.msdn.microsoft.com/ericlippert/2009/05/14/null-is-not-empty/) and [here](https://ericlippert.com/2015/08/31/nullable-comparisons-are-weird/) as well. – Mike Zboray Apr 03 '18 at 20:19
  • @EJoshuaS Yes I'm sure it does. However the point is conceptually how does null behave not what it is represented as. It behaves like an unknown quantity (i.e. one that has a value, but we just don't know what it is) rather than say "invalid operation". The difference is easier to see in an operator like |. What is `true | null`? If null represents "invalid operation". Then the result should probably also be invalid (i.e. null). If null is "unknown" then true seems like the right result, because even if we knew the unknown quantity the result would be unchanged. This is what C# does. – Mike Zboray Apr 03 '18 at 20:49
  • BTW - can you [accept an answer](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) since the question has been around for awhile? (Doesn't necessarily have to be mine - see the linked article for details on how to select which answer to accept). – EJoshuaS - Stand with Ukraine May 10 '18 at 19:01

5 Answers5

14

Excellent question.

Try not to think of null as a specific value but rather "nothing to see here." The documentation defines null as

The null keyword is a literal that represents a null reference, one that does not refer to any object.

With that in mind, the fact that null is not an object means that the classical laws of thought don't exactly apply to it (or, at least, don't apply to it in the same way that it would apply to an actual object).

That being said, the fact that 10 >= null and 10 <= null are both false isn't, strictly speaking, a contradiction - after all, null is quite literally nothing. If you said 10 >= (some actual thing) and 10 <= (some actual thing) were both false, then clearly that would be contradictory, but you can't exactly have a contradiction in the classical sense without some actual object. Aristotle's definition of the law from Metaphysics is as follows:

It is impossible that the same thing can at the same time both belong and not belong to the same object and in the same respect, and all other specifications that might be made, let them be added to meet local objections...

So, we have a bit of a "loophole" here in a sense. At least as Aristotle formulated the Law of Non-Contradiction, it was referring specifically to objects. There are, of course, multiple interpretations of the Law of Non-Contradiction at this point.

Now, turning to the case of 10 + null. We could say that this is the same thing as null + 10 if it's easier. In a sense, then, what should happen at this point - should null "swallow up" the 10, or should we just say "10 + (nothing at all) really should just equal 10"? Truthfully, I don't have a very convincing answer here from a logical perspective beyond saying "well, that's kind of a design choice." I suspect that the language designers wanted to distinguish 10 + null from 10 + 0, but I don't have documentation to prove that. (Indeed, it would be slightly strange if they were the same; after all, 0 is an actual value that can be constructed from the natural numbers, but null is "no value whatever").

  • Thank you! So you're saying that null is a pointer to nothing and also represents nothing itself, which I think is in the docs and I agree! But I wonder why the compiler wouldn't throw in this case since the comparison can't be carried out. It's also confusing to think that you can remove the laws of logic from programming just to explain nulls. Seems backwards to me a bit. – Pookchop Apr 03 '18 at 19:28
  • @Pookchop I agree with you, actually - you could make a very strong case that the compiler should just throw an exception for trying to do something that (arguably) doesn't exactly make sense. – EJoshuaS - Stand with Ukraine Apr 03 '18 at 19:33
  • thanks again :). I just checked how Ruby does it and it throws if you try to do `10 < nil`, same with Python. – Pookchop Apr 03 '18 at 19:41
  • @Pookchop I think that this probably reflects what you'll see in *most* production systems, actually: there's actually not much consensus or consistency surrounding exactly what `null` should mean, so you end up with lots of weird edge cases. – EJoshuaS - Stand with Ukraine Apr 03 '18 at 19:43
10

I know that it's in the language specification somewhere, that's not what I'm interested in knowing. I'm interested in understanding why these logical violations seem to exist, or if my understanding of logical laws is incorrect.

Programming language should not obey philosophy implied logical rules, by any means, as programming language meant to solve concrete industry/business related problems. A lot of the languages do not obey rules even of mathematical thinking, like algebraic expressions, category theory and whatnot, so...

10 must be greater or less than null ... shouldn't it?

No, "The null keyword is a literal that represents a null reference, one that does not refer to any object", and according to C# rules, compiler scans available operators for a provided type and if one of the participants is null, outcome of expression will be null.

Look for lifted operators

Also how can 10 + null be null?

See above.

Tigran
  • 61,654
  • 8
  • 86
  • 123
  • Because any mathematical operation which includes a null value will always equal null – Gregg Apr 03 '18 at 18:40
  • Thank @Tigran! I wouldn't say Aristotle's laws are "philosophically implied" they're kind of the foundation of logic which I think has everything to do with how we solve problems unless I'm misunderstanding you? – Pookchop Apr 03 '18 at 19:09
  • @Pookchop: programming language is much more "grounded", close to concrete business problems (at least `C#`), than sometimes even science is. so I would not draw a comparison between philosophy, science and programming and try to explain any kind of differences that will eventually pop up, as we're not comparing apples to apples. – Tigran Apr 03 '18 at 19:25
  • 2
    @Tigran Logic and philosophy aren't the same, and I'm not discussing philosophy - I believe you brought that up. Logic underpins science and programming and is very relevant. – Pookchop Apr 03 '18 at 19:30
  • I think the point here is that 'null' in C# has been designed to operate with a logically meaningful concept of 'null', just a different (and in some ways perhaps more practically useful) concept from what you were expecting. The concept of 'null' here is essentially similar to that used in SQL, and has fairly strong logical underpinnings in relational theory. – Thomas W May 10 '18 at 02:06
4

It helps if you don't think of 'null' as a value - it's a concept of nothingness, or lack-of-a-value. It's not just C# - you'll run into the same thing in SQL (NULL + 'asdf' will result in NULL.)

So stuff like (NULL < 10) itself doesn't make the most logical sense - you're comparing a concept of nothingness to an actual number. Technically, no, nothingness is NOT less than 10. It's not greater than 10, either. That's why it returns back false in both cases.

The reason why you're cautioned away from NULL has nothing to do with logic. Here's a great example of why NULL's in C# will often result in bugs/errors:

private NumericalAnalysis AnalyzeNumber(int someValue)
{
    if (someCondition)
        return null;
    return new NumericalAnalysis(someValue);
}

... okay, so now we can just call AnalyzeNumber() and start working with NumericalAnalysis:

NumericalAnalysis instance = AnalyzeNumber(3);
if (instance.IsPrime)
    DoSomething();

... whoops! You just ran into a bug. You called AnalyzeNumber() but didn't check whether the return value was null before using it. After all, AnalyzeNumber() will sometimes return a null value - so when instance.IsPrime is invoked, it'll bomb. You'd literally have to do a "if (myVar == null)" check every single time that function was called - and if you forget, you just introduced a bug.

Kevin
  • 2,133
  • 1
  • 9
  • 21
  • You can do `if (instance?.IsPrime) DoSomething();` but I'm not sure if that's helpful to the discussion :-) – David Clarke May 01 '18 at 21:08
  • @David Sort of. I mean, in C#, all classes are nullable, so there's not really way to use the "type?" style for that case. It'll yell at you for trying to use it on an already nullable type. – Kevin May 01 '18 at 21:18
  • Not sure what you're getting at but my example won't work because if `instance` is null then the expression evaluates to null which makes the condition evaluate to null - so yes I expect the compiler should yell at me. – David Clarke May 01 '18 at 22:09
2

I'm not a C# language designer, so (shrug), but personally, I see it as an original type system failure, so my head can just accept it as-is.

The reason is that logically, you can't compare a deterministic value (eg. int) with an indeterministic value (nullable int), without some conversion either way. They are really 2 different types, but modern programming languages just try and smudge them together in their own way.

I also see the null as a good and useful thing, as we never have complete data and never want complete data as we only need a subset of it for the activity at hand.

Cheval
  • 403
  • 4
  • 14
1

I believe this is for historical reasons. The main motivation for adding nullable value types to C# in version 2.0 of the language was to alleviate the pain of working with nullable sql columns in ADO.NET. Therefore, addition of nullable numbers was designed to work as in sql, and so on.

Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46