The art of Generics

от автора

Универсальные шаблоны – они же generics, являются одним из мощнейших инструментов разработки.

CLR поддерживает их на уровне MSIL, и всего рантайма, что позволяет нам совершать некоторые трюки с типобезопасностью.

Если Вы знакомы с шаблонами C++, но хотели бы провернуть, если не вычисления на этапе компиляции, то по изяществу ничем не уступающие операции на C#, то эта статья поможет в этом.

Немного о паттернах

Для более удобной организации кода, а также использования ООП в разработке паттерны программирования обычно используются в связке.

[Примечание: нижеприведенные примеры не имеют отношения конкретно к «трюкам» с generics, являющимся основной целью статьи. Автору лишь хочется показать ход мыслей.]

Чего только стоит MVC. Где для обработки логики на стороне контроллера можно использовать стратегию, а лучше вместе с фабричным методом (не путать с абстрактной фабрикой).

Лучше, чем GoF, описать их не получится, поэтому двинемся далее.

Существуют такие паттерны как:

  • Multiple dispatch
  • Double dispatch (он же вид паттерна Visitor)

Суть первого заключается в расширении single dispatch – она перегрузка по типу объекта.

Например, начиная с C# 4 и его dynamic можно легко показать на примере из 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);     } } 

Как видно простой перегрузки метода не хватило бы для реализации данного паттерна.
Но перейдем теперь к Double dispatch. Перепишем пример таким образом:

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);         }          private void CollideWith(Asteroid asteroid)         {             Console.WriteLine("Spaceship collides with Asteroid");         }          private 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);     } } 

Что же, как видно можно обойтись и без dynamic.

Так к чему все это?

Ответ прост – если мы можем расширять одинарную диспетчеризацию (single dispatch), что есть перегрузка по типу объекта, переходя к случаю перегрузке по нескольким объектам (multiple dispatch), то почему не сделать такое и с generics?!

Covariance && Contravariance

Вообще, ковариантность типов в любом языке программирования кажется само собой разумеющимся. Например:

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

Однако это называется совместимость назначения (assignment compatibility).

Ковариантность проявляется именно при работе с generics.

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

Декларация IEnumerable выглядит следующим образом:

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

При отсутствии ключевого слова out и поддержке ковариантности невозможно было бы привести тип List<Asteroid> к типу IEnumerable<ICollidable>, несмотря на имплементацию данного интерфейса классом List<T>.

Наверное, Вы уже знаете, что типы помеченные как out T нельзя использовать как параметры методов, даже в виде типизированного аргумента к другому классу или интерфейсу. Например:

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

Что же, возьмем эту особенность на заметку, а пока перейдем к нашей цели – расширим возможность перегрузки по generics.

Generics compile-time checking

Рассмотрим следующий интерфейс:

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

Ничего необычного на первый взгляд. Однако, как реализовать имплементацию лишь для структур, чисел или чисел с плавающей запятой? Т.е. ввести ограничение на тип во время компиляции?

C# не предоставляет такую возможность. Можно лишь обозначить как struct, class или конкретный тип (еще есть new()) для типизированного параметра.

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

Помните пример с астероидами для multiple dispatch?

Точно такое же мы применим для имплементации 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];     } } 

Думаю, возникает вопрос – почему именно явная (explicit) имплементация интерфейса?

Все дело именно в поддержке ковариантности для любого метода интерфейса.

Так, ковариантные интерфейсы не могут содержать в методах параметры с типом T, даже, например, IList.

А так как в C# поддержка перегрузки методов по возвращаемому типу невозможна, соответственно множественная неявная имплементация интерфейса с методами, где количество аргументов больше и равно нулю не будет компилироваться.

Что же, осталось использовать данные возможности на практике.

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));         }     } } 

Попробуем изменить тип переменной arr на 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         }     } } 

Но это же достигается лишь через методы расширения?! Как быть если необходимо именно реализация интерфейса?

Немного видоизменим наш интерфейс 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;     } } 

И добавим еще одну реализацию 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>();     } } 

Проверим на практике:

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));             }         }     } } 

Таким образом, мы получили две реализации задачи проверки перегрузки по параметризированным типам – как во время компиляции, так и выполнения.

ссылка на оригинал статьи http://habrahabr.ru/post/201996/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *