📜 ⬆️ ⬇️

The art of generics

Universal templates - they are generics, are one of the most powerful development tools.

The CLR supports them at the MSIL level, and the entire runtime, which allows us to perform some type safety tricks.

If you are familiar with the C ++ templates, but would like to turn, if not the computations at the compilation stage, then the elegance is not inferior to the operation in C #, then this article will help with this.

A bit about patterns


For more convenient organization of the code, as well as the use of OOP in the development of programming patterns are usually used in conjunction.
')
[ Note: the examples below are not specifically related to "tricks" with generics, which are the main purpose of the article. The author just wants to show the train of thought .]

What is MVC worth? Where for processing the logic on the controller side, you can use a strategy , and preferably together with the factory method (not to be confused with an abstract factory ).

Better than GoF, to describe them will not work, so we move on.

There are such patterns as:

The essence of the first is to expand single dispatch - it is an overload by object type.

For example, starting with C # 4 and its dynamic, you can easily show it with 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 we go to Double dispatch. Let's rewrite the example like this:

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, apparently, you can do without dynamic.

So why all this?

The answer is simple - if we can expand single dispatch ( single dispatch ), that there is an overload by object type, moving to the case of multiple objects ( multiple dispatch ), then why not do this with generics ?!

Covariance && Contravariance


In general, the covariance of types in any programming language seems to be taken for granted. For example:

 var asteroid = new Asteroid(); ICollidable collidable = asteroid; 

However, this is called assignment compatibility.

Covariance is manifested when working with generics.

 List<Asteroid> asteroids = new List<Asteroid>(); IEnumerable<ICollidable> collidables = asteroids; 

The IEnumerable declaration is as follows:

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } 

In the absence of the out keyword and the support of covariance, it would be impossible to cast the List <Asteroid> type to the IEnumerable <ICollidable> type, despite the implementation of this interface by the List <T> class.

Probably, you already know that types marked out T cannot be used as method parameters, even as a typed argument to another class or interface. For example:

 interface ICustomInterface<out T> { T Do(T target); //compile-time error T Do(IList<T> targets); //compile-time error } 

Well, let's take this feature into account, but for now let's move on to our goal - we will expand the possibility of overloading by generics.

Generics compile-time checking


Consider the following interface:

 public interface IReader<T> { T Read(T[] arr, int index); } 

Nothing unusual at first glance. However, how to implement the implementation only for numbers or floating-point numbers? Those. to introduce a type constraint at compile time?

C # does not provide this feature. It can only be designated as a struct, class or a specific type (there is still new ()) for the typed parameter.

 public interface IReader<T> where T : class { T Read(T[] arr, int index); } 

Remember the asteroids example for multiple dispatch?

Exactly the same we will apply for the implementation of iReader.

 public class SignedIntegersReader : IReader<Int32>, IReader<Int16>, IReader<Int64> { int IReader<int>.Read(int[] arr, int index) { return arr[index]; } short IReader<short>.Read(short[] arr, int index) { return arr[index]; } long IReader<long>.Read(long[] arr, int index) { return arr[index]; } } 

I think the question arises - why the explicit implementation of the interface?

The whole thing is precisely the support of covariance for any interface method.

So, covariant interfaces cannot contain parameters with type T in methods, even IList, for example.

And since in C # support for overloading methods on the returned type is impossible, respectively, multiple implicit interface implementation with methods where the number of arguments is greater than zero will not be compiled.

Well, it remains to use these opportunities in practice.

 public static class ReaderExtensions { public static T Read<TReader, T>(this TReader reader, T[] arr, int index) where TReader : IReader<T> { 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)); } } } 

Try changing the type of the arr variable 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 the same is achieved only through extension methods? How to be if it is necessary to implement the interface?

We slightly modify our interface IReader.

IReader <T>
 public interface IReader<T> { T Read(T[] arr, int index); bool Supports<TType>(); } public class SignedIntegersReader : IReader<Int32>, IReader<Int16>, IReader<Int64> { int IReader<int>.Read(int[] arr, int index) { return arr[index]; } short IReader<short>.Read(short[] arr, int index) { return arr[index]; } long IReader<long>.Read(long[] arr, int index) { return arr[index]; } public bool Supports<TType>() { return this as IReader<TType> != null; } } 


And add another implementation of IReader - DefaultReader.

 public class DefaultReader<T> : IReader<T> { private IReader<T> _reader = new SignedIntegersReader() as IReader<T>; public T Read(T[] arr, int index) { if (_reader != null) { return _reader.Read(arr, index); } return default(T); } public bool Supports<TType>() { return _reader.Supports<TType>(); } } 

Check in practice:

 class Program { static void Main(string[] args) { var reader = new DefaultReader<int>(); var arr = new int[] { 128, 256 }; if (reader.Supports<int>()) { for (int i = 0; i < arr.Length; i++) { Console.WriteLine("Reader result: {0}", reader.Read(arr, i)); } } } } 

Thus, we have obtained two implementations of the overload check problem for parameterized types, both at compile time and execution.

Source: https://habr.com/ru/post/201996/


All Articles