Why is it not possible to use the is operator to discern between bool and Nullable<bool>?
Asked Answered
I

4

13

I came across this and am curious as to why is it not possible to use the is operator to discern between bool and Nullable<bool>? Example;

void Main()
{
    bool theBool = false;
    Nullable<bool> theNullableBoolThatsFalse = false;
    Nullable<bool> theNullableBoolThatsNull = null;

    void WhatIsIt(object value)
    {
        if(value is bool)
            Console.WriteLine("    It's a bool!");
        if(value is Nullable<bool>)
            Console.WriteLine("    It's a Nullable<bool>!");
        if(value is null)
            Console.WriteLine("    It's a null!");
    }

    Console.WriteLine("Considering theBool:");
    WhatIsIt(theBool);
    Console.WriteLine("Considering theNullableBoolThatsFalse:");
    WhatIsIt(theNullableBoolThatsFalse);
    Console.WriteLine("Considering theNullableBoolThatsNull:");
    WhatIsIt(theNullableBoolThatsNull);
}

Calling Main() gives;

Considering theBool:
    It's a bool!
    It's a Nullable<bool>!
Considering theNullableBoolThatsFalse:
    It's a bool!
    It's a Nullable<bool>!
Considering theNullableBoolThatsNull:
    It's a null!

I'd expect;

Considering theBool:
    It's a bool!
Considering theNullableBoolThatsFalse:
    It's a Nullable<bool>!
Considering theNullableBoolThatsNull:
    It's a null!

Why do both bool and Nullable<bool> match each other?

What have I tried;

  • I've consulted the docs for Nullable, is, switch and pattern matching.
  • I think this might be related to unboxing of the value when it's passed into the method?

I think it might be unique to Nullable because I don't run into the same problems for other generic types. For example;

void Main()
{
     bool theBool = false;
     List<bool> theListOfBool= new List<bool>();    

     void WhatIsIt(object value)
     {
         if(value is bool)
             Console.WriteLine("    It's a bool!");
         if(value is List<bool>)
             Console.WriteLine("    It's a List<bool>!");
     }

     Console.WriteLine("Considering theBool:");
     WhatIsIt(theBool);
     Console.WriteLine("Considering theListOfBool:");
     WhatIsIt(theListOfBool);
}

Gives;

Considering theBool:
    It's a bool!
Considering theListOfBool:
    It's a List<bool>

I'm not looking to solve a problem. Just interested as to why it works this way.

Answers so far suggest it's implicit and explicit conversions that cause this behaviour but I haven't been able to replicate with the following example;

class A
{
    public static implicit operator A(B value) => new A();
    public static explicit operator B(A value) => new B();
}

class B
{
    public static implicit operator A(B value) => new A();
    public static explicit operator B(A value) => new B();
}

static void Main(string[] args)
{
    var a = new A();
    var b = new B();

    void WhatIsIt(object value)
    {
        if (value is A)
            Console.WriteLine("    It's a A!");
        if (value is B)
            Console.WriteLine("    It's a B!");
    }

    Console.WriteLine("Considering a;");
    WhatIsIt(a);
    Console.WriteLine("Considering b;");
    WhatIsIt(b);
}

Gives;

Considering a;
    It's a A!
Considering b;
    It's a B!

The docs for is say:

It only considers reference conversions, boxing conversions, and unboxing conversions; it does not consider user-defined conversions or conversions defined by a type's implicit and explicit operators. The following example generates warnings because the result of the conversion is known at compile-time. Note that the is expression for conversions from int to long and double return false, since these conversions are handled by the implicit operator.

Are reference conversions, boxing conversions, and unboxing conversions something the framework decides?

Impose answered 10/1, 2019 at 12:4 Comment(3)
Sure, because false can safely be converted (via an implicit cast) to both bool and bool?. null on the other hand can only be converted to boo?, which is why value is bool returns false.Nickolai
According to Microsoft boxing/unboxing is one thing, but when it comes to int and int? you just can't use "is" operator: "Don't use the is operator to determine whether an instance is of a nullable type." Source: learn.microsoft.com/en-us/dotnet/csharp/programming-guide/…Aesop
@PiotrWojsa, that makes sense! The reference source for Nullable also points at this in the declaration of GetUnderlyingType. If you make your comment an answer I'll accept it.Impose
B
7

The reason bool and Nullable<bool> behave the same when passed to your method is because whenever you box a Nullable<T> it doesn't actually box the nullable value, instead it unwraps the value of the nullable and boxes that. If the nullable value is null then you end up with just null, rather than a boxed Nullable<T> where HasValue is false.

If you box a non-null value, it'll just box the Value of the Nullable<T>. So from the perspective of WhatIsIt, the first two calls are literally indistinguishable, because the exact same value is being passed in.

That just leaves the question of why both is checks return true, even though what's passed in, in both cases, is a boxed boolean, and not a Nullable<T>. That's answered by the C# language specs, section 7.10.10:

If T is a nullable type, the result is true if D is the underlying type of T.

In this case this is considering E is T and D is defined earlier as a computed value of E where:

If the type of E is a nullable type, D is the underlying type of that nullable type.

This means that the is operator is specifically defined as treating nullable types as being equivalent to their underlying types, regardless of how you mix and match the actual value being checked and the type you're checking with nullable values and that nullable's underlying type.

Bowing answered 10/1, 2019 at 14:50 Comment(0)
N
5

The value false can safely be converted to both bool and bool? because there´s an implicit cast-operator between them.

null on the other hand can´t be converted to bool, which is why null is bool returns false.

The is operator does not (and can´t) care about how you declared the variable - if at all. It just indicates the type of the value provided at runtime. You could have also written this:

WhatIsIt(false)

How would you expect the method to behave here? It simply tries to convert the value to both types - which it can - and thus returns true for both.

Why it does not work this way for other generics is simply because there´s no implicit conversion between the most generic types and their type-argument. Thus the following does not work:

string myString = new List<string>();
Nickolai answered 10/1, 2019 at 12:13 Comment(3)
Implicit conversions aren't considered for the is operator, according to its documentation: 3 is double returns false although the C# language defines an implicit conversion from int to double.Minx
@Minx Hmmm, interesting. Making me wonder how is is implemented.Nickolai
I've edited the question to add an example with implicit and explicit declared.Impose
D
2

Nullable<T> class has implicit and explicit operators implemented which are used in such cases out of the box, take a look at documentation

Here is the excerpt from source code:

[System.Runtime.Versioning.NonVersionable]
public static implicit operator Nullable<T>(T value) {
    return new Nullable<T>(value);
}

[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) {
    return value.Value;
}
Deplete answered 10/1, 2019 at 12:10 Comment(2)
The docs for is say; it does not consider user-defined conversions or conversions defined by a type's implicit and explicit operators. I understand now that is is saying; could I safely cast to this and not were you declared with this type.Impose
I've edited the question to add an example with implicit and explicit declared.Impose
L
0

When you declare

Nullable<bool> theNullableBoolThatsFalse = false;

theNullableBoolThatsFalse can hold 'true','false' or 'null' similarly theNullableBoolThatsNull can hold boolean values and Null. And when you assign Null it compltely becomes null can't be any other type . It does not refer to any object. Furthe Information About Nullable

Latialatices answered 10/1, 2019 at 12:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.