четверг, 20 января 2011 г.

C#/.NET маленькие чудеса: ограничение обобщений при помощи условия where

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

К сожалению, когда вышел .NET 1.0, там не было эквивалента шаблонам. Однако с .NET 2.0, мы окончательно получили обобщения, которые позволили нам еще раз взмахнуть крыльями и программировать более обобщенно в мире .NET.

Однако, обобщения C# иногда введут себя совсем иначе, чем ихние двоюродные братья C++ шаблоны. Существует одно удобное положение, которое поможет обойти эти воды и сделает ваши  обобщения более мощными.

Проблема - C# допускает наименьший общий знаменатель

В C++, вы можете создать шаблон и делать практически все с параметром шаблона, конечно, если это допускается синтаксически, и C++ не будет проверять корректен ли вызванный метод/поле/операция до тех пор, пока вы не объявите реализацию типа. Давайте-ка я вам это продемонстрирую:

// компилируется нормально, C++ не делает 
// никаких предположений о типе T
template <typename T>
class ReverseComparer
{
public:
     int Compare(const T& lhs, const T& rhs)
     {
         return rhs.CompareTo(lhs);
     }
};

Заметьте, что мы спокойно вызываем метод CompareTo() для шаблонного типа T. Потому, как мы на данный момент не знаем, что из себя представляет тип T, и C++ не делает ни каких предположений, следовательно и не возникает ошибок.

C++ стремится не проверять используемый тип шаблона до тех пор, пока метод действительно не будет вызван для конкретного типа, что очень отличается от поведения C#:

// это НЕ скомпилируется! 
// C# допускает наименьший общий знаменатель
public class ReverseComparer<T>
{
     public int Compare(T lhs, T rhs)
     {
         return lhs.CompareTo(rhs);
     }
}

Так почему же C# выдает ошибку компиляции когда, мы еще не знаем, что за тип имеет T? Это происходит потому, что C# идет по-другому пути создания обобщений, в отличии от C++. Пока вы не укажите обратное, T трактуется как нечто похожее на object (заметьте я не сказал как object).

Это обозначает, что разные операция, поля, свойства, методы, которые вы хотите использовать с типом T, должны быть доступны в наименьшем общем знаменателе object.

Сейчас, чем шире объект является, тем более абстрактным (более общим) он должен быть. Так как же мы позволим нашему обобщенному типу заменителю, делать нечто большее, чем может делать object?

Решение: ограничить тип используя условие where

Так как же нам обойти это в C#? Ответ заключается в том, что бы ограничить обобщенный тип при помощи условия where. В основном, условие where позволяет вам определить дополнительные ограничения о том какой тип, фактически, должен поддерживаться обобщенным типом заменителем.

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

Ограничение обобщенного типа интерфейсом или суперклассом

Одно из удобных возможностей where ограничений - указывать какой интерфейс обобщенный тип должен реализовать или какой класс обобщенный тип должен наследовать. Например, вы не могли вызвать метод CompareTo() в нашем первом C# обобщении, но если ограничить тип T интерфейсом IComparable<T>, то вы сможете:

public class ReverseComparer<T> 
    where T : IComparable<T>
{
    public int Compare(T lhs, T rhs)
    {
        return lhs.CompareTo(rhs);
    }
}

Теперь, когда мы ограничили T реализацией IComparable<T>, это означает, что наши переменные обобщенного типа могут вызывать любые члены IComparable<T>. Теперь можно легально вызвать CompareTo().

Если вы ограничиваете ваш тип, то вы также получаете ошибки компиляции мгновенно, если используете тип, который не встречается в ограничении. Это гораздо чище, чем когда вы получите синтаксические ошибки в C++ при использовании шаблонов в коде, если используемый тип не поддерживается C++ шаблоном.

Ограничение обобщенных типов  только ссылочными типами

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

Хорошо, мы можете это исправить ограничением class в where условии. Объявляя то, что обобщенный тип, должен быть class, мы говорим, что он является ссылочным типом и может принимать значение null для экземпляров этого типа:

public static class ObjectExtensions
{
    public static TOut Maybe<TIn, TOut>(
        this TIn value, Func<TIn, TOut> accessor)
        where TOut : class
        where TIn : class
    {
        return (value != null) 
                   ? accessor(value)  
                   : null;
    }
}
>

В примере выше, мы хотим иметь возможность получить доступ к свойству ссылки, и если ссылка является null, то работать со ссылкой на null дальше. Для того, что бы это сделать оба типа, входной тип и выходной тип, должны быть ссылочными типами (да, nullable типы также по логике считаются применимыми, но мы имеем возможности здесь применить прямые ограничения для них).

Ограничение обобщенных типов только типами значений

Как обобщенный тип может быть ссылочным типом, также само он может быть и типом значений. Что бы это сделать используйте ограничение struct, которое говорит, что обобщенный тип должен быть типом значений (примитивный, структура, перечисление и т.п.).

Рассмотрим следующий метод, который будет конвертировать, что угодно реализующее IConvertible (int, double, string и т. п.) в тип значение, который вы укажете, или null, если экземпляр является null:

public static T? ConvertToNullable<T>(
    IConvertible value)
    where T : struct
{
    T? result = null;
  
    if (value != null)
    {
        result = (T)Convert.ChangeType(
                     value, typeof(T));
    }
  
    return result;
}

Так как T был ограничен типом значений, мы можем использовать T? (System.Nullable<T>), где мы не могли этого делать, если T был ссылочным типом.

Ограничение обобщенных типов требованием к наличию конструктора по умолчанию

Вы также можете ограничить тип требованием к наличию конструктора по умолчанию. Так как C# по умолчанию не знает какой конструктор имеет или не имеет обобщенный тип заместитель, то он не может позволить конструктор. Это говорит о том, что если обобщенный тип будет ограничен new(), то это будет обозначать, что тип реализующий обобщенный тип должен иметь конструктор по умолчанию (без параметров).

Предположим, что у вас есть обобщенный класс адаптер, который получив некоторые отображения, будет адаптировать некоторый предмет из типа TFrom в тип TTo. Так как он должен создавать новые экземпляры типа TTo в процессе, то мы должны указать, что TTo обязан иметь конструктор по умолчанию:

// Полученный набор Action 
// отображений будет отображен из TFrom в TTo
public class Adapter<TFrom, TTo> : 
    IEnumerable<Action<TFrom, TTo>>
    where TTo : class, new()
{

    public List<Action<TFrom, TTo>> Translations 
    { 
        get; 
        private set; 
    }
   
    public Adapter()
    {
        Translations = new List<
                           Action<TFrom, TTo>>();
    }
  
    public void Add(
        Action<TFrom, TTo> translation)
    {
        Translations.Add(translation);
    }


    void Add(
        Predicate<TFrom> conditional, 
        Action<TFrom, TTo> translation)
    {
        Translations.Add((from, to) =>
            {
                if (conditional(from))
                {
                    translation(from, to);
                }
            });
    }
  
    public TTo Adapt(TFrom sourceObject)
    {
        var resultObject = new TTo();
  
        Translations.ForEach(
            t => t(sourceObject, resultObject));
        return resultObject;
    }

    public IEnumerator<
        Action<TFrom, TTo>> GetEnumerator()
    {
        return Translations.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

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

Выводы

Условие where - это превосходная вещь, которая дает .NET обобщениям больше мощи для выполнения заданий, которые требуют большего поведения, чем базовое (речь идет об object).

  • Нельзя определить обобщенный тип как enum. 
  • Нельзя определить обобщенный тип, что бы он имел определенный метод, без наследования базового класса или интерфейса - имеется ввиду, что вы не можете сказать, что обобщение имеет метод Start().
  • Нельзя определить, что обобщенный тип позволяет использование арифметических операций.
  • Нельзя определить, что обобщенный тип может иметь любой конструктор, а не только конструктор по умолчанию.

Следующие вещи, которые вы не можете указать, при помощи ограничений на данный момент:

В дополнение, вы не можете перегружать определение шаблона, разными ограничениями. Например, вы можете определить Adapter where T : struct и Adapter where T : class. 

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

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

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

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