✅ Covariance & Contravariance
So i wrote up this example and I stumbled upon the contravariance lacking here.
117 Replies
Now,
DogRepository
inherits as follows: DogRepository : IRepository<Dog>
, so IRepository<Dog>
.
But public static void UseAnimalRepository(IRepository<Animal> repository)
takes IRepository<Animal>
which does not match, however apparently it could.
I dont understand why this is even in a thing to begin with, the types differ therefore it shouldn't compile, if I wanted this to work I could just write smth like:
That'd be my solution in C++ for this poblem.
Which would basically just allow any IRepository<>
, and handle them accordingly.
doing that works fine
ah I already got issue w the new
where T : Animal
is like std::is_base_of
?¯\_(ツ)_/¯
I don't know cpp
does it say: it has to be animal or a derived class
yeah
or is it strictly Animal
animal or derived
kk, noted
okay, so this methods avoids contravariance correct?
so what's the purpose for it, I find this 100x better
yeah you dont need any
in
or out
on your interface nowSo why is this a thing to begin with,
in/out
, are there situations where they are the only viable options?I rarely write library code, where this is most useful
so I truthfully can't answer that
I see, thats fine. What about the
where T : Animal, new()
the new()
part. Why do we need to add it as a constraint?because
repository.Add(new T { Name = "Spot" });
🙂yes
I got that far haha, but why does it not work without it
we can't call
new T
unless we have a new()
constraintye why, that makes no sense
uhm
it makes a lot of sense
we NEED a parameterless constructor to use one
because we use
{}
and not the ctoryes
no
bruh
{}
uses the parameterless constructorI see
so if i were to have a ctor in the animal class
and use
()
in the new
callPobiega
REPL Result: Success
Console Output
Compile: 686.533ms | Execution: 53.066ms | React with ❌ to remove this embed.
yes, it still calls the ctor
its either the one you defined or a default ctor if there's none defined
is this one of those code safety things
because the way I look at it
a class must always be constructed
if we call
new T {...}
it's granted it takes a param less ctor
we dont pass anything
so one thats defined or default ctoryou can't actually use parameterized constructors with generic arguments
what? really
yeah. there is no
new T("hello")
why though
T just represent the type thats passed
shouldnt be much different than a method that has tons of overloads w different types for T
because what guarantees that a T has that constructor?
the compiler
checks for it
just give a compile time error
No overload found for ctor...same concept when u use a type that hasnt been written or call a function that doesnt exist conceptually the same thing, guess the compiler is unable to check a class's defined ctors in C#
you can sort of do it with static abstracts
eh at that point I'd not bother in my cases I guess, I am sure there is a reason for them not allowing ctors
but it can def be done different, maybe a choice they made to simplfy the language
¯\_(ツ)_/¯
oh well good to know, cant use ctor's when constructing objects of generic T
generics are not C++ templates
should I avoid contravariance in code that isnt libs
like do you avoid it, like the way you showed me with the constraints
uh, if you need it you need it
I rarely design with it in mind, and add it as needed if I run into issues
Makes sense, interesting. One more thing, what about
adding dynamic, solved the issue of it not being able to use
operator+
on a
and b
now I prefer never using dynamic unlessyou dont need to use dynamic here
you can use a static abstract on an interface and constrain T to be that interface
for example
I mean, the constraint itself makes sense, though limits you to numeric values
which is w/e, but why does this fix the issue
well yeah, but you can make your own version of
INumber<T>
that specifies that this type must have a + operator for yourselfif
T
is int
, how cant it do operator+
because you didnt specify that T is int
I dont understand why this is so hard for you to grasp
when you call it
you pass T or its deduced
thats NOT HOW GENERICS WORK
-_-
clearly
its not a cpp template
so what is it exactly
depending on your level of exactness needed, you might need to talk to the language design team, or the compiler team
but its similar in concept, if not in how its implemented
a generic method is indeed copied for each T you give it
but generic classes etc can also be made during runtime
Okay, so if I understand correctly it does "generate" a method based on the types for each unique call?
yeah
but due to it also allowing runtime generics it cannot give a guarentee?
it can give guarantees, thats what constraints are for
there's if you call
Add<int>
it cannot guarentee int
and therefore operator+
is not guarenteed
hence compile time error
unless we add constraints saying that we have a certain type, aka we manually guarentee ityou're thinking of it backwards
a generic is valid on its own
it doesnt need an implementation or usage to be deemed valid or not
is not valid on its own
T Add<T>(T a, T b);
is?no
err
what
that method has no body
but yeah, the signature is fine
yeah, so what do you mean with it doesnt need an implementation?
it just says "I will return a T, and i take two Ts in"
yes, I understand that - no different than CPP in that regard
eh, I shouldnt have said implementation there, I meant usage
okay, so ure saying that generics need to be valid on their own
without any call
yes
if they are not they are no OK to begin with
so a Add<int> wont change anything
if you want T to use + with another T, you must constrain T to have a + operator for T
yes
exactly
well thats fundemental difference between C++, as they are OK as long as their syntax is fine
the moment u try to call the template in a way thats not OK, compiler will tell u
or runtime issue
is valid
because that interface guarantees a + operator between T and T that returns a T
Okay, makes sense. Generics need to be valid on their own, and since it does not know what
T
could be it isnt valid in my initial example.yup
its hard to grasp when your fundamentals are wired differently
yeah, when learning a new language the hardest part is to "unlearn" the stuff you already know
all I know is C++, python, C and Intel ASM
none of them
really touch these subjects, except C++
and thats super different to the approach C# took
its a bit like a rust trait
that language is on my todo list
but one at a time, struggling enough w C# already I suppose
Back to my initial question though:
So lets say I wanted to solve this issue using contravariance, I'd have to make my
IRepository
to be out
?
out
-> contravariance
in
-> covariance
however, isnt C# by default covariant?well, you can't actually solve it given the current shape of your interface
so I should split the Get and the Add up?
yep, that'd work
causes some issues with your
UseAnimalRepository
method thou
since it needs both Get and Add
so if we split it, how do we specify a class that has both?Pastebin
public interface IRepository { T Get(); } p - Paste...
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
and we're back at generics
public class AnimalRepository : IRepository<Animal>, IContravariantRepository<Animal>
I thought this wasnt possible
multiple inheritance
but I was proven wrongfor interfaces it works
ah right
you cant have multiple base classes
makes sense
but your old implementation used both
Get
and Add
in the same method
and that wont work with the splityep...
so what would be a solution that would work
generics
bruh
oh well
yeah
guess why they are so common
at that point fuck contravariance and covariance, clearly not the solution
I honestly cant think of an example where I'd need contravariance/covariance applied
so I can make a little example where the solution is using
in/out
bit obnoxious, as I like creating small issues, that showcase its usefulness instantlyso, where its useful is when dealing with base/derived classes as the base
lemme give an example
so this should work, I am fairly sure
since
Dog
inherits from Animal
is this Covariance?actually, shitty example
allow me to expand
Covariance and Contravariance (C#) - C#
Learn about covariance and contravariance and how they affect assignment compatibility. See a code example that demonstrates the differences between them.
This lists a bunch of where they're useful
Yes, so here
IProducer<Dog>
is assigned to IProducer<Animal>
, which would not work in C++ but contravariance allows for it to work in this casemhm
Because the
T
inside the IProducer<T>
is related, covariently in right?because a dog is an Animal, so it makes sense.
yes
would it work if they are swapped, I assume not?
So we try
I<Dog> obj = I<Animal>
yeah no
aha
because all dogs are animals, but not all animals are dogs
exactly, thats a violation of the object instition principle (or smth like it)
substitution 😄
I was close, ill take that as a win
fair enough
okay so, that is contravarient, when the types dont exactly match but make sense
basically
and still follow the Object substitution principle
wait so wouldnt it be like;
Contravariance allows covariance inside interface generics (granted it does not violate the Object substitution principle, of course) ?
I don't follow :p
Creating Variant Generic Interfaces (C#) - C#
Learn how to create variant generic interfaces with covariant or contravariant generic type parameters.
Covariance allows interface methods to have more derived return types than that defined by the generic type parameters. Contravariance allows interface methods to have argument types that are less derived than that specified by the generic parameters.
Whines about B not being implicitly convertible to A.