Skip to content

Latest commit

 

History

History
3224 lines (2851 loc) · 199 KB

c18.md

File metadata and controls

3224 lines (2851 loc) · 199 KB

ГЛАВА 18. Обобщения

Эта глава посвящена обобщениям — одному из самых сложных и эффективных средств С#. Любопытно, что обобщения не вошли в первоначальную версию 1.0 и появились лишь в версии 2.0, но теперь они являются неотъемлемой частью языка С#. Не будет преувеличением сказать, что внедрение обобщений коренным образом из­ менило характер С#. Это нововведение не только означало появление нового элемента синтаксиса данного языка, но и открыло новые возможности для внесения многочисленных изменений и обновлений в библиотеку классов. И хотя по­ сле внедрения обобщений прошло уже несколько лет, по­ следствия этого важного шага до сих пор сказываются на развитии С# как языка программирования.

Обобщения как языковое средство очень важны потому, что они позволяют создавать классы, структуры, интерфей­ сы, методы и делегаты для обработки разнотипных данных с соблюдением типовой безопасности. Как вам должно быть известно, многие алгоритмы очень похожи по своей логике независимо от типа данных, к которым они приме­ няются. Например, механизм, поддерживающий очередь, остается одинаковым независимо от того, предназначена ли очередь для хранения элементов типа int, string, object или для класса, определяемого пользователем. До появле­ ния обобщений для обработки данных разных типов при­ ходилось создавать различные варианты одного и того же алгоритма. А благодаря обобщениям можно сначала вы­ работать единое решение независимо от конкретного типа данных, а затем применить его к обработке данных самых разных типов без каких-либо дополнительных усилий.

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

Что такое обобщения

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

Следует особо подчеркнуть, что в C# всегда имелась возможность создавать обоб­ щенный код, оперируя ссылками типа object. А поскольку класс object является базовым для всех остальных классов, то по ссылке типа object можно обращаться к объекту любого типа. Таким образом, до появления обобщений для оперирования разнотипными объектами в программах служил обобщенный код, в котором для этой цели использовались ссылки типа object.

Но дело в том, что в таком коде трудно было соблюсти типовую безопасность, по­ скольку для преобразования типа object в конкретный тип данных требовалось при­ ведение типов. А это служило потенциальным источником ошибок из-за того, что приведение типов могло быть неумышленно выполнено неверно. Это затруднение по­ зволяют преодолеть обобщения, обеспечивая типовую безопасность, которой раньше так недоставало. Кроме того, обобщения упрощают весь процесс, поскольку исключа­ ют необходимость выполнять приведение типов для преобразования объекта или дру­ гого типа обрабатываемых данных. Таким образом, обобщения расширяют возможно­ сти повторного использования кода и позволяют делать это надежно и просто.

ПРИМЕЧАНИЕ Программирующим на C++ и Java необходимо иметь в виду, что обобщения в C# не сле­ дует путать с шаблонами в C++ и обобщениями в Java, поскольку это разные, хотя и похожие средства. В действительности между этими тремя подходами к реализации обобщений суще­ ствуют коренные различия. Если вы имеете некоторый опыт программирования на C++ или Java, то постарайтесь на основании этого опыта не делать никаких далеко идущих выводов о том, как обобщения действуют в С#.

Простой пример обобщений

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

// Простой пример обобщенного класса.
using System;

// В приведенном ниже классе Gen параметр типа Т заменяется
// реальным типом данных при создании объекта типа Gen.
class Gen<T> {
    Т ob; // объявить переменную типа Т

    // Обратите внимание на то, что у этого конструктора имеется параметр типа Т.
    public Gen(T о) {
        ob = о;
    }

    // Возвратить переменную экземпляра ob, которая относится к типу Т.
    public Т GetOb() {
        return ob;
    }

    // Показать тип Т.
    public void ShowType() {
        Console.WriteLine("К типу T относится " + typeof(Т));
    }
}

// Продемонстрировать применение обобщенного класса.
class GenericsDemo {
    static void Main() {
        // Создать переменную ссылки на объект Gen типа int.
        Gen<int> iOb;

        // Создать объект типа Gen<int> и присвоить ссылку на него переменной iOb.
        iOb = new Gen<int>(102);

        // Показать тип данных, хранящихся в переменной iOb.
        iOb.ShowType();

        // Получить значение переменной iOb.
        int v = iOb.GetOb();
        Console.WriteLine("Значение: " + v);
        Console.WriteLine();

        // Создать объект типа Gen для строк.
        Gen<string> strOb = new Gen<string>("Обобщения повышают эффективность.");

        // Показать тип данных, хранящихся в переменной strOb.
        strOb.ShowType();

        // Получить значение переменной strOb.
        string str = strOb.GetOb();
        Console.WriteLine("Значение: " + str);
    }
}

Эта программа дает следующий результат.

К типу Т относится System.Int32
Значение: 102

К типу Т относится System.String
Значение: Обобщения повышают эффективность.

Внимательно проанализируем эту программу. Прежде всего обратите внимание на объявление класса Gen в приведенной ниже строке кода:

class Gen<T> {

где Т — это имя параметра типа. Это имя служит в качестве метки-заполнителя кон­ кретного типа, который указывается при создании объекта класса Gen. Следовательно, имя Т используется в классе Gen всякий раз, когда требуется параметр типа. Обратите внимание на то, что имя Т заключается в угловые скобки (< >). Этот синтаксис мож­ но обобщить: всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. А поскольку параметр типа используется в классе Gen, то такой класс счита­ ется обобщенным.

В объявлении класса Gen можно указывать любое имя параметра типа, но по тра­ диции выбирается имя Т. К числу других наиболее употребительных имен параметров типа относятся V и Е. Вы, конечно, вольны использовать и более описательные имена, например TValue или ТКеу. Но в этом случае первой в имени параметра типа при­ нято указывать прописную букву Т.

Далее имя Т используется для объявления переменной ob, как показано в следую­ щей строке кода.

Т ob; // объявить переменную типа Т

Как пояснялось выше, имя параметра типа Т служит меткой-заполнителем кон­ кретного типа, указываемого при создании объекта класса Gen. Поэтому переменная ob будет иметь тип, привязываемый к Т при получении экземпляра объекта класса Gen. Так, если вместо Т указывается тип string, то в экземпляре данного объекта перемен­ ная оb будет иметь тип string.

А теперь рассмотрим конструктор класса Gen.

public Gen(Т о) {
    ob = о;
}

Как видите, параметр о этого конструктора относится к типу Т. Это означает, что конкретный тип параметра о определяется типом, привязываемым к Т при создании объекта класса Gen. А поскольку параметр о и переменная экземпляра ob относятся к типу Т, то после создания объекта класса Gen их конкретный тип окажется одним и тем же.

С помощью параметра типа Т можно также указывать тип, возвращаемый мето­ дом, как показано ниже на примере метода GetOb().

public Т GetOb() {
    return ob;
}

Переменная ob также относится к типу Т, поэтому ее тип совпадает с типом, воз­ вращаемым методом GetOb().

Метод ShowType() отображает тип параметра Т, передавая его оператору typeof. Но поскольку реальный тип подставляется вместо Т при создании объекта класса Gen, то оператор typeof получит необходимую информацию о конкретном типе. В классе GenericsDemo демонстрируется применение обобщенного класса Gen.

Сначала в нем создается вариант класса Gen для типа int.

Gen<int> iOb;

Внимательно проанализируем это объявление. Прежде всего обратите внимание на то, что тип int указывается в угловых скобках после имени класса Gen. В этом случае int служит аргументом типа, привязанным к параметру типа Т в классе Gen. В данном объявлении создается вариант класса Gen, в котором тип Т заменяется типом int вез­ де, где он встречается. Следовательно, после этого объявления int становится типом переменной ob и возвращаемым типом метода GetOb().

В следующей строке кода переменной iOb присваивается ссылка на экземпляр объекта класса Gen для варианта типа int.

iOb = new Gen<int>(102);

Обратите внимание на то, что при вызове конструктора класса Gen указывается так­ же аргумент типа int. Это необходимо потому, что переменная (в данном случае — iOb), которой присваивается ссылка, относится к типу Gen. Поэтому ссылка, воз­ вращаемая оператором new, также должна относиться к типу Gen. В противном случае во время компиляции возникнет ошибка. Например, приведенное ниже при­ сваивание станет причиной ошибки во время компиляции.

iOb = new Gen<double>(118.12); // Ошибка!

Переменная iOb относится к типу Gen и поэтому не может использоваться для ссылки на объект типа Gen. Такой контроль типов относится к одним из главных преимуществ обобщений, поскольку он обеспечивает типовую безопасность. Затем в программе отображается тип переменной ob в объекте iOb — тип System. Int32. Это структура .NET, соответствующая типу int. Далее значение переменной ob получается в следующей строке кода.

int v = iOb.GetOb();

Возвращаемым для метода GetOb() является тип Т, который был заменен на тип int при объявлении переменной iOb, и поэтому метод GetOb() возвращает значение того же типа int. Следовательно, данное значение может быть присвоено переменной v типа int.

Далее в классе GenericsDemo объявляется объект типа Gen<string>.

Gen<string> strOb = new Gen<string>("Обобщения повышают эффективность.");

В этом объявлении указывается аргумент типа string, поэтому в объекте класса Gen вместо Т подставляется тип string. В итоге создается вариант класса Gen для типа string, как демонстрируют остальные строки кода рассматриваемой здесь программы.

Прежде чем продолжить изложение, следует дать определение некоторым терми­ нам. Когда для класса Gen указывается аргумент типа, например int или string, то создается так называемый в C# закрыто сконструированный тип. В частности, Gen является закрыто сконструированным типом. Ведь, по существу, такой обобщенный тип, как Gen, является абстракцией. И только после того, как будет сконструиро­ ван конкретный вариант, например Gen, создается конкретный тип. А конструк­ ция, подобная Gen, называется в C# открыто сконструированным типом, поскольку в ней указывается параметр типа Т, но не такой конкретный тип, как int.

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

Различение обобщенных типов по аргументам типа

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

iOb = strOb; // Неверно!

Несмотря на то что обе переменные, iOb и strOb, относятся к типу Gen, они ссылаются на разные типы, поскольку у них разные аргументы.

Повышение типовой безопасности с помощью обобщений

В связи с изложенным выше возникает следующий резонный вопрос: если анало­ гичные функциональные возможности обобщенного класса Gen можно получить и без обобщений, просто указав объект как тип данных и выполнив надлежащее приведение типов, то какая польза от того, что класс Gen делается обобщенным? Ответ на этот во­ прос заключается в том, что обобщения автоматически обеспечивают типовую безо­ пасность всех операций, затрагивающих класс Gen. В ходе выполнения этих операций обобщения исключают необходимость обращаться к приведению типов и проверять соответствие типов в коде вручную.

Для того чтобы стали более понятными преимущества обобщений, рассмотрим сначала программу, в которой создается необобщенный аналог класса Gen.

// Класс NonGen является полным функциональным аналогом
// класса Gen, но без обобщений.
using System;

class NonGen {
    object ob; // переменная ob теперь относится к типу object

    // Передать конструктору ссылку на объект типа object.
    public NonGen(object о) {
        ob = о;
    }

    // Возвратить объект типа object.
    public object GetOb() {
        return ob;
    }

    // Показать тип переменной ob.
    public void ShowType() {
        Console.WriteLine("Тип переменной ob: " + ob.GetType());
    }
}

// Продемонстрировать применение необобщенного класса.
class NonGenDemo {
    static void Main() {
        NonGen iOb;

        // Создать объект класса NonGen.
        iOb = new NonGen(102);

        // Показать тип данных, хранящихся в переменной iOb.
        iOb.ShowType();

        // Получить значение переменной iOb.
        // На этот раз потребуется приведение типов.
        int v = (int) iOb.GetOb();
        Console.WriteLine("Значение: " + v);
        Console.WriteLine();

        // Создать еще один объект класса NonGen и
        // сохранить строку в переменной it.
        NonGen strOb = new NonGen("Тест на необобщенность");

        // Показать тип данных, хранящихся в переменной strOb.
        strOb.ShowType();

        // Получить значение переменной strOb.
        //И в этом случае требуется приведение типов.
        String str = (string) strOb.GetOb();
        Console.WriteLine("Значение: " + str);

        // Этот код компилируется, но он принципиально неверный!
        iOb = strOb;

        // Следующая строка кода приводит к исключительной
        // ситуации во время выполнения.
        // v = (int) iOb.GetOb(); // Ошибка при выполнении!
    }
}

При выполнении этой программы получается следующий результат.

Тип переменной ob: System.Int32
Значение: 102

Тип переменной ob: System.String
Значение: Тест на необобщенность

Как видите, результат выполнения этой программы такой же, как и у предыдущей программы.

В этой программе обращает на себя внимание ряд любопытных моментов. Прежде всего, тип Т заменен везде, где он встречается в классе NonGen. Благодаря этому в клас­ се NonGen может храниться объект любого типа, как и в обобщенном варианте этого класса. Но такой подход оказывается непригодным по двум причинам. Во-первых, для извлечения хранящихся данных требуется явное приведение типов. И во-вторых, мно­ гие ошибки несоответствия типов не могут быть обнаружены вплоть до момента вы­ полнения программы. Рассмотрим каждую из этих причин более подробно.

Начнем со следующей строки кода.

int v = (int) iOb.GetOb();

Теперь возвращаемым типом метода GetOb() является object, а следовательно, для распаковки значения, возвращаемого методом GetOb(), и его последующего со­ хранения в переменной v требуется явное приведение к типу int. Если исключить приведение типов, программа не будет скомпилирована. В обобщенной версии этой программы приведение типов не требовалось, поскольку тип int указывался в каче­ стве аргумента типа при создании объекта iOb. А в необобщенной версии этой про­ граммы потребовалось явное приведение типов. Но это не только неудобно, но и чре­ вато ошибками.

А теперь рассмотрим следующую последовательность кода в конце анализируемой здесь программы.

// Этот код компилируется, но он принципиально неверный!
iOb = strOb;

// Следующая строка кода приводит к исключительной
// ситуации во время выполнения.
// v = (int) iOb.GetOb(); // Ошибка при выполнении!

В этом коде значение переменной strOb присваивается переменной iOb. Но пере­ менная strOb ссылается на объект, содержащий символьную строку, а не целое зна­ чение. Такое присваивание оказывается верным с точки зрения синтаксиса, поскольку все ссылки на объекты класса NonGen одинаковы, а значит, по ссылке на один объект класса NonGen можно обращаться к любому другому объекту класса NonGen. Тем не менее такое присваивание неверно с точки зрения семантики, как показывает следую­ щая далее закомментированная строка кода. В этой строке тип, возвращаемый мето­ дом GetOb(), приводится к типу int, а затем предпринимается попытка присвоить полученное в итоге значение переменной int. К сожалению, в отсутствие обобщений компилятор не сможет выявить подобную ошибку. Вместо этого возникнет исключи­ тельная ситуация во время выполнения, когда будет предпринята попытка приведения к типу int. Для того чтобы убедиться в этом, удалите символы комментария в начале данной строки кода, скомпилируйте, а затем выполните программу. При ее выполне­ нии возникнет ошибка.

Упомянутая выше ситуация не могла бы возникнуть, если бы в программе исполь­ зовались обобщения. Компилятор выявил бы ошибку в приведенной выше последо­ вательности кода, если бы она была включена в обобщенную версию программы, и со­ общил бы об этой ошибке, предотвратив тем самым серьезный сбой, приводящий к исключительной ситуации при выполнении программы. Возможность создавать типизированный код, в котором ошибки несоответствия типов выявляются во время компиляции, является главным преимуществом обобщений. Несмотря на то что в C# всегда имелась возможность создавать "обобщенный" код, используя ссылки на объек­ ты, такой код не был типизированным, т.е. не обеспечивал типовую безопасность, а его неправильное применение могло привести к исключительным ситуациям во время выполнения. Подобные ситуации исключаются благодаря обобщениям. По существу, обобщения переводят ошибки при выполнении в разряд ошибок при компиляции. В этом и заключается основная польза от обобщений.

В рассматриваемой здесь необобщенной версии программы имеется еще один любопытный момент. Обратите внимание на то, как тип переменной ob экземпляра класса NonGen создается с помощью метода ShowType() в следующей строке кода.

Console.WriteLine("Тип переменной ob: " + ob.GetType());

Как пояснялось в главе 11, в классе object определен ряд методов, доступных для всех типов данных. Одним из них является метод GetType(), возвращающий объект класса Туре, который описывает тип вызывающего объекта во время выполнения. Сле­ довательно, конкретный тип объекта, на который ссылается переменная ob, становит­ ся известным во время выполнения, несмотря на то, что тип переменной ob указан в исходном коде как object. Именно поэтому в среде CLR будет сгенерировано ис­ ключение при попытке выполнить неверное приведение типов во время выполнения программы.

Обобщенный класс с двумя параметрами типа

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

// Простой обобщенный класс с двумя параметрами типа Т и V.
using System;

class TwoGen<T, V> {
    T ob1;
    V ob2;

    // Обратите внимание на то, что в этом конструкторе
    // указываются параметры типа Т и V.
    public TwoGen(Т o1, V о2) {
        ob1 = o1;
        оb2 = о2;
    }

    // Показать типы Т и V.
    public void showTypes() {
        Console.WriteLine("К типу T относится " + typeof(Т));
        Console.WriteLine("К типу V относится " + typeof(V));
    }

    public Т getob1() {
        return оb1;
    }

    public V GetObj2() {
        return ob2;
    }
}

// Продемонстрировать применение обобщенного класса с двумя параметрами типа.
class SimpGen {
    static void Main() {
        TwoGen<int, string> tgObj =
            new TwoGen<int, string>(119, "Альфа Бета Гамма");

        // Показать типы.
        tgObj.ShowTypes();

        // Получить и вывести значения.
        int v = tgObj.getob1();
        Console.WriteLine("Значение: " + v);
        string str = tgObj.GetObj2();
        Console.WriteLine("Значение: " + str);
    }
}

Эта программа дает следующий результат.

К типу Т относится System.Int32
К типу V относится System.String
Значение: 119
Значение: Альфа Бета Гамма

Обратите внимание на то, как объявляется класс TwoGen.

class TwoGen<T, V> {

В этом объявлении указываются два параметра типа Т и V, разделенные запятой. А поскольку у класса TwoGen два параметра типа, то при создании объекта этого клас­ са необходимо указывать два соответствующих аргумента типа, как показано ниже.

TwoGen<int, string> tgObj =
    new TwoGen<int, string>(119, "Альфа Бета Гамма");

В данном случае вместо Т подставляется тип int, а вместо V — тип string. В представленном выше примере указываются аргументы разного типа, но они мо­ гут быть и одного типа. Например, следующая строка кода считается вполне допус­ тимой.

TwoGen<string, string> х =
    new TwoGen<string, string> ("Hello", "Goodbye");

В этом случае оба типа, Т и V, заменяются одним и тем же типом, string. Ясно, что если бы аргументы были одного и того же типа, то два параметра типа были бы не нужны.

Общая форма обобщенного класса

Синтаксис обобщений, представленных в предыдущих примерах, может быть све­ ден к общей форме. Ниже приведена общая форма объявления обобщенного класса.

class имя_класса<список_параметров_типа> { // ...

А вот как выглядит синтаксис объявления ссылки на обобщенный класс.

имя_класса<список_аргументов_типа> имя_переменной -
    new имя_класса<список_параметров_типа> (список_аргументов_конструктора);

Ограниченные типы

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

class Gen<T> {

Это означает, что вполне допустимо создавать объекты класса Gen, в которых тип Т заменяется типом int, double, string, FileStream или любым другим типом дан­ ных. Во многих случаях отсутствие ограничений на указание аргументов типа считается вполне приемлемым, но иногда оказывается полезно ограничить круг типов, которые могут быть указаны в качестве аргумента типа.

Допустим, что требуется создать метод, оперирующий содержимым потока, вклю­ чая объекты типа FileStream или MemoryStream. На первый взгляд, такая ситуация идеально подходит для применения обобщений, но при этом нужно каким-то обра­ зом гарантировать, что в качестве аргументов типа будут использованы только типы потоков, но не int или любой другой тип. Кроме того, необходимо как-то уведомить компилятор о том, что методы, определяемые в классе потока, будут доступны для применения. Так, в обобщенном коде должно быть каким-то образом известно, что в нем может быть вызван метод Read().

Для выхода из подобных ситуаций в C# предусмотрены ограниченные типы. Указы­ вая параметр типа, можно наложить определенное ограничение на этот параметр. Это делается с помощью оператора where при указании параметра типа:

class имя_класса<параметр_типа> where параметр_типа : ограничения { // ...

где ограничения указываются списком через запятую.

В C# предусмотрен ряд ограничений на типы данных.

  • Ограничение на базовый класс, требующее наличия определенного базового класса в аргументе типа. Это ограничение накладывается указанием имени требуемого базового класса. Разновидностью этого ограничения является неприкрытое ограничение типа, при котором на базовый класс указывает параметр типа, а не конкретный тип. Благодаря этому устанавливается взаимосвязь между двумя параметрами типа.
  • Ограничение на интерфейс, требующее реализации одного или нескольких интерфейсов аргументом типа. Это ограничение накладывается указанием имени требуемого интерфейса.
  • Ограничение на конструктор, требующее предоставить конструктор без параметров в аргументе типа. Это ограничение накладывается с помощью оператора new().
  • Ограничение ссылочного типа, требующее указывать аргумент ссылочного типа с помощью оператора class.
  • Ограничение типа значения, требующее указывать аргумент типа значения с помощью оператора struct.

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

Применение ограничения на базовый класс

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

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

Ниже приведена общая форма наложения ограничения на базовый класс, в кото­ рой используется оператор where:

where Т : имя_базового_класса

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

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

// Простой пример, демонстрирующий механизм наложения
// ограничения на базовый класс.
using System;

class А {
    public void Hello() {
        Console.WriteLine("Hello");
    }
}

// Класс В наследует класс А.
class В : А { }

// Класс С не наследует класс А.
class С { }

// В силу ограничения на базовый класс во всех аргументах типа,
// передаваемых классу Test, должен присутствовать базовый класс А.
class Test<T> where Т : А {
    Т obj;

    public Test(Т о) {
        obj = о;
    }

    public void SayHello() {
        // Метод Hello() вызывается, поскольку он объявлен в базовом классе А.
        obj.Hello();
    }
}

class BaseClassConstraintDemo {
    static void Main() {
        A a = new A();
        В b = new В();
        С с = new С();

        // Следующий код вполне допустим, поскольку класс А указан как базовый.
        Test<A> t1 = new Test<A>(a);
        t1.SayHello();

        // Следующий код вполне допустим, поскольку класс В наследует от класса А.
        Test<B> t2 = new Test<B>(b);
        t2.SayHello();

        // Следующий код недопустим, поскольку класс С не наследует от класса А.
        // Test<C> t3 = new Test<C>(c); // Ошибка!
        // t3.SayHello(); // Ошибка!
    }
}

В данном примере кода класс А наследуется классом В, но не наследуется классом С. Обратите также внимание на то, что в классе А объявляется метод Hello(), а класс Test объявляется как обобщенный следующим образом.

class Test<T> where Т : А {

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

А теперь обратите внимание на то, что в классе Test объявляется метод SayHello(), как показано ниже.

public void SayHello() {
    // Метод Hello() вызывается, поскольку он объявлен в базовом классе А.
    obj.Hello();
}

Этот метод вызывает в свою очередь метод Hello() для объекта obj типа Т. Любо­ пытно, что единственным основанием для вызова метода Hello() служит следующее требование ограничения на базовый класс: любой аргумент типа, привязанный к типу Т, должен относиться к классу А или наследовать от класса А, в котором объявлен ме­ тод Hello(). Следовательно, любой допустимый тип Т будет также определять метод Hello(). Если бы данное ограничение на базовый класс не было наложено, то компи­ лятору ничего не было бы известно о том, что метод Hello() может быть вызван для объекта типа Т. Убедитесь в этом сами, удалив оператор where из объявления обоб­ щенного класса Test. В этом случае программа не подлежит компиляции, поскольку теперь метод Hello() неизвестен.

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

// Test<C> t3 = new Test<C>(c); // Ошибка!
// t3.SayHello(); // Ошибка!

Класс С не наследует от класса А, и поэтому он не может использоваться в качестве аргумента типа при создании объекта типа Test. Убедитесь в этом сами, удалив сим­ волы комментария и попытавшись перекомпилировать этот код.

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

В предыдущем примере показано, как накладывается ограничение на базовый класс, но из него не совсем ясно, зачем это вообще нужно. Для того чтобы особое зна­ чение ограничения на базовый класс стало понятнее, рассмотрим еще один, более практический пример. Допустим, что требуется реализовать механизм управления списками телефонных номеров, чтобы пользоваться разными категориями таких спи­ сков, в частности отдельными списками для друзей, поставщиков, клиентов и т.д. Для этой цели можно сначала создать класс PhoneNumber, в котором будут храниться имя абонента и номер его телефона. Такой класс может иметь следующий вид.

// Базовый класс, в котором хранятся имя абонента и номер его телефона.
class PhoneNumber {
    public PhoneNumber(string n, string num) {
        Name = n;
        Number = num;
    }

    // Автоматически реализуемые свойства, в которых
    // хранятся имя абонента и номер его телефона.
    public string Number { get; set; }
    public string Name { get; set; }
}

Далее создадим классы, наследующие класс PhoneNumber: Friend и Supplier. Эти классы приведены ниже.

// Класс для телефонных номеров друзей.
class Friend : PhoneNumber {
    public Friend(string n, string num, bool wk) :
        base(n, num)
    {
        IsWorkNumber = wk;
    }

    public bool IsWorkNumber { get; private set; }
    // ...
}

// Класс для телефонных номеров поставщиков.
class Supplier : PhoneNumber {
    public Supplier(string n, string num) :
    base(n, num) { }
    // ...
}

Обратите внимание на то, что в класс Friend введено свойство IsWorkNumber, воз­ вращающее логическое значение true, если номер телефона является рабочим.

Для управления списками телефонных номеров создадим еще один класс под на­ званием PhoneList. Его следует сделать обобщенным, поскольку он должен служить для управления любым списком телефонных номеров. В функции такого управления должен, в частности, входить поиск телефонных номеров по заданным именам и нао­ борот, поэтому на данный класс необходимо наложить ограничение по типу, требую­ щее, чтобы объекты, сохраняемые в списке, были экземплярами класса, производного от класса PhoneNumber.

// Класс PfconeList способен управлять любым видом списка телефонных
// номеров, при условии, что он является производным от класса PhoneNumber.
class PhoneList<T> where T : PhoneNumber {
    T[] phList;
    int end;

    public PhoneList() {
        phList = new T[10];
        end = 0;
    }

    // Добавить элемент в список.
    public bool Add(T newEntry) {
        if(end == 10) return false;
        phList[end] = newEntry;
        end++;
        return true;
    }

    // Найти и возвратить сведения о телефоне по заданному имени.
    public Т FindByName(string name) {
        for(int i=0; i<end; i++) {
            // Имя может использоваться, потому что его свойство Name
            // относится к членам класса PhoneNumber, который является
            // базовым по накладываемому ограничению.
            if(phList[i].Name == name)
            return phList [i];
        }
        // Имя отсутствует в списке.
        throw new NotFoundException();
    }

    // Найти и возвратить сведения о телефоне по заданному номеру.
    public Т FindByNumber(string number) {
        for(int i=0; i<end; i++) {
        // Номер телефона также может использоваться, поскольку
        // его свойство Number относится к членам класса PhoneNumber,
        // который является базовым по накладываемому ограничению.
        if(phList[i].Number == number)
            return phList[i];
        }
        // Номер телефона отсутствует в списке.
        throw new NotFoundException();
    }
    // ...
}

Ограничение на базовый класс разрешает коду в классе PhoneList доступ к свой­ ствам Name и Number для управления любым видом списка телефонных номеров. Оно гарантирует также, что для построения объекта класса PhoneList будут использовать­ ся только доступные типы. Обратите внимание на то, что в классе PhoneList генери­ руется исключение NotFoundException, если имя или номер телефона не найдены. Это специальное исключение, объявляемое ниже.

class NotFoundException : Exception {
    /* Реализовать все конструкторы класса Exception.
    Эти конструкторы выполняют вызов конструктора базового класса.
    Класс NotFoundException ничем не дополняет класс Exception и
    поэтому не требует никаких дополнительных действий. */
    public NotFoundException() : base() { }
    public NotFoundException(string str) : base(str) { }
    public NotFoundException(
    string str, Exception inner) : base(str, inner) { }
    protected NotFoundException(
    System.Runtime.Serialization.Serializationlnfo si,
    System.Runtime.Serialization.StreamingContext sc) :
    base(si, sc) { }
}

В данном примере используется только конструктор, вызываемый по умолчанию, но ради наглядности этого примера в классе исключения NotFoundException реали­ зуются все конструкторы, определенные в классе Exception. Обратите внимание на то, что эти конструкторы вызывают эквивалентный конструктор базового класса, опре­ деленный в классе Exception. А поскольку класс исключения NotFoundException ничем не дополняет базовый класс Exception, то для любых дополнительных дей­ ствий нет никаких оснований.

В приведенной ниже программе все рассмотренные выше фрагменты кода объе­ диняются вместе, а затем демонстрируется применение класса PhoneList. Кро­ ме того, в ней создается класс EmailFriend. Этот класс не наследует от класса PhoneNumber, а следовательно, он не может использоваться для создания объектов класса PhoneList.

// Более практический пример, демонстрирующий применение
// ограничения на базовый класс.
using System;

// Специальное исключение, генерируемое в том случае,
// если имя или номер телефона не найдены.
class NotFoundException : Exception {
    /* Реализовать все конструкторы класса Exception.
    Эти конструкторы выполняют вызов конструктора базового класса.
    Класс NotFoundException ничем не дополняет класс Exception и
    поэтому не требует никаких дополнительных действий. */
    public NotFoundException() : base() { }
    public NotFoundException(string str) : base(str) { }
    public NotFoundException(
    string str, Exception inner) : base(str, inner) { }
    protected NotFoundException)
    System.Runtime.Serialization.SerializationInfo si,
    System.Runtime.Serialization.StreamingContext sc) :
    base(si, sc) { }
}

// Базовый класс, в котором хранятся имя абонента и номер его телефона.
class PhoneNumber {
    public PhoneNumber(string n, string num) {
        Name = n;
        Number = num;
    }

    public string Number { get; set; }
    public string Name { get; set; }
}

// Класс для телефонных номеров друзей.
class Friend : PhoneNumber {
    public Friend(string n, string num, bool wk) :
        base(n, num)
    {
        IsWorkNumber = wk;
    }
    public bool IsWorkNumber { get; private set; }
    // ...
}

// Класс для телефонных номеров поставщиков.
class Supplier : PhoneNumber {
    public Supplier(string n, string num) :
    base (n, num) { }
    // ...
}

// Этот класс не наследует от класса PhoneNumber.
class EmailFriend {
    // ...
}

// Класс PhoneList способен управлять любым видом списка телефонных номеров.
// при условии, что он является производным от класса PhoneNumber.
class PhoneList<T> where T : PhoneNumber {
    T[] phList;
    int end;

    public PhoneList() {
        phList = new T[10];
        end = 0;
    }

    // Добавить элемент в список.
    public bool Add(T newEntry) {
        if(end == 10) return false;
        phList[end] = newEntry;
        end++;
        return true;
    }

    // Найти и возвратить сведения о телефоне по заданному имени.
    public Т FindByName(string name) {
        for (int i=0; i<end; i++) {
        // Имя может использоваться, потому что его свойство Name
        // относится к членам класса PhoneNumber, который является
        // базовым по накладываемому ограничению.
        if(phList[i].Name == name)
            return phList [i];
        }
        // Имя отсутствует в списке.
        throw new NotFoundException();
    }

    // Найти и возвратить сведения о телефоне по заданному номеру.
    public Т FindByNumber(string number) {
        for(int i=0; i<end; i++) {
            // Номер телефона также может использоваться, поскольку
            // его свойство Number относится к членам класса PhoneNumber,
            // который является базовым по накладываемому ограничению.
            if(phList[i].Number == number)
                return phList[i];
        }
        // Номер телефона отсутствует в списке.
        throw new NotFoundException();
    }
    // ...
}

// Продемонстрировать наложение ограничений на базовый класс.
class UseBaseClassConstraint {
    static void Main() {
        // Следующий код вполне допустим, поскольку
        // класс Friend наследует от класса PhoneNumber.
        PhoneList<Friend> plist = new PhoneList<Friend>();
        plist.Add(new Friend("Том", "555-1234", true));
        plist.Add(new Friend("Гари", "555-6756", true));
        plist.Add(new Friend("Матт", "555-9254", false));

        try {
            // Найти номер телефона по заданному имени друга.
            Friend frnd = plist.FindByName("Гари");

            Console.Write(frnd.Name + " " + frnd.Number);

            if(frnd.IsWorkNumber)
                Console.WriteLine(" (рабочий)");
            else
                Console.WriteLine();
        } catch(NotFoundException) {
            Console.WriteLine("He найдено");
        }
        Console.WriteLine();

        // Следующий код также допустим, поскольку
        // класс Supplier наследует от класса PhoneNumber.
        PhoneList<Supplier> plist2 = new PhoneList<Supplier>();
        plist2.Add(new Supplier("Фирма Global Hardware", "555-8834"));
        plist2.Add(new Supplier("Агентство Computer Warehouse", "555-9256"));
        plist2.Add(new Supplier("Компания NetworkCity", "555-2564"));

        try {
            // Найти наименование поставщика по заданному номеру телефона.
            Supplier sp = plist2.FindByNumber("555-2564");
            Console.WriteLine(sp.Name + " + sp.Number);
        } catch(NotFoundException) {
            Console.WriteLine("He найдено");
        }

        // Следующее объявление недопустимо, поскольку
        // класс EmailFriend НЕ наследует от класса PhoneNumber.
        // PhoneList<EmailFriend> plist3 =
        // new PhoneList<EmailFriend>(); // Ошибка!
    }
}

Ниже приведен результат выполнения этой программы.

Гари: 555-6756 (рабочий)
Компания NetworkCity: 555-2564

Поэкспериментируйте с этой программой. В частности, попробуйте составить раз­ ные виды списков телефонных номеров или воспользоваться свойством IsWorkNumber в классе PhoneList. Вы сразу же обнаружите, что компилятор не позволит вам этого сделать, потому что свойство IsWorkNumber определено в классе Friend, а не в классе PhoneNumber, а следовательно, оно неизвестно в классе PhoneList.

Применение ограничения на интерфейс

Ограничение на интерфейс позволяет указывать интерфейс, который должен быть реализован аргументом типа. Это ограничение служит тем же основным целям, что и ограничение на базовый класс. Во-первых, оно позволяет использовать члены интер­ фейса в обобщенном классе. И во-вторых, оно гарантирует использование только тех аргументов типа, которые реализуют указанный интерфейс. Это означает, что для лю­ бого ограничения, накладываемого на интерфейс, аргумент типа должен обозначать сам интерфейс или же тип, реализующий этот интерфейс.

Ниже приведена общая форма наложения ограничения на интерфейс, в которой используется оператор where:

where Т : имя_интерфейса

где Т — это имя параметра типа, а имя_интерфейса — конкретное имя ограничивае­ мого интерфейса. В этой форме ограничения может быть указан список интерфейсов через запятую. Если ограничение накладывается одновременно на базовый класс и ин­ терфейс, то первым в списке должен быть указан базовый класс.

Ниже приведена программа, демонстрирующая наложение ограничения на ин­ терфейс и представляющая собой переработанный вариант предыдущего примера программы, управляющей списками телефонных номеров. В этом варианте класс PhoneNumber преобразован в интерфейс IPhoneNumber, который реализуется в клас­ сах Friend и Supplier.

// Применить ограничение на интерфейс.
using System;

// Специальное исключение, генерируемое в том случае,
// если имя или номер телефона не найдены.
class NotFoundException : Exception {
    /* Реализовать все конструкторы класса Exception.
    Эти конструкторы выполняют вызов конструктора базового класса.
    Класс NotFoundException ничем не дополняет класс Exception и
    поэтому не требует никаких дополнительных действий. */
    public NotFoundException() : base() { }
    public NotFoundException(string str) : base(str) { }
    public NotFoundException(
    string str,Exception inner) : base(str, inner) { }
    protected NotFoundException(
    System.Runtime.Serialization.SerializationInfо si,
    System.Runtime.Serialization.StreamingContext sc) :
    base(si, sc) { }
}

// Интерфейс, поддерживающий имя и номер телефона.
public interface IPhoneNumber {
    string Number {
        get;
        set;
    }

    string Name {
        get;
        set;
    }
}

// Класс для телефонных номеров друзей.
// В нем реализуется интерфейс IPhoneNumber.
class Friend : IPhoneNumber {
    public Friend(string n, string num, bool wk) {
        Name = n;
        Number = num;
        IsWorkNumber = wk;
    }

    public bool IsWorkNumber { get; private set; }

    // Реализовать интерфейс IPhoneNumber.
    public string Number { get; set; }

    public string Name { get; set; }
    // ...
}

// Класс для телефонных номеров поставщиков.
class Supplier : IPhoneNumber {
    public Supplier(string n, string num) {
        Name = n;
        Number = num;
    }

    // Реализовать интерфейс IPhoneNumber.
    public string Number { get; set; }
    public string Name { get; set; }
    // ...
}

// В этом классе интерфейс IPhoneNumber не реализуется.
class EmailFriend {
    // ...
}

// Класс PhoneList способен управлять любым видом списка телефонных
// номеров, при условии, что он реализует интерфейс PhoneNumber.
class PhoneList<T> where T : IPhoneNumber {
    T[] phList;
    int end;

    public PhoneList() {
        phList = new T[10];
        end = 0;
    }

    public bool Add(T newEntry) {
        if(end == 10) return false;
        phList[end] = newEntry;
        end++;
        return true;
    }

    // Найти и возвратить сведения о телефоне по заданному имени.
    public Т FindByName(string name) {
        for(int i=0; i<end; i++) {
            // Имя может использоваться, потому что его свойство Name
            // относится к членам интерфейса IPhoneNumber, на который
            // накладывается ограничение.
            if(phList[i].Name == name)
                return phList[i];
        }
        // Имя отсутствует в списке.
        throw new NotFoundException();
    }

    // Найти и возвратить сведения о телефоне по заданному номеру.
    public Т FindByNumber(string number) {
        for(int i=0; i<end; i++) {
            // Номер телефона также может использоваться, поскольку его
            // свойство Number относится к членам интерфейса IPhoneNumber,
            // на который накладывается ограничение.
            if(phList[i].Number == number)
                return phList[i];
        }
        // Номер телефона отсутствует в списке.
        throw new NotFoundException();
    }
    // ...
}

// Продемонстрировать наложение ограничения на интерфейс.
class UselnterfaceConstraint {
    static void Main() {
        // Следующий код вполне допустим, поскольку
        // в классе Friend реализуется интерфейс IPhoneNumber.
        PhoneList<Friend> plist = new PhoneList<Friend>();
        plist.Add(new Friend("Том", "555-1234", true));
        plist.Add(new Friend("Гари", "555-6756", true));
        plist.Add(new Friend("Матт", "555-9254", false));

        try {
            // Найти номер телефона по заданному имени друга.
            Friend frnd = plist.FindByName("Гари");

            Console.Write(frnd.Name + " + frnd.Number);

            if(frnd.IsWorkNumber)
                Console.WriteLine(" (рабочий)");
            else
                Console.WriteLine();
        } catch(NotFoundException) {
            Console.WriteLine("He найдено");
        }

        Console.WriteLine();

        // Следующий код также допустим, поскольку в классе Supplier
        // также реализуется интерфейс IPhoneNumber.
        PhoneList<Supplier> plist2 = new PhoneList<Supplier>();
        plist2.Add(new Supplier("Фирма Global Hardware", "555-8834"));
        plist2.Add(new Supplier("Агентство Computer Warehouse", "555-9256"));
        plist2.Add(new Supplier("Компания NetworkCity", "555-2564"));

        try {
            // Найти наименование поставщика по заданному номеру телефона.
            Supplier sp = plist2.FindByNumber("555-2564");
            Console.WriteLine(sp.Name + " + sp.Number);
        } catch(NotFoundException) {
            Console.WriteLine("He найдено");
        }
        // Следующее объявление недопустимо, поскольку
        // в классе EmailFriend НЕ реализуется интерфейс IPhoneNumber.
        // PhoneList<EmailFriend> plist3 =
        // new PhoneList<EmailFriend>(); // Ошибка!
    }
}

В этой версии программы ограничение на интерфейс, указываемое в классе PhoneList, требует, чтобы аргумент типа реализовал интерфейс IPhoneList. А по­ скольку этот интерфейс реализуется в обоих классах, Friend и Supplier, то они от­ носятся к допустимым типам, привязываемым к типу Т. В то же время интерфейс не реализуется в классе EmailFriend, и поэтому этот класс не может быть привязан к типу Т. Для того чтобы убедиться в этом, удалите символы комментария в двух по­ следних строках кода в методе Main(). Вы сразу же обнаружите, что программа не компилируется.

Применение ограничения new() на конструктор

Ограничение new() на конструктор позволяет получать экземпляр объекта обоб­ щенного типа. Как правило, создать экземпляр параметра обобщенного типа не уда­ ется. Но это положение изменяет ограничение new(), поскольку оно требует, чтобы аргумент типа предоставил конструктор без параметров. Им может быть конструктор, вызываемый по умолчанию и предоставляемый автоматически, если явно определяе­ мый конструктор отсутствует или же конструктор без параметров явно объявлен поль­ зователем. Накладывая ограничение new(), можно вызывать конструктор без параме­ тров для создания объекта.

Ниже приведен простой пример, демонстрирующий наложение ограничения new().

// Продемонстрировать наложение ограничения new() на конструктор.
using System;

class MyClass {
    public MyClass() {
        // ...
    }
    // ...
}

class Test<T> where T : new() {
    T obj;
    public Test() {
        // Этот код работоспособен благодаря наложению ограничения new().
        obj = new Т(); // создать объект типа Т
    }
    // ...
}

class ConsConstraintDemo {
    static void Main() {
        Test<MyClass> x = new Test<MyClass>();
    }
}

Прежде всего обратите внимание на объявление класса Test.

class Test<T> where T : new() {

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

Далее проанализируем приведенный ниже конструктор класса Test.

public Test() {
    // Этот код работоспособен благодаря наложению ограничения new().
    obj = new Т(); // создать объект типа Т
}

В этом фрагменте кода создается объект типа Т, и ссылка на него присваивается переменной экземпляра obj. Такой код допустим только потому, что ограничение new() требует наличия конструктора. Для того чтобы убедиться в этом, попробуйте сначала удалить ограничение new(), а затем попытайтесь перекомпилировать про­ грамму. В итоге вы получите сообщение об ошибке во время компиляции.

В методе Main() получается экземпляр объекта типа Test, как показано ниже.

Test<MyClass> х = new Test<MyClass>();

Обратите внимание на то, что аргументом типа в данном случае является класс MyClass и что в этом классе определяется конструктор без параметров. Следователь­ но, этот класс допускается использовать в качестве аргумента типа для класса Test. Следует особо подчеркнуть, что в классе MyClass совсем не обязательно определять конструктор без параметров явным образом. Его используемый по умолчанию кон­ структор вполне удовлетворяет накладываемому ограничению. Но если классу по­ требуются другие конструкторы, помимо конструктора без параметров, то придется объявить явным образом и вариант без параметров.

Что касается применения ограничения new(), то следует обратить внимание на три других важных момента. Во-первых, его можно использовать вместе с другими ограничениями, но последним по порядку. Во-вторых, ограничение new() позволяет конструировать объект, используя только конструктор без параметров, — даже если доступны другие конструкторы. Иными словами, передавать аргументы конструктору параметра типа не разрешается. И в-третьих, ограничение new() нельзя использовать одновременно с ограничением типа значения, рассматриваемым далее.

Ограничения ссылочного типа и типа значения

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

where Т : class

В этой форме с оператором where ключевое слово class указывает на то, что ар­ гумент Т должен быть ссылочного типа. Следовательно, всякая попытка использовать тип значения, например int или bool, вместо Т приведет к ошибке во время компи­ ляции.

Ниже приведена общая форма ограничения типа значения.

where Т : struct

В этой форме ключевое слово struct указывает на то, что аргумент Т должен быть типа значения. (Напомним, что структуры относятся к типам значений.) Следователь­ но, всякая попытка использовать ссылочный тип, например string, вместо T приведет к ошибке во время компиляции. Но если имеются дополнительные ограничения, то в любом случае class или struct должно быть первым по порядку накладываемым ограничением.

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

// Продемонстрировать наложение ограничения ссылочного типа.
using System;

class MyClass {
    // ...
}

// Наложить ограничение ссылочного типа.
class Test<T> where Т : class {
    Т obj;
    public Test() {
        // Следующий оператор допустим только потому, что
        // аргумент Т гарантированно относится к ссылочному
        // типу, что позволяет присваивать пустое значение.
        obj = null;
    }
    // ...
}

class ClassConstraintDemo {
    static void Main() {
        // Следующий код вполне допустим, поскольку MyClass является классом.
        Test<MyClass> х = new Test<MyClass>();

        // Следующая строка кода содержит ошибку, поскольку
        // int относится к типу значения.
        // Test<int> у = new Test<int>();
    }
}

Обратите внимание на следующее объявление класса Test.

class Test<T> where T : class {

Ограничение class требует, чтобы любой аргумент Т был ссылочного типа. В дан­ ном примере кода это необходимо для правильного выполнения операции присваи­ вания в конструкторе класса Test.

public Test() {
    // Следующий оператор допустим только потому, что
    // аргумент Т гарантированно относится к ссылочному
    // типу, что позволяет присваивать пустое значение.
    obj = null;
}

В этом фрагменте кода переменной obj типа T присваивается пустое значение. Та­ кое присваивание допустимо только для ссылочных типов. Как правило, пустое зна­ чение нельзя присвоить переменной типа значения. (Исключением из этого правила является обнуляемый тип, который представляет собой специальный тип структуры, инкапсулирующий тип значения и допускающий пустое значение (null). Подробнее об этом — в главе 20.) Следовательно, в отсутствие ограничения такое присваивание было бы недопустимым, и код не подлежал бы компиляции. Это один из тех случаев, когда для обобщенного кода может оказаться очень важным различие между типами значений и ссылочными типами.

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

// Продемонстрировать наложение ограничения типа значения.
using System;

struct MyStruct {
    // ...
}

class MyClass {
    // ...
}

class Test<T> where T : struct {
    T obj;
    public Test(T x) {
        obj = x;
    }
    // ...
}

class ValueConstraintDemo {
    static void Main() {
        // Оба следующих объявления вполне допустимы.
        Test<MyStruct> х = new Test<MyStruct>(new MyStruct());

        Test<int> у = new Test<int>(10);

        // А следующее объявление недопустимо!
        // Test<MyClass> z = new Test<MyClass>(new MyClass());
    }
}

В этом примере кода класс Test объявляется следующим образом.

class Test<T> where Т : struct {

На параметр типа Т в классе Test накладывается ограничение struct, и поэто­ му к нему могут быть привязаны только аргументы типа значения. Это означает, что объявления Test и Test вполне допустимы, тогда как объявление Test недопустимо. Для того чтобы убедиться в этом, удалите символы ком­ ментария в начале последней строки приведенного выше кода и перекомпилируйте его. В итоге вы получите сообщение об ошибке во время компиляции.

Установление связи между двумя параметрами типа с помощью ограничения

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

class Gen<T; V> where V : T {

В этом объявлении оператор where уведомляет компилятор о том, что аргумент типа, привязанный к параметру типа V, должен быть таким же, как и аргумент типа, привязанный к параметру типа Т, или же наследовать от него. Если подобная связь отсутствует при объявлении объекта типа Gen, то во время компиляции возникнет ошибка. Такое ограничение на параметр типа называется неприкрытым ограничением типа. В приведенном ниже примере демонстрируется наложение этого ограничения.

// Установить связь между двумя параметрами типа.
using System;

class А {
    // ...
}

class В : А {
    // ...
}

// Здесь параметр типа V должен наследовать от параметра типа Т.
class Gen<T, V> where V : T {
    // ...
}

class NakedConstraintDemo {
    static void Main() {
        // Это объявление вполне допустимо, поскольку
        // класс В наследует от класса А.
        GerKA, В> х = new Gen<A, В>();

        // А это объявление недопустимо, поскольку
        // класс А не наследует от класса В.
        // Gen<B, А> у = new Gen<B, А>();
    }
}

Обратите внимание на то, что класс В наследует от класса А. Проанализируем далее оба объявления объектов класса Gen в методе Main(). Как следует из комментария к первому объявлению

Gen<A, В> х = new Gen<A, В>();

оно вполне допустимо, поскольку класс В наследует от класса А. Но второе объявление

// Gen<B, А> у = new Gen<B, А>();

недопустимо, поскольку класс А не наследует от класса В.

Применение нескольких ограничений

С параметром типа может быть связано несколько ограничений. В этом случае ограничения указываются списком через запятую. В этом списке первым должно быть указано ограничение class либо struct, если оно присутствует, или же ограничение на базовый класс, если оно накладывается. Указывать ограничения class или struct одновременно с ограничением на базовый класс не разрешается. Далее по списку должно следовать ограничение на интерфейс, а последним по порядку — ограничение new(). Например, следующее объявление считается вполне допустимым.

class Gen<T> where Т : MyClass, IMyInterface, new() {
    // ...

В данном случае параметр типа Т должен быть заменен аргументом типа, наследу­ ющим от класса MyClass, реализующим интерфейс IMyInterface и использующим конструктор без параметра.

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

// Использовать несколько операторов where.
using System;

// У класса Gen имеются два параметра типа, и на оба накладываются
// ограничения с помощью отдельных операторов where.
class Gen<T, V> where T : class
where V : struct {
    T ob1;
    V ob2;

    public Gen(T t, V v) {
        ob1 = t;
        ob2 = v;
    }
}

class MultipleConstraintDemo {
    static void Main() {
        // Эта строка кода вполне допустима, поскольку
        // string — это ссылочный тип, a int — тип значения.
        Gen<string, int> obj = new Gen<string, int>("тест", 11);

        // А следующая строка кода недопустима, поскольку
        // bool не относится к ссылочному типу.
        // Gen<bool, int> obj = new Gencbool, int>(true, 11);
    }
}

В данном примере класс Gen принимает два аргумента с ограничениями, накла­ дываемыми с помощью отдельных операторов where. Обратите особое внимание на объявление этого класса.

class Gen<T, V> where T : class
    where V : struct {

Как видите, один оператор where отделяется от другого только пробелом. Другие знаки препинания между ними не нужны и даже недопустимы.

Получение значения, присваиваемого параметру типа по умолчанию

Как упоминалось выше, при написании обобщенного кода иногда важно провести различие между типами значений и ссылочными типами. Такая потребность возника­ ет, в частности, в том случае, если переменной параметра типа должно быть присвое­ но значение по умолчанию. Для ссылочных типов значением по умолчанию является null, для неструктурных типов значений — 0 или логическое значение false, если это тип bool, а для структур типа struct — объект соответствующей структуры с полями, установленными по умолчанию. В этой связи возникает вопрос: какое значение следует присваивать по умолчанию переменной параметра типа: null, 0 или нечто другое? Например, если в следующем объявлении класса Test:

class Test<T> {
    Т obj;
    // ...

переменной obj требуется присвоить значение по умолчанию, то какой из двух вариантов

obj = null; // подходит только для ссылочных типов

или

obj = 0; // подходит только для числовых типов и
        // перечислений, но не для структур

следует выбрать? Для разрешения этой дилеммы можно воспользоваться еще одной формой оператора default, приведенной ниже.

default(тип)

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

Ниже приведен короткий пример, демонстрирующий данную форму оператора default.

// Продемонстрировать форму оператора default.
using System;

class MyClass {
    // ...
}

// Получить значение, присваиваемое параметру типа Т по умолчанию.
class Test<T> {
    public Т obj;
    public Test() {
        // Следующий оператор годится только для ссылочных типов.
        // obj = null; // не годится

        // Следующий оператор годится только для типов значений.
        // obj = 0; // не годится

        // А этот оператор годится как для ссылочных типов,
        // так и для типов значений.
        obj = default(T); // Годится!
    }
    // ...
}

class DefaultDemo {
    static void Main() {
        // Сконструировать объект класса Test, используя ссылочный тип.
        Test<MyClass> х = new Test<MyClass>();

        if(x.obj == null)
            Console.WriteLine("Переменная x.obj имеет пустое значение <null>.");

        // Сконструировать объект класса Test, используя тип значения.
        Test<int> у = new Test<int>();

        if(у.obj == 0)
            Console.WriteLine("Переменная у.obj имеет значение 0.");
    }
}

Вот к какому результату приводит выполнение этого кода.

Переменная x.obj имеет пустое значение <null>.
Переменная у.obj имеет значение 0.

Обобщенные структуры

В C# разрешается создавать обобщенные структуры. Синтаксис для них такой же, как и для обобщенных классов. В качестве примера ниже приведена программа, в ко­ торой создается обобщенная структура XY для хранения координат X, Y.

// Продемонстрировать применение обобщенной структуры.
using System;

// Эта структура является обобщенной.
struct XY<T> {
    Т х;
    Т у;

    public XY(Т а, Т b) {
        х = а;
        У = b;
    }

    public Т X {
        get { return х; }
        set { х = value; }
    }

    public T Y {
        get { return y; }
        set { у = value; }
    }
}

class StructTest {
    static void Main() {
        XY<int> xy = new XY<int>(10, 20);
        XY<double> xy2 = new XY<double>(88.0, 99.0);

        Console.WriteLine(xy.X + ", " + xy.Y);

        Console.WriteLine(xy2.X + ", " + xy2.Y);
    }
}

При выполнении этой программы получается следующий результат.

10, 20
88, 99

Как и на обобщенные классы, на обобщенные структуры могут накладываться огра­ ничения. Например, на аргументы типа в приведенном ниже варианте структуры XY накладывается ограничение типа значения.

struct XY<T> where Т : struct {
// ...

Создание обобщенного метода

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

Рассмотрим для начала простой пример. В приведенной ниже программе объяв­ ляется необобщенный класс ArrayUtils, а в нем — статический обобщенный метод CopyInsert(). Этот метод копирует содержимое одного массива в другой, вводя по ходу дела новый элемент в указанном месте. Метод CopyInsert() можно использо­ вать вместе с массивами любого типа.

// Продемонстрировать применение обобщенного метода.
using System;

// Класс обработки массивов. Этот класс не является обобщенным.
class ArrayUtils {
    // Копировать массив, вводя по ходу дела новый элемент.
    // Этот метод является обобщенным.
    public static bool CopyInsert<T> (Т e, uint idx,
                                    T[] src, T[] target) {
        // Проверить, насколько велик массив.
        if(target.Length < src.Length+1)
            return false;

        // Скопировать содержимое массива src в целевой массив,
        // попутно введя значение е по индексу idx.
        for(int i=0, j=0; i < src.Length; i++, j++) {
            if(i == idx) {
                target[j] = e;
                j++;
            }
            target[j] = src[i];
        }
        return true;
    }
}

class GenMethDemo {
    static void Main() {
        int[] nums = { 1, 2, 3 };
        int[] nums2 = new int[4];

        // Вывести содержимое массива nums.
        Console.Write("Содержимое массива nums: ");
        foreach(int x in nums)
            Console.Write(х + " ");

        Console.WriteLine();

        // Обработать массив типа int.
        ArrayUtils.Copylnsert(99, 2, nums, nums2);

        // Вывести содержимое массива nums2.
        Console.Write("Содержимое массива nums2: ");
        foreach(int x in nums2)
            Console.Write(x + " ");

        Console.WriteLine();

        //А теперь обработать массив строк, используя метод copyInsert.
        string[] strs = {"Обобщения", "весьма", "эффективны."};
        string[] strs2 = new string[4];

        // Вывести содержимое массива strs.
        Console.Write("Содержимое массива strs: ");
        foreach(string s in strs)
            Console.Write(s + " ");

        Console.WriteLine();

        // Ввести элемент в массив строк.
        ArrayUtils.Copylnsert("в С#", 1, strs, strs2);

        // Вывести содержимое массива strs2.
        Console.Write("Содержимое массива strs2: ");
        foreach(string s in strs2)
            Console.Write(s + " ");

        Console.WriteLine();

        // Этот вызов недопустим, поскольку первый аргумент
        // относится к типу double, а третий и четвертый
        // аргументы обозначают элементы массивов типа int.
        // ArrayUtils.Copylnsert(0.01, 2, nums, nums2);
    }
}

Вот к какому результату приводит выполнение этой программы.

Содержимое массива nums: 1 2 3
Содержимое массива nums2: 1 2 99 3
Содержимое массива strs: Обобщения весьма эффективны.
Содержимое массива strs2: Обобщения в C# весьма эффективны.

Внимательно проанализируем метод CopyInsert(). Прежде всего обратите вни­ мание на объявление этого метода в следующей строке кода.

public static bool CopyInsert<T>(Т e, uint idx,
                                T[] src, T[] target) {

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

Далее обратите внимание на то, что метод CopyInsert() вызывается в методе Main() с помощью обычного синтаксиса и без указания аргументов типа. Дело в том, что типы аргументов различаются автоматически, а тип Т соответственно подстраи­ вается. Этот процесс называется выводимостью типов. Например, в первом вызове дан­ ного метода

ArrayUtils.CopyInsert(99, 2, nums, nums2);

тип T становится типом int, поскольку числовое значение 99 и элементы массивов nums и nums2 относятся к типу int. А во втором вызове данного метода используются строковые типы, и поэтому тип Т заменяется типом string.

А теперь обратите внимание на приведенную ниже закомментированную строку кода.

// ArrayUtils.CopyInsert(0.01, 2, nums, nums2);

Если удалить символы комментария в начале этой строки кода и затем попытаться перекомпилировать программу, то будет получено сообщение об ошибке. Дело в том, что первый аргумент в данном вызове метода CopyInsert() относится к типу double, а третий и четвертый аргументы обозначают элементы массивов nums и nums2 типа int. Но все эти аргументы типа должны заменить один и тот же параметр типа Т, а это приведет к несоответствию типов и, как следствие, к ошибке во время компиля­ ции. Подобная возможность соблюдать типовую безопасность относится к одним из самых главных преимуществ обобщенных методов.

Синтаксис объявления метода CopyInsert() может быть обобщен. Ниже приве­ дена общая форма объявления обобщенного метода.

возвращаемый_тип имя_метода<список_параметров_типа>(список_параметров) { // ...

В любом случае список_параметров_типа обозначает разделяемый запятой спи­ сок параметров типа. Обратите внимание на то, что в объявлении обобщенного метода список параметров типа следует после имени метода.

Вызов обобщенного метода с явно указанными аргументами типа

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

ArrayUtils.CopyInsert<string>("В С#", 1, strs, strs2);

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

Применение ограничений в обобщенных методах

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

public static bool CopyInsert<T>(Т e, uint idx,
                                T[] src, T[] target) where T : class {

Если попробовать применить этот вариант в предыдущем примере программы об­ работки массивов, то приведенный ниже вызов метода CopyInsert() не будет ском­ пилирован, поскольку int является типом значения, а не ссылочным типом.

// Теперь неправильно, поскольку параметр Т должен быть ссылочного типа!
ArrayUtils.Copylnsert(99, 2, nums, nums2); // Теперь недопустимо!

Обобщенные делегаты

Как и методы, делегаты также могут быть обобщенными. Ниже приведена общая форма объявления обобщенного делегата.

delegate возврашдемый_тип имя_делегата<список_параметров_типа>(список_аргументов);

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

В приведенном ниже примере программы демонстрируется применение делегата SomeOp с одним параметром типа Т. Этот делегат возвращает значение типа Т и при­ нимает аргумент типа Т.

// Простой пример обобщенного делегата.
using System;

// Объявить обобщенный делегат.
delegate Т SomeOp<T>(T v);
class GenDelegateDemo {
    // Возвратить результат суммирования аргумента.
    static int Sum(int v) {
        int result = 0;
        for(int i=v; i>0; i--)
        result += i;
        return result;
    }

    // Возвратить строку, содержащую обратное значение аргумента.
    static string Reflect(string str) {
        string result = "";
        foreach(char ch in str)
            result = ch + result;
        return result;
    }

    static void Main() {
        // Сконструировать делегат типа int.
        SomeOp<int> intDel = Sum;
        Console.WriteLine(intDel(3));
        // Сконструировать делегат типа string.
        SomeOp<string> strDel = Reflect;
        Console.WriteLine(strDel("Привет"));
    }
}

Эта программа дает следующий результат.

6
тевирП

Рассмотрим эту программу более подробно. Прежде всего обратите внимание на следующее объявление делегата SomeOp.

delegate Т SomeOp<T>(Т v);

Как видите, тип Т может служить в качестве возвращаемого типа, несмотря на то, что параметр типа Т указывается после имени делегата SomeOp.

Далее в классе GenDelegateDemo объявляются методы Sum() и Reflect(), как по­ казано ниже.

static int Sum(int v) {
static string Reflect(string str) {

Метод Sum() возвращает результат суммирования целого значения, передаваемого в качестве аргумента, а метод Reflect() — символьную строку, которая получается об­ ращенной по отношению к строке, передаваемой в качестве аргумента.

В методе Main() создается экземпляр intDel делегата, которому присваивается ссылка на метод Sum().

SomeOp<int> intDel = Sum;

Метод Sum() принимает аргумент типа int и возвращает значение типа int, поэ­ тому он совместим с целочисленным экземпляром делегата SomeOp.

Аналогичным образом создается экземпляр strDel делегата, которому присваива­ ется ссылка на метод Reflect().

SomeOp<string> strDel = Reflect;

Метод Reflect() принимает аргумент типа string и возвращает результат типа string, поэтому он совместим со строковым экземпляром делегата SomeOp. В силу присущей обобщениям типовой безопасности обобщенным делегатам нель­ зя присваивать несовместимые методы. Так, следующая строка кода оказалась бы оши­ бочной в рассматриваемой здесь программе.

SomeOp<int> intDel = Reflect; // Ошибка!

Ведь метод Reflect() принимает аргумент типа string и возвращает результат типа string, а следовательно, он несовместим с целочисленным экземпляром деле­ гата SomeOp.

Обобщенные интерфейсы

Помимо обобщенных классов и методов, в C# допускаются обобщенные интерфей­ сы. Такие интерфейсы указываются аналогично обобщенным классам. Ниже приведен измененный вариант примера из главы 12, демонстрирующего интерфейс ISeries. (Напомним, что ISeries является интерфейсом для класса, генерирующего последо­ вательный ряд числовых значений.) Тип данных, которым оперирует этот интерфейс, теперь определяется параметром типа.

// Продемонстрировать применение обобщенного интерфейса.
using System;

public interface ISeries<T> {
    T GetNext();// возвратить следующее по порядку число
    void Reset(); // генерировать ряд последовательных чисел с самого начала
    void SetStart(Т v); // задать начальное значение
}

// Реализовать интерфейс ISeries.
class ByTwos<T> : ISeries<T> {
    T start;
    T val;

    // Этот делегат определяет форму метода, вызываемого для генерирования
    // очередного элемента в ряду последовательных значений.
    public delegate Т IncByTwo(T v);

    // Этой ссылке на делегат будет присвоен метод,
    // передаваемый конструктору класса ByTwos.
    IncByTwo incr;
    public ByTwos(IncByTwo incrMeth) {
        start = default(T);
        val = default(T);
        incr = incrMeth;
    }

    public T GetNext() {
        val = incr(val);
        return val;
    }

    public void Reset() {
        val = start;
    }

    public void SetStart(T v) {
        start = v;
        val = start;
    }
}

class ThreeD {
    public int x, y, z;
    public ThreeD(int a, int b, int c) {
        x = a;
        У = b;
        z = c;
    }
}

class GenlntfDemo {
    // Определить метод увеличения на два каждого
    // последующего значения типа int.
    static int IntPlusTwo (int v) {
        return v + 2;
    }

    // Определить метод увеличения на два каждого
    // последующего значения типа double.
    static double DoublePlusTwo (double v) {
        return v + 2.0;
    }

    // Определить метод увеличения на два каждого
    // последующего значения координат объекта типа ThreeD.
    static ThreeD ThreeDPlusTwo(ThreeD v) {
        if(v==null) return new ThreeD(0, 0, 0);
        else return new ThreeD(v.x + 2, v.y + 2, v.z + 2);
    }

    static void Main() {
        // Продемонстрировать генерирование
        // последовательного ряда значений типа int.
        ByTwos<int> intBT = new ByTwos<int>(IntPlusTwo);
        for(int i=0; i < 5; i++)
            Console.Write(intBT.GetNext() + " ");
        Console.WriteLine();

        // Продемонстрировать генерирование
        // последовательного ряда значений типа double.
        ByTwos<double> dblBT =
            new ByTwos<double>(DoublePlusTwo);
        dblBT.SetStart(11.4);
        for (int i=0; i < 5; i++)
            Console.Write(dblBT.GetNext() + " ");
        Console.WriteLine();

        // Продемонстрировать генерирование последовательного ряда
        // значений координат объекта типа ThreeD.
        ByTwos<ThreeD> ThrDBT = new ByTwos<ThreeD>(ThreeDPlusTwo);
        ThreeD coord;
        for(int i=0; i < 5; i++) {
            coord = ThrDBT.GetNext();
            Console.Write(coord.x + "," +
            coord.у + "," +
            coord.z + " ");
        }
        Console.WriteLine();
    }
}

Этот код выдает следующий результат.

2 4 6 8 10
13.4 15.4 17.4 19.4 21.4
0,0,0 2,2,2 4,4,4 6,6,6 8,8,8

В данном примере кода имеется ряд любопытных моментов. Прежде всего обрати­ те внимание на объявление интерфейса ISeries в следующей строке кода.

public interface ISeries<T> {

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

А теперь обратите внимание на следующее объявление класса ByTwos, реализую­ щего интерфейс Iseries.

class ByTwos<T> : ISeries<T> {

Параметр типа Т указывается не только при объявлении класса ByTwos, но и при объявлении интерфейса ISeries. И это очень важно. Ведь класс, реализующий обоб­ щенный вариант интерфейса, сам должен быть обобщенным. Так, приведенное ниже объявление недопустимо, поскольку параметр типа Т не определен.

class ByTwos : ISeries<T> { // Неверно!

Аргумент типа, требующийся для интерфейса ISeries, должен быть передан клас­ су ByTwos. В противном случае интерфейс никак не сможет получить аргумент типа. Далее переменные, хранящие текущее значение в последовательном ряду (val) и его начальное значение (start), объявляются как объекты обобщенного типа Т. По­ сле этого объявляется делегат IncByTwo. Этот делегат определяет форму метода, ис­ пользуемого для увеличения на два значения, хранящегося в объекте типа Т. Для того чтобы в классе ByTwos могли обрабатываться данные любого типа, необходимо каким- то образом определить порядок увеличения на два значения каждого типа данных. Для этого конструктору класса ByTwos передается ссылка на метод, выполняющий увеличение на два. Эта ссылка хранится в переменной экземпляра делегата incr. Ког­ да требуется сгенерировать следующий элемент в последовательном ряду, этот метод вызывается с помощью делегата incr.

А теперь обратите внимание на класс ThreeD. В этом классе инкапсулируются ко­ ординаты трехмерного пространства (X,Z,Y). Его назначение — продемонстрировать обработку данных типа класса в классе ByTwos.

Далее в классе GenIntfDemo объявляются три метода увеличения на два для объек­ тов типа int, double и ThreeD. Все эти методы передаются конструктору класса ByTwos при создании объектов соответствующих типов. Обратите особое внимание на приведенный ниже метод ThreeDPlusTwo().

// Определить метод увеличения на два каждого
// последующего значения координат объекта типа ThreeD.
static ThreeD ThreeDPlusTwo(ThreeD v) {
    if(v==null) return new ThreeD(0, 0, 0);
    else return new ThreeD(v.x + 2, v.y + 2, v.z + 2);
}

В этом методе сначала проверяется, содержит ли переменная экземпляра v пустое значение (null). Если она содержит это значение, то метод возвращает новый объект типа ThreeD со всеми обнуленными полями координат. Ведь дело в том, что перемен­ ной v по умолчанию присваивается значение типа default(Т) в конструкторе класса ByTwos. Это значение оказывается по умолчанию нулевым для типов значений и пу­ стым для типов ссылок на объекты. Поэтому если предварительно не был вызван ме­ тод SetStart(), то перед первым увеличением на два переменная v будет содержать пустое значение вместо ссылки на объект. Это означает, что для первого увеличения на два требуется новый объект.

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

public interface ISeries<T> where T : class {

Если реализуется именно такой вариант интерфейса ISeries, в реализующем его классе следует указать то же самое ограничение на параметр типа Т, как показано ниже.

class ByTwos<T> : ISeries<T> where T : class {

В силу ограничения ссылочного типа этот вариант интерфейса ISeries нельзя применять к типам значений. Поэтому если реализовать его в рассматриваемом здесь примере программы, то допустимым окажется только объявление ByTwos, но не объявления ByTwos и ByTwos.

Сравнение экземпляров параметра типа

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

// Не годится!
public static bool IsIn<T>(T what, T[] obs) {
    foreach(T v in obs)
    if(v == what) // Ошибка!
        return true;
    return false;
}

К сожалению, эта попытка не пройдет. Ведь параметр Т относится к обобщенному типу, и поэтому компилятору не удастся выяснить, как сравнивать два объекта. Требу­ ется ли для этого поразрядное сравнение или же только сравнение отдельных полей? А возможно, сравнение ссылок? Вряд ли компилятор сможет найти ответы на эти во­ просы. Правда, из этого положения все же имеется выход.

Для сравнения двух объектов параметра обобщенного типа они должны реализовы­ вать интерфейс IComparable или IComparable и/или интерфейс IEquatable. В обоих вариантах интерфейса IComparable для этой цели определен метод CompareTo(), а в интерфейсе IEquatable — метод Equals(). Разновидности интерфейса IComparable предназначены для применения в тех случаях, когда тре­ буется определить относительный порядок следования двух объектов. А интерфейс IEquatable служит для определения равенства двух объектов. Все эти интерфейсы определены в пространстве имен System и реализованы во встроенных в C# типах дан­ ных, включая int, string и double. Но их нетрудно реализовать и для собственных создаваемых классов. Итак, начнем с обобщенного интерфейса IEquatable.

Интерфейс IEquatable объявляется следующим образом.

public interface IEquatable<T>

Сравниваемый тип данных передается ему в качестве аргумента типа Т. В этом ин­ терфейсе определяется метод Equals(), как показано ниже.

bool Equals(Т other)

В этом методе сравниваются взывающий объект и другой объект, определяемый параметром other. В итоге возвращается логическое значение true, если оба объекта равны, а иначе — логическое значение false.

В ходе реализации интерфейса IEquatable обычно требуется также переопре­ делять методы GetHashCode() и Equals(Object), определенные в классе Object, чтобы они оказались совместимыми с конкретной реализацией метода Equals(). Ниже приведен пример программы, в которой демонстрируется исправленный вари­ ант упоминавшегося ранее метода IsIn().

// Требуется обобщенный интерфейс IEquatable<T>.
public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T> {
    foreach(T v in obs)
    if(v.Equals(what)) // Применяется метод Equals().
        return true;
    return false;
}

Обратите внимание в приведенном выше примере на применение следующего ограничения.

where Т : IEquatable<T>

Это ограничение гарантирует, что только те типы, в которых реализован интерфейс IEquatable, являются действительными аргументами типа для метода IsIn(). Вну­ три этого метода применяется метод Equals(), который определяет равенство одного объекта другому.

Для определения относительного порядка следования двух элементов применяется интерфейс IComparable. У этого интерфейса имеются две формы: обобщенная и не­ обобщенная. Обобщенная форма данного интерфейса обладает преимуществом обе­ спечения типовой безопасности, и поэтому мы рассмотрим здесь именно ее. Обоб­ щенный интерфейс IComparable объявляется следующим образом.

public interface IComparable<T>

Сравниваемый тип данных передается ему в качестве аргумента типа Т. В этом ин­ терфейсе определяется метод CompareTo(), как показано ниже.

int CompareTo(Т other)

В этом методе сравниваются вызывающий объект и другой объект, определяемый параметром other. В итоге возвращается нуль, если вызывающий объект оказывается больше, чем объект other; и отрицательное значение, если вызывающий объект ока­ зывается меньше, чем объект other.

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

Ниже приведен пример применения обобщенного интерфейса IComparable. В этом примере вызывается метод InRange(), возвращающий логическое значение true, если объект оказывается среди элементов отсортированного массива.

// Требуется обобщенный интерфейс IComparable<T>. В данном методе
// предполагается, что массив отсортирован. Он возвращает логическое
// значение true, если значение параметра what оказывается среди элементов
// массива, передаваемых параметру obs.
public static bool InRange<T>(T what, T[] obs) where T : IComparable<T> {
    if(what.CompareTo(obs[0]) < 0 ||
        what.CompareTo(obs[obs.Length-1]) > 0) return false;
    return true;
}

В приведенном ниже примере программы демонстрируется применение обоих методов IsIn() и InRange() на практике.

// Продемонстрировать применение обобщенных
// интерфейсов IComparable<T> и IEquatable<T>.
using System;

// Теперь в классе MyClass реализуются обобщенные
// интерфейсы IComparable<T> и IEquatable<T>.
class MyClass : IComparable<MyClass>, IEquatable<MyClass> {
    public int Val;
    public MyClass(int x) { Val = x; }

    // Реализовать обобщенный интерфейс IComparable<T>.
    public int CompareTo(MyClass other) {
        return Val - other.Val; // Now, no cast is needed.
    }

    // Реализовать обобщенный интерфейс IEquatable<T>.
    public bool Equals(MyClass other) {
        return Val == other.Val;
    }

    // Переопределить метод Equals(Object).
    public override bool Equals(Object obj) {
        if(obj is MyClass)
            return Equals((MyClass) obj);
        return false;
    }

    // Переопределить метод GetHashCode().
    public override int GetHashCode() {
        return Val.GetHashCode();
    }
}

class CompareDemo {
    // Требуется обобщенный интерфейс IEquatable<T>.
    public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T> {
        foreach(T v in obs)
            if(v.Equals(what)) // Применяется метод Equals()
                return true;
        return false;
    }

    // Требуется обобщенный интерфейс IComparable<T>. В данном методе
    // предполагается, что массив отсортирован. Он возвращает логическое
    // значение true, если значение параметра what оказывается среди элементов
    // массива, передаваемых параметру obs.
    public static bool InRange<T>(T what, T[] obs) where T : IComparable<T> {
        if(what.CompareTo(obs[0]) < 0 ||
            what.CompareTo(ob?[obs.Length-1]) > 0) return false;
        return true;
    }

    // Продемонстрировать операции сравнения.
    static void Main() {
        // Применить метод IsIn() к данным типа int.
        int[] nums = { 1, 2, 3, 4, 5 };

        if(IsIn(2, nums))
            Console.WriteLine("Найдено значение 2.");

        if(IsIn(99, nums))
            Console.WriteLine("He подлежит выводу.");

        // Применить метод IsIn() к объектам класса MyClass.
        MyClass[] mcs = { new MyClass(1), new MyClass(2),
                        new MyClass(3), new MyClass(4) );

        if(IsIn(new MyClass()), mcs))
            Console.WriteLine("Найден объект MyClass()).");

        if(IsIn(new MyClass(99), mcs))
            Console.WriteLine("He подлежит выводу.");

        // Применить метод InRange() к данным типа int.
        if(InRange(2, nums))
            Console.WriteLine("Значение 2 находится в границах массива nums.");
        if(InRange(1, nums))
            Console.WriteLine("Значение 1 находится в границах массива nums.");
        if(InRange(5, nums))
            Console.WriteLine("Значение 5 находится в границах массива nums.");
        if(!InRange(0, nums))
            Console.WriteLine("Значение 0 HE находится в границах массива nums.");
        if(!InRange(6, nums))
            Console.WriteLine("Значение 6 HE находится в границах массива nums.");

        // Применить метод InRange() к объектам класса MyClass.
        if(InRange(new MyClass(2), mcs))
            Console.WriteLine("Объект MyClass(2) находится в границах массива nums.");
        if(InRange(new MyClass(1), mcs))
            Console.WriteLine("Объект MyClass(1) находится " +
                            "в границах массива nums.");
        if(InRange(new MyClass(4), mcs))
            Console.WriteLine("Объект MyClass(4) находится " +
                            "в границах массива nums.");
        if(!InRange(new MyClass(0), mcs))
            Console.WriteLine("Объект MyClass(0) HE " +
                            "находится в границах массива nums.");
        if (!InRange(new MyClass(5), mcs))
            Console.WriteLine("Объект MyClass (5) HE " +
                            "находится в границах массива nums.");
    }
}

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

Найдено значение 2.
Найден объект MyClass(3).
Значение 2 находится в границах массива nums.
Значение 1 находится в границах массива nums.
Значение 5 находится в границах массива nums.
Значение 0 НЕ находится в границах массива nums
Значение 6 НЕ находится в границах массива nums
Объект MyClass(2) находится в границах массива nums.
Объект MyClass(1) находится в границах массива nums.
Объект MyClass(4) находится в границах массива nums.
Объект MyClass(0) НЕ находится в границах массива nums.
Объект MyClass(5) НЕ находится в границах массива nums.

ПРИМЕЧАНИЕ Если параметр типа обозначает ссылку или ограничение на базовый класс, то к экземплярам объектов, определяемых таким параметром типа, можно применять операторы == и !=, хотя они проверяют на равенство только ссылки. А для сравнения значений придется реализовать интер­ фейс IComparable или же обобщенные интерфейсы IComparable и lEquatable.

Иерархии обобщенных классов

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

Применение обобщенного базового класса

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

// Простая иерархия обобщенных классов.
using System;

// Обобщенный базовый класс.
class Gen<T> {
    Т ob;

    public Gen(T о) {
        ob = о;
    }

    // Возвратить значение переменной ob.
    public Т GetOb() {
        return ob;
    }
}

// Класс, производный от класса Gen.
class Gen2<T> : Gen<T> {
    public Gen2(T o) : base(o) {
        // ...
    }
}

class GenHierDemo {
    static void Main() {
        Gen2<string> g2 = new Gen2<string>("Привет");
        Console.WriteLine(g2.GetOb());
    }
}

В этой иерархии класс Gen2 наследует от обобщенного класса Gen. Обратите вни­ мание на объявление класса Gen2 в следующей строке кода.

class Gen2<T> : Gen<T> {

Параметр типа Т указывается в объявлении класса Gen2 и в то же время передается классу Gen. Это означает, что любой тип, передаваемый классу Gen2, будет передавать­ ся также классу Gen. Например, в следующем объявлении:

Gen2<string> g2 = new Gen2<string>("Привет");

параметр типа string передается классу Gen. Поэтому переменная ob в той части класса Gen2, которая относится к классу Gen, будет иметь тип string. Обратите также внимание на то, что в классе Gen2 параметр типа Т не использует­ ся, а только передается вверх по иерархии базовому классу Gen. Это означает, что в производном классе следует непременно указывать параметры типа, требующиеся его обобщенному базовому классу, даже если этот производный класс не обязательно должен быть обобщенным.

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

// Пример добавления собственных параметров типа в производный класс.
using System;

// Обобщенный базовый класс.
class Gen<T> {
    Т ob; // объявить переменную типа Т

    // Передать конструктору ссылку типа Т.
    public Gen(T о) {
        ob = о;
    }

    // Возвратить значение переменной ob.
    public Т GetOb() {
        return ob;
    }
}

// Класс, производный от класса Gen. В этом классе
// определяется второй параметр типа V.
class Gen2<T, V> : Gen<T> {
    V ob2;

    public Gen2(T о, V o2) : base(о) {
        ob2 = o2;
    }

    public V GetObj2() {
        return ob2;
    }
}

// Создать объект класса Gen2.
class GenHierDemo2 {
    static void Main() {
        // Создать объект класса Gen2 с параметрами
        // типа string и int.
        Gen2<string, int> x =
            new Gen2<string, int>("Значение равно: ", 99);

        Console.Write(x.GetOb());
        Console.WriteLine(x.GetObj2());
    }
}

Обратите внимание на приведенное ниже объявление класса Gen2 в данном вари­ анте иерархии классов.

class Gen2<T, V> : Gen<T> {

В этом объявлении Т — это тип, передаваемый базовому классу Gen; а V — тип, ха­ рактерный только для производного класса Gen2. Он служит для объявления объекта оb2 и в качестве типа, возвращаемого методом GetObj2(). В методе Main() создается объект класса Gen2 с параметром Т типа string и параметром V типа int. Поэтому код из приведенного выше примера дает следующий результат.

Значение равно: 99

Обобщенный производный класс

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

// Пример необобщенного класса в качестве базового для
// обобщенного производного класса.
using System;

// Необобщенный базовый класс.
class NonGen {
    int num;

    public NonGen(int i) {
        num = i;
    }

    public int GetNum() {
        return num;
    }
}

// Обобщенный производный класс.
class Gen<T> : NonGen {
    T ob;

    public Gen(T о, int i) : base (i) {
        ob = o;
    }

    // Возвратить значение переменной ob.
    public T GetOb() {
        return ob;
    }
}

// Создать объект класса Gen.
class HierDemo3 {
    static void Main() {
        // Создать объект класса Gen с параметром типа string.
        Gen<String> w = new Gen<String>("Привет", 47);

        Console.Write(w.GetOb() + " ");
        Console.WriteLine(w.GetNum());
    }
}

Эта программа дает следующий результат.

Привет 47

В данной программе обратите внимание на то, как класс Gen наследует от класса NonGen в следующем объявлении.

class Gen<T> : NonGen {

Класс NonGen не является обобщенным, и поэтому аргумент типа для него не ука­ зывается. Это означает, что параметр т, указываемый в объявлении обобщенного про­ изводного класса Gen, не требуется для указания базового класса NonGen и даже не может в нем использоваться. Следовательно, класс Gen наследует от класса NonGen обычным образом, т.е. без выполнения каких-то особых условий.

Переопределение виртуальных методов в обобщенном классе

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

// Пример переопределения виртуального метода в обобщенном классе.
using System;

// Обобщенный базовый класс.
class Gen<T> {
    protected Т ob;

    public Gen(T о) {
        ob = о;
    }

    // Возвратить значение переменной ob. Этот метод является виртуальным.
    public virtual T GetOb() {
        Console.Write("Метод GetOb() из класса Gen" + " возвращает результат: ");
        return ob;
    }
}

// Класс, производный от класса Gen. В этом классе
// переопределяется метод GetOb().
class Gen2<T> : Gen<T> {
    public Gen2 (T o) : base(o) { }

        // Переопределить метод GetOb().
    public override T GetOb() {
        Console.Write("Метод GetOb() из класса Gen2" + " возвращает результат: ");
        return ob;
    }
}

// Продемонстрировать переопределение метода в обобщенном классе.
class OverrideDemo {
    static void Main() {
    // Создать объект класса Gen с параметром типа int.
    Gen<int> iOb = new Gen<int>(88);

    // Здесь вызывается вариант метода GetOb() из класса Gen.
    Console.WriteLine(iOb.GetOb());

    // А теперь создать объект класса Gen2 и присвоить
    // ссылку на него переменной iOb типа Gen<int>.
    iOb = new Gen2<int>(99);

    // Здесь вызывается вариант метода GetOb() из класса Gen2.
    Console.WriteLine(iOb.GetOb());
    }
}

Ниже приведен результат выполнения этой программы.

Метод GetOb() из класса Gen возвращает результат: 88
Метод GetOb() из класса Gen2 возвращает результат: 99

Как следует из результата выполнения приведенной выше программы, переопреде­ ляемый вариант метода GetOb() вызывается для объекта типа Gen2, а его вариант из базового класса вызывается для объекта типа Gen.

Обратите внимание на следующую строку кода.

iOb = new Gen2<int>(99);

Такое присваивание вполне допустимо, поскольку iOb является переменной типа Gen. Следовательно, она может ссылаться на любой объект типа Gen или же объект класса, производного от Gen, включая и Gen2. Разумеется, пере­ менную iOb нельзя использовать, например, для ссылки на объект типа Gen2, поскольку это может привести к несоответствию типов.

Перегрузка методов с несколькими параметрами типа

Методы, параметры которых объявляются с помощью параметров типа, могут быть перегружены. Но правила их перегрузки упрощаются по сравнению с методами без параметров типа. Как правило, метод, в котором параметр типа служит для указания типа данных параметра этого метода, может быть перегружен при условии, что сиг­ натуры обоих его вариантов отличаются. Это означает, что оба варианта перегружае­ мого метода должны отличаться по типу или количеству их параметров. Но типовые различия должны определяться не по параметру обобщенного типа, а исходя из ар­ гумента типа, подставляемого вместо параметра типа при конструировании объекта этого типа. Следовательно, метод с параметрами типа может быть перегружен таким образом, что он окажется пригодным не для всех возможных случаев, хотя и будет вы­ глядеть верно.

В качестве примера рассмотрим следующий обобщенный класс.

// Пример неоднозначности, к которой может привести
// перегрузка методов с параметрами типа.
//
// Этот код не подлежит компиляции.
using System;

// Обобщенный класс, содержащий метод Set(), перегрузка
// которого может привести к неоднозначности.
class Gen<T, V> {
    Т оb1;
    V ob2;
    // ...

    // В некоторых случаях эти два метода не будут
    // отличаться своими параметрами типа.
    public void Set(T о) {
        ob1 = о;
    }

    public void Set(V о) {
        ob2 = о;
    }
}

class AmbiguityDemo {
    static void Main() {
        Gen<int, double> ok = new Gen<int, double>();
        Gen<int, int> notOK = new Gen<int, int>();

        ok.Set(10); // верно, поскольку аргументы типа отличаются
        notOK.Set(10); // неоднозначно, поскольку аргументы ничем не отличаются!
    }
}

Рассмотрим приведенный выше код более подробно. Прежде всего обратите вни­ мание на то, что класс Gen объявляется с двумя параметрами типа: Т и V. В классе Gen метод Set() перегружается по параметрам типа Т и V, как показано ниже.

public void Set(T о) {
    ob1 = о;
}

public void Set(V о) {
    ob2 = о;
}

Такой подход кажется вполне обоснованным, поскольку типы Т и V ничем внешне не отличаются. Но подобная перегрузка таит в себе потенциальную неоднозначность. При таком объявлении класса Gen не соблюдается никаких требований к разли­ чению типов Т и V. Например, нет ничего принципиально неправильного в том, что объект класса Gen будет сконструирован так, как показано ниже.

Ger<int, int> notOK = new Gen<int, int>();

В данном случае оба типа, Т и V, заменяются типом int. В итоге оба варианта мето­ да Set() оказываются совершенно одинаковыми, что, разумеется, приводит к ошибке. Следовательно, при последующей попытке вызвать метод Set() для объекта notOK в методе Main() появится сообщение об ошибке вследствие неоднозначности во время компиляции.

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

Ковариантность и контравариантность в параметрах обобщенного типа

В главе 15 ковариантность и контравариантность были рассмотрены в связи с не­ обобщенными делегатами. Эта форма ковариантности и контравариантности по- прежнему поддерживается в С#, поскольку она очень полезна. Но в версии C# 4.0 воз­ можности ковариантности и контравариантности были расширены до параметров обобщенного типа, применяемых в обобщенных интерфейсах и делегатах. Ковариант­ ность и контравариантность применяется, главным образом, для рационального разре­ шения особых ситуаций, возникающих в связи с применением обобщенных интерфей­ сов и делегатов, определенных в среде .NET Framework. И поэтому некоторые интер­ фейсы и делегаты, определенные в библиотеке, были обновлены, чтобы использовать ковариантность и контравариантность параметров типа. Разумеется, преимуществами ковариантности и контравариантности можно также воспользоваться в интерфейсах и делегатах, создаваемых собственными силами. В этом разделе механизмы ковариант­ ности и контравариантности параметров типа поясняются на конкретных примерах.

Применение ковариантности в обобщенном интерфейсе

Применительно к обобщенному интерфейсу ковариантность служит средством, разрешающим методу возвращать тип, производный от класса, указанного в пара­ метре типа. В прошлом возвращаемый тип должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Кова­ риантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр ковариантного типа объявляется с помощью ключевого слова out, которое предваряет имя этого параметра.

Для того чтобы стали понятнее последствия применения ковариантности, обратим­ ся к конкретному примеру. Ниже приведен очень простой интерфейс IMyCoVarGenIF, в котором применяется ковариантность.

// В этом обобщенном интерфейсе поддерживается ковариантность.
public interface IMyCoVarGenIF<out Т> {
    Т GetObject();
}

Обратите особое внимание на то, как объявляется параметр обобщенного типа Т. Его имени предшествует ключевое слово out. В данном контексте ключевое слово out обозначает, что обобщенный тип Т является ковариантным. А раз он ковариантный, то метод GetObject() может возвращать ссылку на обобщенный тип Т или же ссылку на любой класс, производный от типа Т.

Несмотря на свою ковариантность по отношению к обобщенному типу Т, интер­ фейс IMyCoVarGenIF реализуется аналогично любому другому обобщенному интер­ фейсу. Ниже приведен пример реализации этого интерфейса в классе MyClass.

// Реализовать интерфейс IMyCoVarGenIF.
class MyClass<T> : IMyCoVarGenIF<T> {
    T obj;
    public MyClass(T v) { obj = v; }
    public T GetObject() { return obj; }
}

Обратите внимание на то, что ключевое слово out не указывается еще раз в выраже­ нии, объявляющем реализацию данного интерфейса в классе MyClass. Это не только не нужно, но и вредно, поскольку всякая попытка еще раз указать ключевое слово out будет расцениваться компилятором как ошибка.

А теперь рассмотрим следующую простую реализацию иерархии классов.

// Создать простую иерархию классов.
class Alpha {
    string name;
    public Alpha(string n) { name = n; }
    public string GetName() { return name; }
    // ...
}

class Beta : Alpha {
    public Beta(string n) : base(n) { }
    // ...
}

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

// Создать ссылку из интерфейса IMyCoVarGenIF на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии ковариантности, так и без нее.
IMyCoVarGenIF<Alpha> AlphaRef =
    new MyClass<Alpha>(new Alpha("Alpha #1"));
Console.WriteLine("Имя объекта, на который ссылается переменная AlphaRef: " +
AlphaRef.GetObject().GetName());

// А теперь создать объект MyClass<Beta> и присвоить его переменной AlphaRef.
// *** Эта строка кода вполне допустима благодаря ковариантности. ***
AlphaRef = new MyClass<Beta>(new Beta("Beta #1"));
Console.WriteLine("Имя объекта, на который теперь ссылается " +
                "переменная AlphaRef: " + AlphaRef.GetObject().GetName());

Прежде всего, переменной AlphaRef типа IMyCoVarGenIF в этом фраг­ менте кода присваивается ссылка на объект типа MyClass. Это вполне допу­ стимая операция, поскольку в классе MyClass реализуется интерфейс IMyCoVarGenIF, причем и в том, и в другом в качестве аргумента типа указывается Alpha. Далее имя объекта выводится на экран при вызове метода GetName() для объекта, возвращаемо­ го методом GetObject(). И эта операция вполне допустима, поскольку Alpha — это и тип, возвращаемый методом GetName(), и обобщенный тип Т. После этого пере­ менной AlphaRef присваивается ссылка на экземпляр объекта типа MyClass, что также допустимо, потому что класс Beta является производным от класса Alpha, а обобщенный тип Т — ковариантным в интерфейсе IMyCoVarGenIF. Если бы любое из этих условий не выполнялось, данная операция оказалась бы недопустимой. Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.

// Продемонстрировать ковариантность в обобщенном интерфейсе.
using System;

// Этот обобщенный интерфейс поддерживает ковариантность.
public interface IMyCoVarGenIF<out Т> {
    Т GetObject();
}

// Реализовать интерфейс IMyCoVarGenIF.
class MyClass<T> : IMyCoVarGenIF<T> {
    T obj;
    public MyClass(T v) { obj = v; }
    public T GetObject() { return obj; }
}

// Создать простую иерархию классов.
class Alpha {
    string name;
    public Alpha(string n) { name = n; }
    public string GetName() { return name; }
    // ...
}

class Beta : Alpha {
    public Beta(string n) : base(n) { }
    // ...
}

class VarianceDemo {
    static void Main() {
        // Создать ссылку из интерфейса IMyCoVarGenIF на объект типа MyClass<Alpha>.
        // Это вполне допустимо как при наличии ковариантности, так и без нее.
        IMyCoVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>(new Alpha("Alpha #1"));

        Console.WriteLine("Имя объекта, на который ссылается переменная " +
                        "AlphaRef: " + AlphaRef.GetObject().GetName());

        // А теперь создать объект MyClass<Beta> и присвоить его
        // переменной AlphaRef.
        // *** Эта строка кода вполне допустима благодаря ковариантности. ***
        AlphaRef = new MyClass<Beta>(new Beta("Beta #1"));

        Console.WriteLine("Имя объекта, на который теперь ссылается переменная " +
                        "AlphaRef: " + AlphaRef.GetObject().GetName());
    }
}

Результат выполнения этой программы выглядит следующим образом.

Имя объекта, на который ссылается переменная AlphaRef: Alpha #1
Имя объекта, на который теперь ссылается переменная AlphaRef: Beta #1

Следует особо подчеркнуть, что переменной AlphaRef можно присвоить ссылку на объект типа MyClass благодаря только тому, что обобщенный тип Т указан как ковариантный в интерфейсе IMyCoVarGenIF. Для того чтобы убедиться в этом, удалите ключевое слово out из объявления параметра обобщенного типа Т в интер­ фейсе IMyCoVarGenIF и попытайтесь скомпилировать данную программу еще раз. Компиляция завершится неудачно, поскольку строгая проверка на соответствие типов не разрешит теперь подобное присваивание.

Один обобщенный интерфейс может вполне наследовать от другого. Иными сло­ вами, обобщенный интерфейс с параметром ковариантного типа можно расширить, как показано ниже.

public interface IMyCoVarGenIF2<out Т> : IMyCoVarGenIF<T> {
    // ...
}

Обратите внимание на то, что ключевое слово out указано только в объявлении рас­ ширенного интерфейса. Указывать его в объявлении базового интерфейса не только не нужно, но и не допустимо. И последнее замечание: обобщенный тип Т допускается не указывать как ковариантный в объявлении интерфейса IMyCoVarGenIF2. Но при этом исключается ковариантность, которую может обеспечить расширенный интерфейс IMyCoVarGetIF. Разумеется, возможность сделать интерфейс IMyCoVarGenIF2 инва­ риантным может потребоваться в некоторых случаях его применения.

На применение ковариантности накладываются некоторые ограничения. Ковари­ антность параметра типа может распространяться только на тип, возвращаемый ме­ тодом. Следовательно, ключевое слово out нельзя применять в параметре типа, слу­ жащем для объявления параметра метода. Ковариантность оказывается пригодной только для ссылочных типов. Ковариантный тип нельзя использовать в качестве огра­ ничения в интерфейсном методе. Так, следующий интерфейс считается недопустимым.

public interface IMyCoVarGenIF2<out Т> {
    void M<V>() where V:T; // Ошибка, ковариантный тип T нельзя
    // использовать как ограничение
}

Применение контравариантности в обобщенном интерфейсе

Применительно к обобщенному интерфейсу контравариантность служит сред­ ством, разрешающим методу использовать аргумент, тип которого относится к базо­ вому классу, указанному в соответствующем параметре типа. В прошлом тип аргу­ мента метода должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Контравариантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр контрава­ риантного типа объявляется с помощью ключевого слова in, которое предваряет имя этого параметра.

Для того чтобы стали понятнее последствия применения ковариантности, вновь обратимся к конкретному примеру. Ниже приведен обобщенный интерфейс IMyContraVarGenIF контравариантного типа. В нем указывается контравариантный параметр обобщенного типа Т, который используется в объявлении метода Show().

// Это обобщенный интерфейс, поддерживающий контравариантность.
public interface IMyContraVarGenIF<in Т> {
    void Show(T obj);
}

Как видите, обобщенный тип Т указывается в данном интерфейсе как контрава­ риантный с помощью ключевого слова in, предшествующего имени его параметра. Обратите также внимание на то, что Т является параметром типа для аргумента obj в методе Show().

Далее интерфейс IMyContraVarGenIF реализуется в классе MyClass, как показано ниже.

// Реализовать интерфейс IMyContraVarGenIF.
class MyClass<T> : IMyContraVarGenIF<T> {
    public void Show(T x) { Console.WriteLine(x); }
}

В данном случае метод Show() просто выводит на экран строковое представление переменной х, получаемое в результате неявного обращения к методу ToString() из метода WriteLine().

После этого объявляется иерархия классов, как показано ниже.

// Создать простую иерархию классов.
class Alpha {
    public override string ToString() {
        return "Это объект класса Alpha.";
    }
    // ...
}

class Beta : Alpha {
    public override string ToString() {
        return "Это объект класса Beta.";
    }
    // ...
}

Ради большей наглядности классы Alpha и Beta несколько отличаются от анало­ гичных классов из предыдущего примера применения ковариантности. Обратите так­ же внимание на то, что метод ToString() переопределяется таким образом, чтобы возвращать тип объекта.

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

// Создать ссылку из интерфейса IMyContraVarGenIF<Alpha>
// на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии контравариантности, так и без нее.
IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>();

// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
// на объект типа MyClass<Beta>.
// И это вполне допустимо как при наличии контравариантности, так и без нее.
IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>();

// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
// на объект типа MyClass<Alpha>.
// *** Это вполне допустимо благодаря контравариантности. ***
IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>();

// Этот вызов допустим как при наличии контравариантности, так и без нее.
BetaRef.Show(new Beta());

// Присвоить переменную AlphaRef переменной BetaRef.
// *** Это вполне допустимо благодаря контравариантности. ***
BetaRef = AlphaRef;
BetaRef.Show(new Beta());

Прежде всего, обратите внимание на создание двух переменных ссылочного типа IMyContraVarGenIF, которым присваиваются ссылки на объекты класса MyClass, где параметры типа совпадают с аналогичными параметрами в интерфейсных ссылках. В первом случае используется параметр типа Alpha, а во втором — параметр типа Beta. Эти объявления не требуют контравариантности и допустимы в любом случае.

Далее создается переменная ссылочного типа IMyContraVarGenIF, но на этот раз ей присваивается ссылка на объект класса MyClass. Эта операция вполне допустима, поскольку обобщенный тип Т объявлен как контравариантный.

Как и следовало ожидать, следующая строка, в которой вызывается метод BetaRef. Show() с аргументом Beta, является вполне допустимой. Ведь Beta — это обобщен­ ный тип Т в классе MyClass и в то же время аргумент в методе Show().

В следующей строке переменная AlphaRef присваивается переменной BetaRef. Эта операция вполне допустима лишь в силу контравариантности. В данном случае переменная относится к типу MyClass, а переменная AlphaRef — к типу MyClass. Но поскольку Alpha является базовым классом для класса Beta, то такое преобразование типов оказывается допустимым благодаря контравариантности. Для того чтобы убедиться в необходимости контравариантности в рассматриваемом здесь примере, попробуйте удалить ключевое слово in из объявления обобщенного типа Т в интерфейсе IMyContraVarGenIF, а затем попытайтесь скомпилировать при­ веденный выше код еще раз. В результате появятся ошибки компиляции.

Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.

// Продемонстрировать контравариантность в обобщенном интерфейсе.
using System;

// Это обобщенный интерфейс, поддерживающий контравариантность.
public interface IMyContraVarGenIF<in Т> {
    void Show(T obj);
}

// Реализовать интерфейс IMyContraVarGenIF.
class MyClass<T> : IMyContraVarGenIF<T> {
    public void Show(T x) { Console.WriteLine(x); }
}

// Создать простую иерархию классов.
class Alpha {
    public override string ToString() {
        return "Это объект класса Alpha.";
    }
    // ...
}

class Beta : Alpha {
    public override string ToString() {
        return "Это объект класса Beta.";
    }
    // ...
}

class VarianceDemo {
    static void Main() {
        // Создать ссылку из интерфейса IMyContraVarGenIF<Alpha>
        // на объект типа MyClass<Alpha>.
        // Это вполне допустимо как при наличии контравариантности, так и без нее.
        IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>();

        // Создать ссылку из интерфейса IMyContraVarGenIF<beta>
        // на объект типа MyClass<Beta>.
        // И это вполне допустимо как при наличии контравариантности,
        // так и без нее.
        IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>();

        // Создать ссылку из интерфейса IMyContraVarGenIF<beta>
        // на объект типа MyClass<Alpha>.
        // *** Это вполне допустимо благодаря контравариантности. ***
        IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>();

        // Этот вызов допустим как при наличии контравариантности, так и без нее.
        BetaRef.Show(new Beta());

        // Присвоить переменную AlphaRef переменной BetaRef.
        // *** Это вполне допустимо благодаря контравариантности. ***
        BetaRef = AlphaRef;

        BetaRef.Show(new Beta());
    }
}

Выполнение этой программы дает следующий результат.

Это объект класса Beta.
Это объект класса Beta.

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

public interface IMyContraVarGenIF2<in Т> : IMyContraVarGenIF<T> {
    // ...
}

Следует иметь в виду, что указывать ключевое слово in в объявлении базового интерфейса не только не нужно, но и не допустимо. Более того, сам расширенный интерфейс IMyContraVarGenIF2 не обязательно должен быть контравариантным. Иными словами, обобщенный тип Т в интерфейсе IMyContraVarGenIF2 не требу­ ется модифицировать ключевым словом in. Разумеется, все преимущества, которые сулит контравариантность в интерфейсе IMyContraVarGen, при этом будут утрачены в интерфейсе IMyContraVarGenIF2.

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

Вариантные делегаты

Как пояснялось в главе 15, ковариантность и контравариантность поддерживается в необобщенных делегатах в отношении типов, возвращаемых методами, и типов, ука­ зываемых при объявлении параметров. Начиная с версии C# 4.0, возможности кова­ риантности и контравариантности были распространены и на обобщенные делегаты. Подобные возможности действуют таким же образом, как было описано выше в от­ ношении обобщенных интерфейсов.

Ниже приведен пример контравариантного делегата.

// Объявить делегат, контравариантный по отношению к обобщенному типу Т.
delegate bool SomeOp<in Т>(Т obj);

Этому делегату можно присвоить метод с параметром обобщенного типа Т или же класс, производный от типа Т.

А вот пример ковариантного делегата.

// Объявить делегат, ковариантный по отношению к обобщенному типу Т.
delegate Т AnotherOp<out Т, V>(V obj);

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

В следующем примере программы демонстрируется применение обоих разновид­ ностей вариантных делегатов на практике.

// Продемонстрировать конвариантность и контравариантность
// в обобщенных делегатах.
using System;

// Объявить делегат, контравариантный по отношению к обобщенному типу Т.
delegate bool SomeOp<in Т>(Т obj);

// Объявить делегат, ковариантный по отношению к обобщенному типу Т.
delegate Т AnotherOp<out Т, V>(V obj);

class Alpha {
    public int Val { get; set; }
    public Alpha(int v) { Val = v; }
}

class Beta : Alpha {
    public Beta (int v) : base(v) { }
}

class GenDelegateVarianceDemo {
    // Возвратить логическое значение true, если значение
    // переменной obj.Val окажется четным.
    static bool IsEven(Alpha obj) {
        if((obj.Val % 2) == 0) return true;
        return false;
    }

    static Beta ChangeIt(Alpha obj) {
        return new Beta(obj.Val +2);
    }

    static void Main() {
        Alpha objA = new Alpha(4);
        Beta objB = new Beta(9);

        // Продемонстрировать сначала контравариантность.
        // Объявить делегат SomeOp<Alpha> и задать для него метод IsEven.
        SomeOp<Alpha> checkIt = IsEven;

        // Объявить делегат SomeOp<Beta>.
        SomeOp<Beta> checkIt2;

        // А теперь- присвоить делегат SomeOp<Alpha> делегату SomeOp<Beta>.
        // *** Это допустимо только благодаря контравариантности. ***
        checklt2 = checkIt;

        // Вызвать метод через делегат.
        Console.WriteLine(checkIt2(objВ));

        // Далее, продемонстрировать контравариантность.
        // Объявить сначала два делегата типа AnotherOp.
        // Здесь возвращаемым типом является класс Beta,
        // а параметром типа - класс Alpha.
        // Обратите внимание на то, что для делегата modifyIt
        // задается метод ChangeIt.
        AnotherOp<Beta, Alpha> modifyIt = ChangeIt;

        // Здесь возвращаемым типом является класс Alpha,
        // а параметром типа — тот же класс Alpha.
        AnotherOp<Alpha, Alpha> modifyIt2;

        // А теперь присвоить делегат modifyIt делегату modifyIt2.
        // *** Это допустимо только благодаря ковариантности. ***
        modifyIt2 = modifyIt;

        // Вызвать метод и вывести результаты на экран.
        objA = modifyIt2(objА);
        Console.WriteLine(objA.Val);
    }
}

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

False
6

Каждая операция достаточно подробно поясняется в комментариях к данной про­ грамме. Следует особо подчеркнуть, для успешной компиляции программы в объяв­ лении обоих типов делегатов SomeOp and AnotherOp должны быть непременно ука­ заны ключевые слова in и out соответственно. Без этих модификаторов компиляция программы будет выполнена с ошибками из-за отсутствия неявных преобразований типов в означенных строках кода.

Создание экземпляров объектов обобщенных типов

Когда приходится иметь дело с обобщениями, то нередко возникает вопрос: не приведет ли применение обобщенного класса к неоправданному раздуванию кода? Ответ на этот вопрос прост: не приведет. Дело в том, что в C# обобщения реализованы весьма эффективным образом: новые объекты конструируемого типа создаются лишь по мере надобности. Этот процесс описывается ниже.

Когда обобщенный класс компилируется в псевдокод MSIL, он сохраняет все свои параметры типа в их обобщенной форме. А когда конкретный экземпляр класса по­ требуется во время выполнения программы, то JIT-компилятор сконструирует кон­ кретный вариант этого класса в исполняемом коде, в котором параметры типа заме­ няются аргументами типа. В каждом экземпляре с теми же самыми аргументами типа будет использоваться один и тот же вариант данного класса в исполняемом коде.

Так, если имеется некоторый обобщенный класс Gen, то во всех объектах типа Gen будет использоваться один и тот же исполняемый код данного класса. Сле­ довательно, раздувание кода исключается благодаря тому, что в программе создают­ ся только те варианты класса, которые действительно требуются. Когда же возникает потребность сконструировать объект другого типа, то компилируется новый вариант класса в исполняемом коде.

Как правило, новый исполняемый вариант обобщенного класса создается для каж­ дого объекта конструируемого типа, в котором аргумент имеет тип значения, например int или double. Следовательно, в каждом объекте типа Gen будет использовать­ ся один исполняемый вариант класса Gen, а в каждом объекте типа Gen — другой вариант класса Gen, причем каждый вариант приспосабливается к конкретно­ му типу значения. Но во всех случаях, когда аргумент оказывается ссылочного типа, используется только один вариант обобщенного класса, поскольку все ссылки имеют одинаковую длину (в байтах). Такая оптимизация также исключает раздувание кода.

Некоторые ограничения, присущие обобщениям

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

  • Свойства, операторы, индексаторы и события не могут быть обобщенными. Но эти элементы могут использоваться в обобщенном классе, причем с параметрами обобщенного типа этого класса.
  • К обобщенному методу нельзя применять модификатор extern.
  • Типы указателей нельзя использовать в аргументах типа.
  • Если обобщенный класс содержит поле типа static, то в объекте каждого конструируемого типа должна быть своя копия этого поля. Эго означает, что во всех экземплярах объектов одного конструируемого типа совместно используется одно и то же поле типа static. Но в экземплярах объектов другого конструируемого типа совместно используется другая копия этого поля. Следовательно, поле типа static не может совместно использоваться объектами всех конструируемых типов.

Заключительные соображения относительно обобщений

Обобщения являются весьма эффективным дополнением С#, поскольку они упро­ щают создание типизированного, повторно используемого кода. Несмотря на несколь­ ко усложненный, на первый взгляд, синтаксис обобщений, их применение быстро входит в привычку. Аналогично, умение применять ограничения к месту требует не­ которой практики и со временем не вызывает особых затруднений. Обобщения теперь стали неотъемлемой частью программирования на С#. Поэтому освоение этого важно­ го языкового средства стоит затраченных усилий.