0

I have the following code in a .Net6 console application.

internal class Program

    {

        static void Main(string[] args)

        {

            var x = new SomethingUser<Something>();

            var y = new SomethingUser<ISomething>();

 

            Console.WriteLine(x is ISomethingUser<Something>);

            Console.WriteLine(y is ISomethingUser<ISomething>);

            Console.WriteLine(x is SomethingUser<ISomething>);

            Console.WriteLine(y is SomethingUser<ISomething>);

 

            Console.Read();

        }

    }


    public class Something : ISomething {}

    public class SomethingUser<T> : ISomethingUser {}

    public interface ISomething {}
 
    public interface ISomethingUser<T> {}
}

The actual output is: True, True, False, True

Can someone explain why the is check evaluates to false for the third scenario please?

I expected the output to be: True, True, True, True

AKJ
  • 11
  • 3

4 Answers4

0

Because SomethingUser<Something> and SomethingUser<ISomething> are different types. C# has covariance for generic parameters of interfaces and delegates only, but not for classes.

From ECMA-334 C# standard 6th edition, ch. 17.2.3.1:

Variant type parameter lists can only occur on interface and delegate types.

...

If the variance annotation is out, the type parameter is said to be covariant.

Btw, you may slightly improve compatibility by declaring ISomethingUser to be covariant by adding out specification for generic parameter public interface ISomethingUser<out T> {}, then x is ISomethingUser<ISomething> will return true. So by casting to interface, still no covariance for classes.

Renat
  • 7,718
  • 2
  • 20
  • 34
  • "then x is ISomethingUser will return true" returns true when ISomethingUser is declared as "ISomethingUser" as well. It doesn't relate to "x is SomethingUser" which is asked about. – rotabor Aug 30 '23 at 11:56
  • 1
    @rotabor The point is you *can* use covariance on an interface, but not on a class, so if `SomethingUser` was instead an interface then it might work, assuming only out-positioned values. – Charlieface Aug 30 '23 at 12:29
0

Evidently x doesn't meet any condition enlisted below against SomethingUser<ISomething>:

(Type-testing operators and cast expressions - is, as, typeof and casts)

The is operator returns true when an expression result is non-null and any of the following conditions are true:

  • The run-time type of an expression result is T.
  • The run-time type of an expression result derives from type T, implements interface T, or another implicit reference conversion exists from it to T.
  • The run-time type of an expression result is a nullable value type with the underlying type T and the Nullable.HasValue is true.
  • A boxing or unboxing conversion exists from the run-time type of an expression result to type T.

The code below demonstrates the most close case:

var x = new SomethingUser<Something>();
SomethingUser<ISomething> z; // x is SomethingUser<ISomething> ?
z = x;
-->
 error CS0029: Cannot implicitly convert type 'SomethingUser<Something>' to 'SomethingUser<ISomething>'

To allow conversions such as T<A> <--> T<B>, where A inherits B, variance of T<A> to A should be explicitly defined using in or out variance modifiers. But it's allowed only when T is an interface or a delegate not a class.

The conversions between A<T> and B<T> are allowed according to inheritance relationship between A and B (cases 1 and 2).

The general approach is well explained in Wikipedia.

rotabor
  • 561
  • 2
  • 10
0

It's probably easier to explain this by way of an example showing what would happen if such a conversion was possible.

Consider the following class hierarchy:

public interface IMammal
{
}

public sealed class Dog: IMammal
{
    public void Woof() { Console.WriteLine("Woof"); }
}

public sealed class Cat: IMammal
{
    public void Meow() { Console.WriteLine("Meow");}
}

Now consider the following code:

// You can add Dogs AND Cats to a List<IMammal>:

var mammals = new List<IMammal>();

mammals.Add(new Dog()); // OK, a Dog is a Mammal.
mammals.Add(new Cat()); // OK, a Cat is a Mammal.

// Here's a list of Cats.

var cats = new List<Cat>();

List<IMammal> reallyListOfCats = cats; // Doesn't compile!

reallyListOfCats.Add(new Dog()); // And this is why! Chaos would ensue.

cats[0].Meow(); // What happens now? This is a Dog, and Dog's go "woof"!

The first part of that just demonstrates that you can put Dogs AND Cats into a List<IMammal>.

The next part shows what could happen if you allow a List<Cat> to be assigned to a List<IMammal>: If the line marked Doesn't compile! actually DID compile, it would mean that reallyListOfCats would allow you to add a Dog to it, although it's really a list of Cats.

So now cats[0] is actually a Dog so when you call cats[0].Meow() it would explode.

Note that reallyListOfCats would be an alias for cats if this code compiled, so if you modify reallyListOfCats you are also modifying cats because they both reference the selfsame object.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
0

Just a counter-example to explain why SomethingUser<Something> cannot be SomethingUser<ISomething>.

Assume the SomethingUser<T> class has a property with a setter:

public class SomethingUser<T> : ISomethingUser<T>
{
    public T Thing { set; }
}

And a new class Foo implementes the ISomething interface:

public class Foo : ISomething {}

Now if it is able to convert SomethingUser<Something> to SomethingUser<ISomething>, you will get:

var x = new SomethingUser<Something>();
if(x is SomethingUser<ISomething> xx)
    xx.Thing = new Foo(); // What?

You can see that at the beginning, the type of the Thing property of x was Something, but this false conversion allows us to set any object that implements the ISomething interface to it, which obviously should not happen.

shingo
  • 18,436
  • 5
  • 23
  • 42