The art of generics
Universal templates - they are also generics, are one of the most powerful development tools.
The CLR supports them at the MSIL level, and the whole runtime, which allows us to perform some type-safety tricks.
If you are familiar with C ++ templates, but would like to crank it up, if not compilation at the compilation stage, then by grace are in no way inferior to C # operations, then this article will help in this.
For more convenient organization of the code, as well as the use of OOP in development, programming patterns are usually used in conjunction.
[ Note: the examples below are not specifically related to “tricks” with generics, which is the main purpose of the article. The author just wants to show the train of thought .]
What is MVC worth. Where, for processing logic on the controller side, you can use a strategy , but rather with a factory method (not to be confused with an abstract factory ).
It’s better than GoF to describe them, so let's move on.
There are such patterns as:
The essence of the first is to expand single dispatch - it overloads by type of object.
For example, starting with C # 4 and its dynamic, you can easily show an example from wikipedia .
As you can see, a simple method overload would not be enough to implement this pattern.
But now let's move on to Double dispatch. We rewrite the example in this way:
Well, as you can see, you can do without dynamic.
So why all this?
The answer is simple - if we can expand a single dispatch ( single the dispatch ), that there is an overload on the type of object, passing to the case of multiple objects overload ( the multiple the dispatch ), then why not do that with generics ?!
In general, covariance of types in any programming language seems to be taken for granted. For instance:
However, this is called assignment compatibility.
Covariance is manifested precisely when working with generics.
The IEnumerable declaration is as follows:
In the absence of the out keyword and covariance support, it would be impossible to cast List to type IEnumerable, despite the implementation of this interface by the List class.
You probably already know that types marked as out T cannot be used as method parameters, even as a typed argument to another class or interface. For instance:
Well, let's take this feature to a note, but for now, let's move on to our goal - we will expand the possibility of overloading with generics.
Consider the following interface:
Nothing unusual at first glance. However, how to implement implementation only for numbers or floating point numbers? Those. introduce type restriction at compile time?
C # does not provide such an opportunity. You can only designate it as a struct, class, or a specific type (there is still new ()) for a typed parameter.
Remember the asteroid example for multiple dispatch?
We will use the same for implementation of IReader.
I think the question arises - why exactly the explicit implementation of the interface?
It is all about supporting covariance for any interface method.
So, covariant interfaces cannot contain parameters with type T in methods, even, for example, IList.
And since in C # support for method overloading by return type is impossible, accordingly multiple implicit implementation of the interface with methods, where the number of arguments is greater and equal to zero, will not be compiled.
Well, it remains to use these opportunities in practice.
Let's try changing the type of the variable arr to float [].
But is this achieved only through extension methods ?! What if the interface implementation is necessary?
We will slightly modify our IReader interface.
And add another implementation of IReader - DefaultReader.
Check in practice:
Thus, we got two implementations of the task of checking overloads by parameterized types - both during compilation and execution.
The CLR supports them at the MSIL level, and the whole runtime, which allows us to perform some type-safety tricks.
If you are familiar with C ++ templates, but would like to crank it up, if not compilation at the compilation stage, then by grace are in no way inferior to C # operations, then this article will help in this.
▌ A little bit about patterns
For more convenient organization of the code, as well as the use of OOP in development, programming patterns are usually used in conjunction.
[ Note: the examples below are not specifically related to “tricks” with generics, which is the main purpose of the article. The author just wants to show the train of thought .]
What is MVC worth. Where, for processing logic on the controller side, you can use a strategy , but rather with a factory method (not to be confused with an abstract factory ).
It’s better than GoF to describe them, so let's move on.
There are such patterns as:
- Multiple dispatch
- Double dispatch (it is also a type of Visitor pattern)
The essence of the first is to expand single dispatch - it overloads by type of object.
For example, starting with C # 4 and its dynamic, you can easily show an example from wikipedia .
Muliple dispatch
class Program
{
class Thing { }
class Asteroid : Thing { }
class Spaceship : Thing { }
static void CollideWithImpl(Asteroid x, Asteroid y)
{
Console.WriteLine("Asteroid collides with Asteroid");
}
static void CollideWithImpl(Asteroid x, Spaceship y)
{
Console.WriteLine("Asteroid collides with Spaceship");
}
static void CollideWithImpl(Spaceship x, Asteroid y)
{
Console.WriteLine("Spaceship collides with Asteroid");
}
static void CollideWithImpl(Spaceship x, Spaceship y)
{
Console.WriteLine("Spaceship collides with Spaceship");
}
static void CollideWith(Thing x, Thing y)
{
dynamic a = x;
dynamic b = y;
CollideWithImpl(a, b);
}
static void Main(string[] args)
{
var asteroid = new Asteroid();
var spaceship = new Spaceship();
CollideWith(asteroid, spaceship);
CollideWith(spaceship, spaceship);
}
}
As you can see, a simple method overload would not be enough to implement this pattern.
But now let's move on to Double dispatch. We rewrite the example in this way:
Double dispatch
class Program
{
interface ICollidable
{
void CollideWith(ICollidable other);
}
class Asteroid : ICollidable
{
public void CollideWith(Asteroid other)
{
Console.WriteLine("Asteroid collides with Asteroid");
}
public void CollideWith(Spaceship spaceship)
{
Console.WriteLine("Asteroid collides with Spaceship");
}
public void CollideWith(ICollidable other)
{
other.CollideWith(this);
}
}
class Spaceship : ICollidable
{
public void CollideWith(ICollidable other)
{
other.CollideWith(this);
}
public void CollideWith(Asteroid asteroid)
{
Console.WriteLine("Spaceship collides with Asteroid");
}
public void CollideWith(Spaceship spaceship)
{
Console.WriteLine("Spaceship collides with Spaceship");
}
}
static void Main(string[] args)
{
var asteroid = new Asteroid();
var spaceship = new Spaceship();
asteroid.CollideWith(spaceship);
asteroid.CollideWith(asteroid);
}
}
Well, as you can see, you can do without dynamic.
So why all this?
The answer is simple - if we can expand a single dispatch ( single the dispatch ), that there is an overload on the type of object, passing to the case of multiple objects overload ( the multiple the dispatch ), then why not do that with generics ?!
▌Covariance && Contravariance
In general, covariance of types in any programming language seems to be taken for granted. For instance:
var asteroid = new Asteroid();
ICollidable collidable = asteroid;
However, this is called assignment compatibility.
Covariance is manifested precisely when working with generics.
List asteroids = new List();
IEnumerable collidables = asteroids;
The IEnumerable declaration is as follows:
public interface IEnumerable : IEnumerable
{
IEnumerator GetEnumerator();
}
In the absence of the out keyword and covariance support, it would be impossible to cast List
You probably already know that types marked as out T cannot be used as method parameters, even as a typed argument to another class or interface. For instance:
interface ICustomInterface
{
T Do(T target); //compile-time error
T Do(IList targets); //compile-time error
}
Well, let's take this feature to a note, but for now, let's move on to our goal - we will expand the possibility of overloading with generics.
▌Generics compile-time checking
Consider the following interface:
public interface IReader
{
T Read(T[] arr, int index);
}
Nothing unusual at first glance. However, how to implement implementation only for numbers or floating point numbers? Those. introduce type restriction at compile time?
C # does not provide such an opportunity. You can only designate it as a struct, class, or a specific type (there is still new ()) for a typed parameter.
public interface IReader where T : class
{
T Read(T[] arr, int index);
}
Remember the asteroid example for multiple dispatch?
We will use the same for implementation of IReader.
public class SignedIntegersReader : IReader, IReader, IReader
{
int IReader.Read(int[] arr, int index)
{
return arr[index];
}
short IReader.Read(short[] arr, int index)
{
return arr[index];
}
long IReader.Read(long[] arr, int index)
{
return arr[index];
}
}
I think the question arises - why exactly the explicit implementation of the interface?
It is all about supporting covariance for any interface method.
So, covariant interfaces cannot contain parameters with type T in methods, even, for example, IList.
And since in C # support for method overloading by return type is impossible, accordingly multiple implicit implementation of the interface with methods, where the number of arguments is greater and equal to zero, will not be compiled.
Well, it remains to use these opportunities in practice.
public static class ReaderExtensions
{
public static T Read(this TReader reader, T[] arr, int index)
where TReader : IReader
{
return reader.Read(arr, index);
}
}
class Program
{
static void Main(string[] args)
{
var reader = new SignedIntegersReader();
var arr = new int[] {128, 256};
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine("Reader result: {0}", reader.Read(arr, i));
}
}
}
Let's try changing the type of the variable arr to float [].
class Program
{
static void Main(string[] args)
{
var reader = new SignedIntegersReader();
var arr = new float[] {128.0f, 256.0f};
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine("Reader result: {0}", reader.Read(arr, i)); //compile-time error
}
}
}
But is this achieved only through extension methods ?! What if the interface implementation is necessary?
We will slightly modify our IReader interface.
Ireader
public interface IReader
{
T Read(T[] arr, int index);
bool Supports();
}
public class SignedIntegersReader : IReader, IReader, IReader
{
int IReader.Read(int[] arr, int index)
{
return arr[index];
}
short IReader.Read(short[] arr, int index)
{
return arr[index];
}
long IReader.Read(long[] arr, int index)
{
return arr[index];
}
public bool Supports()
{
return this as IReader != null;
}
}
And add another implementation of IReader - DefaultReader.
public class DefaultReader : IReader
{
private IReader _reader = new SignedIntegersReader() as IReader;
public T Read(T[] arr, int index)
{
if (_reader != null)
{
return _reader.Read(arr, index);
}
return default(T);
}
public bool Supports()
{
return _reader.Supports();
}
}
Check in practice:
class Program
{
static void Main(string[] args)
{
var reader = new DefaultReader();
var arr = new int[] { 128, 256 };
if (reader.Supports())
{
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine("Reader result: {0}", reader.Read(arr, i));
}
}
}
}
Thus, we got two implementations of the task of checking overloads by parameterized types - both during compilation and execution.