...

среда, 13 ноября 2013 г.

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 к типу IEnumerable, несмотря на имплементацию данного интерфейса классом List.

Наверное, Вы уже знаете, что типы помеченные как 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));
}
}
}
}




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

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий