❔ 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:
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
Co/contravariance (in C#) is specifically in relation to interfaces and delegates
to solve the problem above a cast would be perfectly fine?
yeah
although
Derived
above doesn't actually derive from Base
Okay, could you write me an example where co/contra variance comes into play
wait no nevermind, you can call
Func
on both Base
and Derived
, but you can only call Fuc
on Derived
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++C# and C++ are very different
I am well aware that they are different, otherwise I wouldnt be asking trivial questions ;)
b
is of type Derived
, so calling Fuc
on it will always worknah
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
yeah it's a compile error
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 senseit's not "despite it being a derived type", the variable is
Base
yeahits not a variable, its an object and its underlying type is Derived
but its treated as a base type
b
is a variable of type Base
with all due respect I dont think you are able to help me
Therefore you can only call methods available on
Base
from b
, unless you cast itI said that at the start
So what is your actual question?
I am already aware of this, I am wondering where covariance/contravariance gets into play
what problem it solved, by showcasing it
Okay so you know about generics right
yes
Variance allows this to work
yes because every type originates from
object
, anything fits in that type.well yeah but that's not the entire story
You could have an interface like this and you could use it like but this wouldn't work:
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>
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
would this work?
as
b
derives from a
, I assume it wouldnt make a difference?
as T
does not match regardlessno, because
IFoo<T>
isn't contravariant, as mentionedYeah, so unless its contravariant
T
has to match at least implicitly
so a = b
or b = a
wouldnt make a differenceThinker
REPL Result: Failure
Exception: CompilationErrorException
Compile: 678.252ms | Execution: 0.000ms | React with ❌ to remove this embed.
Thinker
REPL Result: Success
Compile: 665.086ms | Execution: 91.048ms | React with ❌ to remove this embed.
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 contravariantyes
but it only down down into the inheritance chain
Yeah, because inheritance allows it the other way around?
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>
Thinker
REPL Result: Success
Compile: 492.140ms | Execution: 32.117ms | React with ❌ to remove this embed.
ah ye
makes sense
And if you want an example of something which is contravariant in practice then you have
Action<T>
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
Thinker
REPL Result: Success
Console Output
Compile: 652.231ms | Execution: 120.256ms | React with ❌ to remove this embed.
Action
is like a delegate?yeah
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 interfacesx => Console.WriteLine(x)
looks like a lambda correct? takes x
and executes the console writeline?yep
or well, returns somewhat?
Well,
Action
returns void, so it's actually just a statement.suppose its not a return, but I assume that'd be possible too in a C# lambda
ah
Thinker
REPL Result: Success
Result: Concat2Iterator<object>
Compile: 584.891ms | Execution: 37.253ms | React with ❌ to remove this embed.
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>
.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 supposeNo,
IEnumerable<out T>
is declared as covariant itselfas normally it wouldnt match
wait
does:
compile?
yeah
its implicitly convertible?
Thinker
REPL Result: Success
Compile: 462.239ms | Execution: 37.387ms | React with ❌ to remove this embed.
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)i dont really understand how applying covariance to the enumerable makes it work with the Concat though
Okay I'll clarify it a bit more
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 itYou can pass in two enumerable types where one has elements which are derived from the other
so one of them has to be the base of the other
the result yields an enumerable of the derived type?
yep
🤔 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
Thinker
REPL Result: Success
Result: <Concat>d__1<object>
Compile: 575.889ms | Execution: 125.629ms | React with ❌ to remove this embed.
this is essentially that
They do match in type, because
IEnumerable<string>
is assignable to IEnumerable<object>
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?no not as long as it derives from object, as long as a derives from b
Thinker
REPL Result: Success
Result: Concat2Iterator<A>
Compile: 504.428ms | Execution: 30.216ms | React with ❌ to remove this embed.
okay the repl doesn't show them but they are two a and two b objects
it does, I see it in the code above
joren
REPL Result: Success
Result: Concat2Iterator<A>
Compile: 580.053ms | Execution: 47.598ms | React with ❌ to remove this embed.
that works too it seems
but
in both examples would be type
B
?Result: Concat2Iterator<A>
It's an enumerable of A
s
because both enumerables passed to Concat are assignable to IEnumerable<A>
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
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.