Lab: Variant Generic Interfaces#
Objective#
In this lab, we will dive deep into the world of variant generic interfaces in C#. Our goal is to grasp the power of the in
and out
keywords by refactoring and extending an existing codebase, thereby making our code more reusable and adaptable.
Provided Code#
Carefully review the provided code. Notice how we have defined two classes, Shape
and Circle
, representing geometric shapes. We also have an interface IPair<out T>
that allows us to define a covariant pair of items, and a Pair<T>
class implementing this interface. Similarly, we have introduced the concept of contravariance with the IComparer<in T>
interface and a ShapeAreaComparer
class that implements it.
class Shape
{
public virtual double Area { get; set; }
}
class Circle : Shape
{
public double Radius { get; set; }
public override double Area => Math.PI * Radius * Radius;
}
interface IPair<out T>
{
T First { get; }
T Second { get; }
}
class Pair<T> : IPair<T>
{
public T First { get; }
public T Second { get; }
public Pair(T first, T second)
{
First = first;
Second = second;
}
}
We will be working with these classes and interfaces to understand and experiment with the concepts of covariance and contravariance.
Instructions#
Step 1: Experience Covariance#
Firstly, we will experience the power of covariance:
Create an instance of
Pair<Circle>
.Compute the combined area of these circles using the local function
TotalArea
.Output the result to the console.
Try removing the keyword
out
fromIPair<T>
. Does the code still work?
double TotalArea(IPair<Shape> shapePair)
=> shapePair.First.Area + shapePair.Second.Area;
Step 2: Experience Contravariance#
Next, we’ll utilize contravariance:
Add an instance method with the signature
T Largest(IComparer<T> comparer)
to the classPair<T>
. The method should use thecomparer
to determine which item is the greatest and then return that.Create an instance of
Pair<Circle>
.Determine which
Circle
is the larger of the two by passing aShapeAreaComparer
to the methodLargest
.Output the radius of the larger circle to the console.
interface IComparer<in T>
{
bool IsGreaterThan(T left, T right);
}
class ShapeAreaComparer : IComparer<Shape>
{
public bool IsGreaterThan(Shape x, Shape y)
=> x.Area > y.Area;
}
Step 3: Create an Invariant Interface#
Create an invariant interface called
IContainer<T>
.Add methods:
void Add(T item)
andT Get()
.Implement this interface in a class called
Queue<T>
which contains a list ofT
.Construct and instantiate this generic class with both
Shape
andCircle
and test adding and getting items.Is it possible to use a
Queue<Circle>
where aQueue<Shape>
is expected? Why or why not?Is it possible to use a
Queue<Shape>
where aQueue<Circle>
is expected? Why or why not?
🤔 Reflection
Why can’t we use in
or out
with the IContainer<T>
interface?
Challenge#
Now, with our understanding of covariance, contravariance, and invariance:
Create a covariant interface
IReadOnlyList<out T>
which exposes a methodT Get(int index)
and a propertyint Count
.Implement this interface in the
Queue<T>
class.Instantiate a
Queue<Circle>
and try to assign it to anIReadOnlyList<Shape>
variable.
🤔 Reflection
What benefits does the IReadOnlyList<out T>
interface offer in terms of flexibility and type safety? Reflect on how covariance can make our code more adaptable.
By the end of this lab, we should have a firmer grasp on the concepts of variance in generic interfaces and appreciate the flexibility it can introduce to our codebase.
Happy coding! 🤓