CLR поддерживает их на уровне MSIL, и всего рантайма, что позволяет нам совершать некоторые трюки с типобезопасностью.
Если Вы знакомы с шаблонами C++, но хотели бы провернуть, если не вычисления на этапе компиляции, то по изяществу ничем не уступающие операции на C#, то эта статья поможет в этом.
▌Немного о паттернах
Для более удобной организации кода, а также использования ООП в разработке паттерны программирования обычно используются в связке.
[Примечание: нижеприведенные примеры не имеют отношения конкретно к «трюкам» с generics, являющимся основной целью статьи. Автору лишь хочется показать ход мыслей.]
Чего только стоит MVC. Где для обработки логики на стороне контроллера можно использовать стратегию, а лучше вместе с фабричным методом (не путать с абстрактной фабрикой).
Лучше, чем GoF, описать их не получится, поэтому двинемся далее.
Существуют такие паттерны как:
- Multiple dispatch
- Double dispatch (он же вид паттерна Visitor)
Суть первого заключается в расширении single dispatch – она перегрузка по типу объекта.
Например, начиная с C# 4 и его dynamic можно легко показать на примере из wikipedia.
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. Перепишем пример таким образом:
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.
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/
Добавить комментарий