C
C#14mo ago
joren

❔ Covariance and Contravariance C#

So I stumbled on this topic and I dont seem to grasp it properly, though I saw the following example somewhere:
class Base
{
void func() { ... }
}
class Derived : Base
{
void fuc() { ... }
}

Base a = new Base(); // obv fine
Base b = new Derived(); // obv fine
class Base
{
void func() { ... }
}
class Derived : Base
{
void fuc() { ... }
}

Base a = new Base(); // obv fine
Base b = new Derived(); // obv fine
Both now can call func(), but b cannot call fuc() too as its treated as a Base (reference) type. Now in C++, you'd just cast b to Derived and it'd work and be perfectly valid code. The sources I looked at, mentioned that this is why covariance/contravariance exists?
75 Replies
Thinker
Thinker14mo ago
Co/contravariance (in C#) is specifically in relation to interfaces and delegates
joren
jorenOP14mo ago
to solve the problem above a cast would be perfectly fine?
Thinker
Thinker14mo ago
yeah although Derived above doesn't actually derive from Base
joren
jorenOP14mo ago
Okay, could you write me an example where co/contra variance comes into play
Thinker
Thinker14mo ago
wait no nevermind, you can call Func on both Base and Derived, but you can only call Fuc on Derived
joren
jorenOP14mo ago
and solves a problem that you'd have with interfaces/delegates yes func you can, fuc neither in this case as b is treated as a base type, not derived despite it being a Derived type under the hood until its casted it wouldnt work at least that how it is in C++
Thinker
Thinker14mo ago
C# and C++ are very different
joren
jorenOP14mo ago
Base a = new Base(); // obv fine
Base b = new Derived(); // obv fine

a.func(); // fine
b.func(); // fine
a.fuc(); // will never work, type is Base
b.fuc(); // could work, but not while b is treated as Base type
((Derived)b).fuc(); // works, might be wrong syntax
Base a = new Base(); // obv fine
Base b = new Derived(); // obv fine

a.func(); // fine
b.func(); // fine
a.fuc(); // will never work, type is Base
b.fuc(); // could work, but not while b is treated as Base type
((Derived)b).fuc(); // works, might be wrong syntax
I am well aware that they are different, otherwise I wouldnt be asking trivial questions ;)
Thinker
Thinker14mo ago
well b is of type Derived, so calling Fuc on it will always work wait no I can't read yes that code is correct
joren
jorenOP14mo ago
nah Severity Code Description Project File Line Suppression State Error CS1061 'Base' does not contain a definition for 'fuc' and no accessible extension method 'fuc' accepting a first argument of type 'Base' could be found (are you missing a using directive or an assembly reference?) 1.1.2.8 - Extension Methods D:\Documents\coding\learncsharp\1.1.2.8 - Extension Methods\Program.cs 51 Active ure incorrect
Thinker
Thinker14mo ago
yeah it's a compile error
joren
jorenOP14mo ago
b is treated as a Base type, despite it being a Derived type Obviously, as I said: b.fuc(); // could work, but not while b is treated as Base type wont work, its illegal and it doesnt make sense
Thinker
Thinker14mo ago
it's not "despite it being a derived type", the variable is Base yeah
joren
jorenOP14mo ago
its not a variable, its an object and its underlying type is Derived but its treated as a base type
Thinker
Thinker14mo ago
b is a variable of type Base
joren
jorenOP14mo ago
with all due respect I dont think you are able to help me
Thinker
Thinker14mo ago
Therefore you can only call methods available on Base from b, unless you cast it
joren
jorenOP14mo ago
I said that at the start
Thinker
Thinker14mo ago
So what is your actual question?
joren
jorenOP14mo ago
I am already aware of this, I am wondering where covariance/contravariance gets into play what problem it solved, by showcasing it
Thinker
Thinker14mo ago
Okay so you know about generics right
joren
jorenOP14mo ago
yes
Thinker
Thinker14mo ago
Variance allows this to work
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
joren
jorenOP14mo ago
yes because every type originates from object, anything fits in that type.
Thinker
Thinker14mo ago
well yeah but that's not the entire story You could have an interface like this
interface IFoo<T>
{
void DoStuff(T value);
}
interface IFoo<T>
{
void DoStuff(T value);
}
and you could use it like
interface Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<A> a = new Foo<A>();
interface Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<A> a = new Foo<A>();
but this wouldn't work:
IFoo<B> b = a;
IFoo<B> b = a;
What variance does is allow you to do is to treat IFoo<A> in this case as an IFoo<B>, because IFoo<T> only takes a T as a parameter, therefore it could accept any type which derives from T. Marking IFoo<T> as contravariant would allow IFoo<A> to be assigned to IFoo<B>
interface IFoo<in T>
{
// ...
}

IFoo<A> a = new Foo<T>();
IFoo<B> b = a;
interface IFoo<in T>
{
// ...
}

IFoo<A> a = new Foo<T>();
IFoo<B> b = a;
Covariance is the same, except instead of for parameter types it's if the interface only returns T aaaand I have it all backwards, what is described above is contravariance
joren
jorenOP14mo ago
interface IFoo<T>
{
void DoStuff(T value);
}
interface Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<B> b = new Foo<B>();
IFoo<A> a = b;
interface IFoo<T>
{
void DoStuff(T value);
}
interface Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<B> b = new Foo<B>();
IFoo<A> a = b;
would this work? as b derives from a, I assume it wouldnt make a difference? as T does not match regardless
Thinker
Thinker14mo ago
no, because IFoo<T> isn't contravariant, as mentioned
joren
jorenOP14mo ago
Yeah, so unless its contravariant T has to match at least implicitly so a = b or b = a wouldnt make a difference
MODiX
MODiX14mo ago
Thinker
REPL Result: Failure
interface IFoo<T>
{
void DoStuff(T value);
}

class Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<B> b = new Foo<B>();
IFoo<A> a = b;
interface IFoo<T>
{
void DoStuff(T value);
}

class Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<B> b = new Foo<B>();
IFoo<A> a = b;
Exception: CompilationErrorException
- Cannot implicitly convert type 'IFoo<B>' to 'IFoo<A>'. An explicit conversion exists (are you missing a cast?)
- Cannot implicitly convert type 'IFoo<B>' to 'IFoo<A>'. An explicit conversion exists (are you missing a cast?)
Compile: 678.252ms | Execution: 0.000ms | React with ❌ to remove this embed.
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
interface IFoo<in T>
{
void DoStuff(T value);
}

class Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<A> a = new Foo<A>();
IFoo<B> b = a;
interface IFoo<in T>
{
void DoStuff(T value);
}

class Foo<T> : IFoo<T>
{
public void DoStuff(T value) => Console.WriteLine(value);
}

class A {}
class B : A {}

IFoo<A> a = new Foo<A>();
IFoo<B> b = a;
Compile: 665.086ms | Execution: 91.048ms | React with ❌ to remove this embed.
joren
jorenOP14mo ago
okay, so essentially, if I were to have an interface that takes T for instance, and I want two instances of that interface with different types for T to be able to hold eachother it has to be contravariant
Thinker
Thinker14mo ago
yes but it only down down into the inheritance chain
joren
jorenOP14mo ago
Yeah, because inheritance allows it the other way around?
Thinker
Thinker14mo ago
i.e. (as I tried to eval before but it failed) you wouldn't be able to assign IFoo<B> to IFoo<A> even if IFoo<T> is contravariant An example of a covariant interface (which is the other way around) is IEnumerable<T>
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
class A {}
class B : A {}

IEnumerable<B> b = new List<B>();
IEnumerable<A> a = b;
class A {}
class B : A {}

IEnumerable<B> b = new List<B>();
IEnumerable<A> a = b;
Compile: 492.140ms | Execution: 32.117ms | React with ❌ to remove this embed.
joren
jorenOP14mo ago
ah ye makes sense
Thinker
Thinker14mo ago
And if you want an example of something which is contravariant in practice then you have Action<T>
joren
jorenOP14mo ago
yeah was about to ask for a use case because I honestly havent found a reason to use it I would avoid it naturally I assume as this is handled differently in C++ hell, I would never try contravariance in practice as it'd be UB or just dumb to begin with
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
class A {}
class B : A {}

Action<A> f = x => Console.WriteLine(x);
f(new B());
class A {}
class B : A {}

Action<A> f = x => Console.WriteLine(x);
f(new B());
Console Output
Submission#0+B
Submission#0+B
Compile: 652.231ms | Execution: 120.256ms | React with ❌ to remove this embed.
joren
jorenOP14mo ago
Action is like a delegate?
Thinker
Thinker14mo ago
yeah
delegate void Action<in T>(T value);
delegate void Action<in T>(T value);
In my experience, the only time you really think about variance is when it causes errors Otherwise it's just kinda convenient sometimes, especially when working with IEnumerable<T> and related interfaces
joren
jorenOP14mo ago
x => Console.WriteLine(x) looks like a lambda correct? takes x and executes the console writeline?
Thinker
Thinker14mo ago
yep
joren
jorenOP14mo ago
or well, returns somewhat?
Thinker
Thinker14mo ago
Well, Action returns void, so it's actually just a statement.
joren
jorenOP14mo ago
suppose its not a return, but I assume that'd be possible too in a C# lambda ah
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
var objects = new object[] { 1, true };
var strings = new string[] { "a", "b" };

objects.Concat(strings)
var objects = new object[] { 1, true };
var strings = new string[] { "a", "b" };

objects.Concat(strings)
Result: Concat2Iterator<object>
[
1,
true,
"a",
"b"
]
[
1,
true,
"a",
"b"
]
Compile: 584.891ms | Execution: 37.253ms | React with ❌ to remove this embed.
Thinker
Thinker14mo ago
This is a (somewhat) more practical example of when variance is actually useful, Concat takes two IEnumerable<T>s and concatenates them. If IEnumerable<T> wasn't covariant then you wouldn't be able to concatenate an IEnumerable<string> with an IEnumerable<object>.
joren
jorenOP14mo ago
makes sense, though you wouldnt have to mark it with anything? or with in, inside Concat? actually you would mark it thats the whole point I suppose
Thinker
Thinker14mo ago
No, IEnumerable<out T> is declared as covariant itself
joren
jorenOP14mo ago
as normally it wouldnt match wait does:
void func(object a) {}
func(1);
void func(object a) {}
func(1);
compile?
Thinker
Thinker14mo ago
yeah
joren
jorenOP14mo ago
its implicitly convertible?
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
void Func(object a) {}
Func(1);
void Func(object a) {}
Func(1);
Compile: 462.239ms | Execution: 37.387ms | React with ❌ to remove this embed.
Thinker
Thinker14mo ago
yeah you can implicitly convert (almost) any type to object (the only ones you can't are ref structs and pointers, but those are very special anyway)
joren
jorenOP14mo ago
i dont really understand how applying covariance to the enumerable makes it work with the Concat though
Thinker
Thinker14mo ago
Okay I'll clarify it a bit more
joren
jorenOP14mo ago
I guess it works fine since concat takes T, Y? and not T, T as parameter types so you can pass two different enumerable types that makes sense, but how does the concat happen and why does it allow it
Thinker
Thinker14mo ago
You can pass in two enumerable types where one has elements which are derived from the other
joren
jorenOP14mo ago
so one of them has to be the base of the other the result yields an enumerable of the derived type?
Thinker
Thinker14mo ago
yep
joren
jorenOP14mo ago
🤔 interesting okay, either way, so we pass the enumerables and it passes these contraints, then what. It has to concat them, but the enumerables dont match in type
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
IEnumerable<T> Concat<T>(IEnumerable<T> a, IEnumerable<T> b)
{
foreach (var x in a) yield return x;
foreach (var x in b) yield return x;
}

Concat<object>(new object[] { 1, true }, new string[] { "a", "b" })
IEnumerable<T> Concat<T>(IEnumerable<T> a, IEnumerable<T> b)
{
foreach (var x in a) yield return x;
foreach (var x in b) yield return x;
}

Concat<object>(new object[] { 1, true }, new string[] { "a", "b" })
Result: <Concat>d__1<object>
[
1,
true,
"a",
"b"
]
[
1,
true,
"a",
"b"
]
Compile: 575.889ms | Execution: 125.629ms | React with ❌ to remove this embed.
Thinker
Thinker14mo ago
this is essentially that They do match in type, because IEnumerable<string> is assignable to IEnumerable<object>
joren
jorenOP14mo ago
[
1,
true,
"a",
"b"
]
[
1,
true,
"a",
"b"
]
oh wait no its object, really ? oh boy I dont like that at all i mean its the only option but I personally wouldnt allow this, either make them convert the types or compile error this would mean the contraint in Concat is basically: "As long as it derives from object"? I mean cant you basically concat anything at that point?
Thinker
Thinker14mo ago
no not as long as it derives from object, as long as a derives from b
MODiX
MODiX14mo ago
Thinker
REPL Result: Success
class A {}
class B : A {}

new[] { new A(), new A() }.Concat(new[] { new B(), new B() })
class A {}
class B : A {}

new[] { new A(), new A() }.Concat(new[] { new B(), new B() })
Result: Concat2Iterator<A>
[
{},
{},
{},
{}
]
[
{},
{},
{},
{}
]
Compile: 504.428ms | Execution: 30.216ms | React with ❌ to remove this embed.
Thinker
Thinker14mo ago
okay the repl doesn't show them but they are two a and two b objects
joren
jorenOP14mo ago
it does, I see it in the code above
MODiX
MODiX14mo ago
joren
REPL Result: Success
class A {}
class B : A {}

new[] { new B(), new B() }.Concat(new[] { new A(), new A() })
class A {}
class B : A {}

new[] { new B(), new B() }.Concat(new[] { new A(), new A() })
Result: Concat2Iterator<A>
[
{},
{},
{},
{}
]
[
{},
{},
{},
{}
]
Compile: 580.053ms | Execution: 47.598ms | React with ❌ to remove this embed.
joren
jorenOP14mo ago
that works too it seems but
[
{},
{},
{},
{}
]
[
{},
{},
{},
{}
]
in both examples would be type B?
Thinker
Thinker14mo ago
Result: Concat2Iterator<A> It's an enumerable of As because both enumerables passed to Concat are assignable to IEnumerable<A>
joren
jorenOP14mo ago
Okay, I see - I've gotten a better grasp now. Thank you so much, I'll go do some testing and come back with any questions that ill most likely get! tyvm
Thinker
Thinker14mo ago
catok
Accord
Accord14mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server