« Все записи

Маленькие чудеса C#/.NET: ограничение универсальных типов с помощью условия where

Возвращаемся к "Маленьким чудесам"! Оригинал статьи

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

Когда вышла .NET 1.0, в ней, к сожалению, не было эквивалента шаблонов. В .NET 2.0 мы, наконец, получили универсальные типы (generic types), что лишний раз позволяет нам расправить наши крылья и программировать более обобщенно и в мире .NET.

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

Проблема - использование общего базового типа в C#

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

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

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

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

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

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

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

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

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

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

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

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

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

Например, вы не можете вызвать CompareTo() в нашем первом примере на C# без ограничений, но если вы ограничите Т IComparable<T>, все получится:

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

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

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

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

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

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

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

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

Ограничение универсального типа только значимыми типами

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

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

public static T? ConvertToNullable(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(), это будет означать, что типы, которые используются для реализации универсального типа, должны иметь конструктор по умолчанию (без аргументов).

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

// Given a set of Action mappings will map TFrom to TTo
public class Adapter : IEnumerable>
    where TTo : class, new()
{
    // The list of translations from TFrom to TTo
    public List> Translations { get; private set; }

    // Construct with empty translation and reverse translation sets.
    public Adapter()

    {
        // did this instead of auto-properties to allow simple use of initializers
        Translations = new List>();
    }

    // Add a translator to the collection, useful for initializer list
    public void Add(Action translation)
    {
        Translations.Add(translation);
    }

    // Add a translator that first checks a predicate to determine if the translation
    // should be performed, then translates if the predicate returns true
    public void Add(Predicate conditional, Action translation)
    {
        Translations.Add((from, to) =>
                             {
                                 if (conditional(from))
                                 {
                                     translation(from, to);
                                 }
                             });
    }

    // Translates an object forward from TFrom object to TTo object.
    public TTo Adapt(TFrom sourceObject)
    {
        var resultObject = new TTo();

        // Process each translation
        Translations.ForEach(t => t(sourceObject, resultObject));

        return resultObject;
    }

    // Returns an enumerator that iterates through the collection.
    public IEnumerator> GetEnumerator()
    {
        return Translations.GetEnumerator();
    }

    // Returns an enumerator that iterates through a collection.
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

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

Выводы

Условие where является прекрасным инструментом, который дает универсальным типам .NET еще больше мощности для выполнения задач, больше, чем просто базовое поведение "на уровне объекта".

Есть несколько вещей, которые вы не можете определить с помощью ограничений (в настоящее время):

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

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

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

Progg it

comments powered by Disqus