C
C#8mo ago
joren

✅ Covariance & Contravariance

public interface IRepository<T>
{
T Get();
void Add(T item);
}

public class Animal
{
public string Name { get; set; }
}

public class Dog : Animal
{
public string Breed { get; set; }
}

public class AnimalRepository : IRepository<Animal>
{
public Animal Get()
{
Console.WriteLine("Getting an animal from the repository.");
return new Animal { Name = "Fido" };
}

public void Add(Animal item)
{
Console.WriteLine("Adding an animal to the repository.");
}
}
public class DogRepository : IRepository<Dog>
{
public Dog Get()
{
Console.WriteLine("Getting a dog from the repository.");
return new Dog { Name = "Buddy", Breed = "Golden Retriever" };
}

public void Add(Dog item)
{
Console.WriteLine("Adding a dog to the repository.");
}
}

internal class Program
{
public static void UseAnimalRepository(IRepository<Animal> repository)
{
var animal = repository.Get();
Console.WriteLine("Used repository to get an animal: " + animal.Name);
repository.Add(new Animal { Name = "Spot" });
Console.WriteLine("Used repository to add an animal.");
}

public static void Main()
{
AnimalRepository animalRepository = new AnimalRepository();
DogRepository dogRepository = new DogRepository();

Console.WriteLine("Using Animal Repository:");
UseAnimalRepository(animalRepository);

Console.WriteLine("\nUsing Dog Repository:");
UseAnimalRepository(dogRepository); // This line won't work without contravariance
}
}
public interface IRepository<T>
{
T Get();
void Add(T item);
}

public class Animal
{
public string Name { get; set; }
}

public class Dog : Animal
{
public string Breed { get; set; }
}

public class AnimalRepository : IRepository<Animal>
{
public Animal Get()
{
Console.WriteLine("Getting an animal from the repository.");
return new Animal { Name = "Fido" };
}

public void Add(Animal item)
{
Console.WriteLine("Adding an animal to the repository.");
}
}
public class DogRepository : IRepository<Dog>
{
public Dog Get()
{
Console.WriteLine("Getting a dog from the repository.");
return new Dog { Name = "Buddy", Breed = "Golden Retriever" };
}

public void Add(Dog item)
{
Console.WriteLine("Adding a dog to the repository.");
}
}

internal class Program
{
public static void UseAnimalRepository(IRepository<Animal> repository)
{
var animal = repository.Get();
Console.WriteLine("Used repository to get an animal: " + animal.Name);
repository.Add(new Animal { Name = "Spot" });
Console.WriteLine("Used repository to add an animal.");
}

public static void Main()
{
AnimalRepository animalRepository = new AnimalRepository();
DogRepository dogRepository = new DogRepository();

Console.WriteLine("Using Animal Repository:");
UseAnimalRepository(animalRepository);

Console.WriteLine("\nUsing Dog Repository:");
UseAnimalRepository(dogRepository); // This line won't work without contravariance
}
}
So i wrote up this example and I stumbled upon the contravariance lacking here.
117 Replies
joren
joren8mo ago
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:
public static void UseAnimalRepository<T>(IRepository<T> repository)
public static void UseAnimalRepository<T>(IRepository<T> repository)
That'd be my solution in C++ for this poblem. Which would basically just allow any IRepository<>, and handle them accordingly.
Pobiega
Pobiega8mo ago
public static void UseAnimalRepository<T>(IRepository<T> repository) where T : Animal, new()
public static void UseAnimalRepository<T>(IRepository<T> repository) where T : Animal, new()
doing that works fine
joren
joren8mo ago
ah I already got issue w the new where T : Animal is like std::is_base_of?
Pobiega
Pobiega8mo ago
¯\_(ツ)_/¯ I don't know cpp
joren
joren8mo ago
does it say: it has to be animal or a derived class
Pobiega
Pobiega8mo ago
yeah
joren
joren8mo ago
or is it strictly Animal
Pobiega
Pobiega8mo ago
animal or derived
joren
joren8mo ago
kk, noted okay, so this methods avoids contravariance correct? so what's the purpose for it, I find this 100x better
Pobiega
Pobiega8mo ago
yeah you dont need any in or out on your interface now
joren
joren8mo ago
So why is this a thing to begin with, in/out, are there situations where they are the only viable options?
Pobiega
Pobiega8mo ago
I rarely write library code, where this is most useful so I truthfully can't answer that
joren
joren8mo ago
I see, thats fine. What about the where T : Animal, new() the new() part. Why do we need to add it as a constraint?
Pobiega
Pobiega8mo ago
because repository.Add(new T { Name = "Spot" }); 🙂
joren
joren8mo ago
yes I got that far haha, but why does it not work without it
Pobiega
Pobiega8mo ago
we can't call new T unless we have a new() constraint
joren
joren8mo ago
ye why, that makes no sense
Pobiega
Pobiega8mo ago
uhm it makes a lot of sense we NEED a parameterless constructor to use one
joren
joren8mo ago
because we use {} and not the ctor
Pobiega
Pobiega8mo ago
yes no
joren
joren8mo ago
bruh
Pobiega
Pobiega8mo ago
{} uses the parameterless constructor
joren
joren8mo ago
I see so if i were to have a ctor in the animal class and use () in the new call
MODiX
MODiX8mo ago
Pobiega
REPL Result: Success
var test = new Test
{
A = "hello"
};

public class Test
{
public Test()
{
Console.WriteLine("MEEP MEEP");
}
public string A { get; set; }
}
var test = new Test
{
A = "hello"
};

public class Test
{
public Test()
{
Console.WriteLine("MEEP MEEP");
}
public string A { get; set; }
}
Console Output
MEEP MEEP
MEEP MEEP
Compile: 686.533ms | Execution: 53.066ms | React with ❌ to remove this embed.
joren
joren8mo ago
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
Pobiega
Pobiega8mo ago
a class must always be constructed
joren
joren8mo ago
if we call new T {...} it's granted it takes a param less ctor we dont pass anything so one thats defined or default ctor
Pobiega
Pobiega8mo ago
you can't actually use parameterized constructors with generic arguments
joren
joren8mo ago
what? really
Pobiega
Pobiega8mo ago
yeah. there is no new T("hello")
joren
joren8mo ago
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
Pobiega
Pobiega8mo ago
because what guarantees that a T has that constructor?
joren
joren8mo ago
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#
Pobiega
Pobiega8mo ago
you can sort of do it with static abstracts
joren
joren8mo ago
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
Pobiega
Pobiega8mo ago
¯\_(ツ)_/¯
joren
joren8mo ago
oh well good to know, cant use ctor's when constructing objects of generic T
Pobiega
Pobiega8mo ago
generics are not C++ templates
joren
joren8mo ago
should I avoid contravariance in code that isnt libs like do you avoid it, like the way you showed me with the constraints
Pobiega
Pobiega8mo ago
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
joren
joren8mo ago
Makes sense, interesting. One more thing, what about
static T Add<T>(T a, T b)
{
dynamic dynamicA = a;
dynamic dynamicB = b;

return dynamicA + dynamicB;
}
static T Add<T>(T a, T b)
{
dynamic dynamicA = a;
dynamic dynamicB = b;

return dynamicA + dynamicB;
}
adding dynamic, solved the issue of it not being able to use operator+ on a and b now I prefer never using dynamic unless
Pobiega
Pobiega8mo ago
you dont need to use dynamic here you can use a static abstract on an interface and constrain T to be that interface
static T Add<T>(T a, T b) where T : INumber<T>
{
return a + b;
}
static T Add<T>(T a, T b) where T : INumber<T>
{
return a + b;
}
for example
joren
joren8mo ago
I mean, the constraint itself makes sense, though limits you to numeric values which is w/e, but why does this fix the issue
Pobiega
Pobiega8mo ago
well yeah, but you can make your own version of INumber<T> that specifies that this type must have a + operator for yourself
joren
joren8mo ago
if T is int, how cant it do operator+
Pobiega
Pobiega8mo ago
because you didnt specify that T is int I dont understand why this is so hard for you to grasp
joren
joren8mo ago
when you call it you pass T or its deduced
Pobiega
Pobiega8mo ago
thats NOT HOW GENERICS WORK
joren
joren8mo ago
-_- clearly
Pobiega
Pobiega8mo ago
its not a cpp template
joren
joren8mo ago
so what is it exactly
Pobiega
Pobiega8mo ago
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
joren
joren8mo ago
Okay, so if I understand correctly it does "generate" a method based on the types for each unique call?
Pobiega
Pobiega8mo ago
var t = typeof(Test<>).MakeGenericType(typeof(string));

public class Test<T>
{
private T Value { get; set; }
}
var t = typeof(Test<>).MakeGenericType(typeof(string));

public class Test<T>
{
private T Value { get; set; }
}
yeah
joren
joren8mo ago
but due to it also allowing runtime generics it cannot give a guarentee?
Pobiega
Pobiega8mo ago
it can give guarantees, thats what constraints are for
joren
joren8mo ago
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 it
Pobiega
Pobiega8mo ago
you'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
T Add<T>(T a, T b) => a + b;
T Add<T>(T a, T b) => a + b;
is not valid on its own
joren
joren8mo ago
T Add<T>(T a, T b); is?
Pobiega
Pobiega8mo ago
no err what that method has no body but yeah, the signature is fine
joren
joren8mo ago
yeah, so what do you mean with it doesnt need an implementation?
Pobiega
Pobiega8mo ago
it just says "I will return a T, and i take two Ts in"
joren
joren8mo ago
yes, I understand that - no different than CPP in that regard
Pobiega
Pobiega8mo ago
eh, I shouldnt have said implementation there, I meant usage
joren
joren8mo ago
okay, so ure saying that generics need to be valid on their own without any call
Pobiega
Pobiega8mo ago
yes
joren
joren8mo ago
if they are not they are no OK to begin with so a Add<int> wont change anything
Pobiega
Pobiega8mo ago
if you want T to use + with another T, you must constrain T to have a + operator for T yes exactly
joren
joren8mo ago
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
Pobiega
Pobiega8mo ago
T Add<T>(T a, T b) where T : IAdditionOperators<T, T, T> => a + b;
T Add<T>(T a, T b) where T : IAdditionOperators<T, T, T> => a + b;
is valid because that interface guarantees a + operator between T and T that returns a T
joren
joren8mo ago
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.
Pobiega
Pobiega8mo ago
yup
joren
joren8mo ago
its hard to grasp when your fundamentals are wired differently
Pobiega
Pobiega8mo ago
yeah, when learning a new language the hardest part is to "unlearn" the stuff you already know
joren
joren8mo ago
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
Pobiega
Pobiega8mo ago
its a bit like a rust trait
joren
joren8mo ago
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:
public static void UseAnimalRepository(IRepository<Animal> repository)
{
var animal = repository.Get();
Console.WriteLine("Used repository to get an animal: " + animal.Name);
repository.Add(Animal T { Name = "Spot" });
Console.WriteLine("Used repository to add an animal.");
}
public static void UseAnimalRepository(IRepository<Animal> repository)
{
var animal = repository.Get();
Console.WriteLine("Used repository to get an animal: " + animal.Name);
repository.Add(Animal T { Name = "Spot" });
Console.WriteLine("Used repository to add an animal.");
}
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?
Pobiega
Pobiega8mo ago
well, you can't actually solve it given the current shape of your interface
Pobiega
Pobiega8mo ago
No description
Pobiega
Pobiega8mo ago
No description
joren
joren8mo ago
so I should split the Get and the Add up?
Pobiega
Pobiega8mo ago
yep, that'd work causes some issues with your UseAnimalRepositorymethod thou since it needs both Get and Add so if we split it, how do we specify a class that has both?
joren
joren8mo ago
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.
Pobiega
Pobiega8mo ago
and we're back at generics
joren
joren8mo ago
public class AnimalRepository : IRepository<Animal>, IContravariantRepository<Animal> I thought this wasnt possible multiple inheritance but I was proven wrong
Pobiega
Pobiega8mo ago
for interfaces it works
joren
joren8mo ago
ah right
Pobiega
Pobiega8mo ago
you cant have multiple base classes
joren
joren8mo ago
makes sense
Pobiega
Pobiega8mo ago
but your old implementation used both Get and Add in the same method and that wont work with the split
joren
joren8mo ago
yep... so what would be a solution that would work
Pobiega
Pobiega8mo ago
generics when
joren
joren8mo ago
bruh oh well
Pobiega
Pobiega8mo ago
yeah guess why they are so common
joren
joren8mo ago
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 instantly
Pobiega
Pobiega8mo ago
so, where its useful is when dealing with base/derived classes as the base lemme give an example
IProducer<Dog> dogProducer = null!;
Animal a = dogProducer.Produce();

interface IProducer<out T>
{
T Produce();
}

class Animal { }

class Dog : Animal{}
IProducer<Dog> dogProducer = null!;
Animal a = dogProducer.Produce();

interface IProducer<out T>
{
T Produce();
}

class Animal { }

class Dog : Animal{}
joren
joren8mo ago
so this should work, I am fairly sure since Dog inherits from Animal is this Covariance?
Pobiega
Pobiega8mo ago
actually, shitty example allow me to expand
IProducer<Dog> dogProducer = null!;
Animal a = dogProducer.Produce();
Dog d = dogProducer.Produce();

IProducer<Animal> animalProducer = dogProducer; // This is only valid because of `out T`

interface IProducer<out T>
{
T Produce();
}
IProducer<Dog> dogProducer = null!;
Animal a = dogProducer.Produce();
Dog d = dogProducer.Produce();

IProducer<Animal> animalProducer = dogProducer; // This is only valid because of `out T`

interface IProducer<out T>
{
T Produce();
}
hime
hime8mo ago
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.
hime
hime8mo ago
This lists a bunch of where they're useful
joren
joren8mo ago
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 case
Pobiega
Pobiega8mo ago
mhm
joren
joren8mo ago
Because the T inside the IProducer<T> is related, covariently in right?
Pobiega
Pobiega8mo ago
because a dog is an Animal, so it makes sense.
joren
joren8mo ago
yes would it work if they are swapped, I assume not? So we try I<Dog> obj = I<Animal>
Pobiega
Pobiega8mo ago
yeah no
joren
joren8mo ago
aha
Pobiega
Pobiega8mo ago
because all dogs are animals, but not all animals are dogs
joren
joren8mo ago
exactly, thats a violation of the object instition principle (or smth like it)
Pobiega
Pobiega8mo ago
substitution 😄
joren
joren8mo ago
I was close, ill take that as a win
Pobiega
Pobiega8mo ago
fair enough
joren
joren8mo ago
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) ?
Pobiega
Pobiega8mo ago
IProducer<Dog> dogProducer = null!;
IProducer<Animal> animalProducer = dogProducer; // only valid because of `out T` - Covariance

IConsumer<Animal> animalConsumer = null!;
IConsumer<Dog> dogConsumer = animalConsumer; // only valid because of `in T` - contravariance
IProducer<Dog> dogProducer = null!;
IProducer<Animal> animalProducer = dogProducer; // only valid because of `out T` - Covariance

IConsumer<Animal> animalConsumer = null!;
IConsumer<Dog> dogConsumer = animalConsumer; // only valid because of `in T` - contravariance
I don't follow :p
hime
hime8mo ago
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.
joren
joren8mo ago
public class A { }
public class B : A { }

public delegate void Lol<in T>(T obj);

public static void SomeMethod(A obj)
{
// ...
}
static void Main(string[] args)
{

Lol<A> a = new Lol<B>(SomeMethod);

Lol<B> b = new Lol<A>(SomeMethod);
}
public class A { }
public class B : A { }

public delegate void Lol<in T>(T obj);

public static void SomeMethod(A obj)
{
// ...
}
static void Main(string[] args)
{

Lol<A> a = new Lol<B>(SomeMethod);

Lol<B> b = new Lol<A>(SomeMethod);
}
Whines about B not being implicitly convertible to A.