Читаем Полное руководство. С# 4.0 полностью

Для управления списками телефонных номеров создадим еще один класс под на званием PhoneList. Его следует сделать обобщенным, поскольку он должен служить для управления любым списком телефонных номеров. В функции такого управления должен, в частности, входить поиск телефонных номеров по заданным именам и нао борот, поэтому на данный класс необходимо наложить ограничение по типу, требую щее, чтобы объекты, сохраняемые в списке, были экземплярами класса, производного от класса PhoneNumber. // Класс PfconeList способен управлять любым видом списка телефонных // номеров, при условии, что он является производным от класса PhoneNumber. class PhoneList 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

Ограничение на базовый класс разрешает коду в классе 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 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 plist = new PhoneList; 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 plist2 = new PhoneList; 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 plist3 = // new PhoneList; // Ошибка! } }

Ниже приведен результат выполнения этой программы. Гари: 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 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 plist = new PhoneList; 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 plist2 = new PhoneList; 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 plist3 = // new PhoneList; // Ошибка! } }

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

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

Ниже приведен простой пример, демонстрирующий наложение ограничения new. // Продемонстрировать наложение ограничения new на конструктор. using System; class MyClass { public MyClass { // ... } // ... } class Test where T : new { T obj; public Test { // Этот код работоспособен благодаря наложению ограничения new. obj = new Т; // создать объект типа Т } // ... } class ConsConstraintDemo { static void Main { Test x = new Test; } }

Прежде всего обратите внимание на объявление класса Test. class Test where T : new {

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

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

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

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

Обратите внимание на то, что аргументом типа в данном случае является класс 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 where Т : class { Т obj; public Test { // Следующий оператор допустим только потому, что // аргумент Т гарантированно относится к ссылочному // типу, что позволяет присваивать пустое значение. obj = null; } // ... } class ClassConstraintDemo { static void Main { // Следующий код вполне допустим, поскольку MyClass является классом. Test х = new Test; // Следующая строка кода содержит ошибку, поскольку // int относится к типу значения. // Test у = new Test; } }

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

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

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

Ограничение типа значения является дополнением ограничения ссылочного типа. Оно просто гарантирует, что любой аргумент, обозначающий тип, должен быть типа значения, в том числе struct и enum. (В данном случае обнуляемый тип не относится к типу значения.) Ниже приведен пример наложения ограничения типа значения. // Продемонстрировать наложение ограничения типа значения. using System; struct MyStruct { // ... } class MyClass { // ... } class Test where T : struct { T obj; public Test(T x) { obj = x; } // ... } class ValueConstraintDemo { static void Main { // Оба следующих объявления вполне допустимы. Test х = new Test(new MyStruct); Test у = new Test(10); // А следующее объявление недопустимо! // Test z = new Test(new MyClass); } }

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

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

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

В этом объявлении оператор where уведомляет компилятор о том, что аргумент типа, привязанный к параметру типа V, должен быть таким же, как и аргумент типа, привязанный к параметру типа Т, или же наследовать от него. Если подобная связь отсутствует при объявлении объекта типа Gen, то во время компиляции возникнет ошибка. Такое ограничение на параметр типа называется неприкрытым ограничением типа. В приведенном ниже примере демонстрируется наложение этого ограничения. // Установить связь между двумя параметрами типа. using System; class А { // ... } class В : А { // ... } // Здесь параметр типа V должен наследовать от параметра типа Т. class Gen where V : T { // ... } class NakedConstraintDemo { static void Main { // Это объявление вполне допустимо, поскольку // класс В наследует от класса А. GerKA, В> х = new Gen; // А это объявление недопустимо, поскольку // класс А не наследует от класса В. // Gen у = new Gen; } }

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

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

недопустимо, поскольку класс А не наследует от класса В. Применение нескольких ограничений

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

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

Если же в обобщении используются два или более параметра типа, то ограничения на каждый из них накладываются с помощью отдельного оператора where, как в при веденном ниже примере. // Использовать несколько операторов where. using System; // У класса Gen имеются два параметра типа, и на оба накладываются // ограничения с помощью отдельных операторов where. class Gen 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 obj = new Gen("тест", 11); // А следующая строка кода недопустима, поскольку // bool не относится к ссылочному типу. // Gen obj = new Gencbool, int>(true, 11); } }

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

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

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

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

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

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

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

Ниже приведен короткий пример, демонстрирующий данную форму оператора default. // Продемонстрировать форму оператора default. using System; class MyClass { // ... } // Получить значение, присваиваемое параметру типа Т по умолчанию. class Test { public Т obj; public Test { // Следующий оператор годится только для ссылочных типов. // obj = null; // не годится // Следующий оператор годится только для типов значений. // obj = 0; // не годится // А этот оператор годится как для ссылочных типов, // так и для типов значений. obj = default(T); // Годится! } // ... } class DefaultDemo { static void Main { // Сконструировать объект класса Test, используя ссылочный тип. Test х = new Test; if(x.obj == null) Console.WriteLine("Переменная x.obj имеет пустое значение ."); // Сконструировать объект класса Test, используя тип значения. Test у = new Test; if(у.obj == 0) Console.WriteLine("Переменная у.obj имеет значение 0."); } }

Вот к какому результату приводит выполнение этого кода. Переменная x.obj имеет пустое значение . Переменная у.obj имеет значение 0. Обобщенные структуры

В C# разрешается создавать обобщенные структуры. Синтаксис для них такой же, как и для обобщенных классов. В качестве примера ниже приведена программа, в ко торой создается обобщенная структура XY для хранения координат X, Y. // Продемонстрировать применение обобщенной структуры. using System; // Эта структура является обобщенной. struct XY { Т х; Т у; public XY(Т а, Т b) { х = а; У = b; } public Т X { get { return х; } set { х = value; } } public T Y { get { return y; } set { у = value; } } } class StructTest { static void Main { XY xy = new XY(10, 20); XY xy2 = new XY(88.0, 99.0); Console.WriteLine(xy.X + ", " + xy.Y); Console.WriteLine(xy2.X + ", " + xy2.Y); } }

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

Как и на обобщенные классы, на обобщенные структуры могут накладываться огра ничения. Например, на аргументы типа в приведенном ниже варианте структуры XY накладывается ограничение типа значения. struct XY where Т : struct { // ... Создание обобщенного метода

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

Рассмотрим для начала простой пример. В приведенной ниже программе объяв ляется необобщенный класс ArrayUtils, а в нем — статический обобщенный метод CopyInsert. Этот метод копирует содержимое одного массива в другой, вводя по ходу дела новый элемент в указанном месте. Метод CopyInsert можно использо вать вместе с массивами любого типа. // Продемонстрировать применение обобщенного метода. using System; // Класс обработки массивов. Этот класс не является обобщенным. class ArrayUtils { // Копировать массив, вводя по ходу дела новый элемент. // Этот метод является обобщенным. public static bool CopyInsert (Т 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(Т 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("В С#", 1, strs, strs2);

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

На аргументы обобщенного метода можно наложить ограничения, указав их после списка параметров. В качестве примера ниже приведен вариант метода CopyInsert для обработки данных только ссылочных типов. public static bool CopyInsert(Т 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 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 intDel = Sum; Console.WriteLine(intDel(3)); // Сконструировать делегат типа string. SomeOp strDel = Reflect; Console.WriteLine(strDel("Привет")); } }

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

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

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

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

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

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

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

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

Метод Reflect принимает аргумент типа string и возвращает результат типа string, поэтому он совместим со строковым экземпляром делегата SomeOp. В силу присущей обобщениям типовой безопасности обобщенным делегатам нель зя присваивать несовместимые методы. Так, следующая строка кода оказалась бы оши бочной в рассматриваемой здесь программе. SomeOp intDel = Reflect; // Ошибка!

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

Помимо обобщенных классов и методов, в C# допускаются обобщенные интерфей сы. Такие интерфейсы указываются аналогично обобщенным классам. Ниже приведен измененный вариант примера из главы 12, демонстрирующего интерфейс ISeries. (Напомним, что ISeries является интерфейсом для класса, генерирующего последо вательный ряд числовых значений.) Тип данных, которым оперирует этот интерфейс, теперь определяется параметром типа. // Продемонстрировать применение обобщенного интерфейса. using System; public interface ISeries { T GetNext;// возвратить следующее по порядку число void Reset; // генерировать ряд последовательных чисел с самого начала void SetStart(Т v); // задать начальное значение } // Реализовать интерфейс ISeries. class ByTwos : ISeries { 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 intBT = new ByTwos(IntPlusTwo); for(int i=0; i < 5; i++) Console.Write(intBT.GetNext + " "); Console.WriteLine; // Продемонстрировать генерирование // последовательного ряда значений типа double. ByTwos dblBT = new ByTwos(DoublePlusTwo); dblBT.SetStart(11.4); for (int i=0; i < 5; i++) Console.Write(dblBT.GetNext + " "); Console.WriteLine; // Продемонстрировать генерирование последовательного ряда // значений координат объекта типа ThreeD. ByTwos ThrDBT = new ByTwos(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 {

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

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

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

Аргумент типа, требующийся для интерфейса 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 where T : class {

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

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

Иногда возникает потребность сравнить два экземпляра параметра типа. Допу стим, что требуется написать обобщенный метод IsIn, возвращающий логическое значение true, если в массиве содержится некоторое значение. Для этой цели сначала можно попробовать сделать следующее. // Не годится! public static bool IsIn(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

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

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

В ходе реализации интерфейса IEquatable обычно требуется также переопре делять методы GetHashCode и Equals(Object), определенные в классе Object, чтобы они оказались совместимыми с конкретной реализацией метода Equals. Ниже приведен пример программы, в которой демонстрируется исправленный вари ант упоминавшегося ранее метода IsIn. // Требуется обобщенный интерфейс IEquatable. public static bool IsIn(T what, T[] obs) where T : IEquatable { foreach(T v in obs) if(v.Equals(what)) // Применяется метод Equals. return true; return false; }

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

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

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

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

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

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

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

В приведенном ниже примере программы демонстрируется применение обоих методов IsIn и InRange на практике. // Продемонстрировать применение обобщенных // интерфейсов IComparable и IEquatable. using System; // Теперь в классе MyClass реализуются обобщенные // интерфейсы IComparable и IEquatable. class MyClass : IComparable, IEquatable { public int Val; public MyClass(int x) { Val = x; } // Реализовать обобщенный интерфейс IComparable. public int CompareTo(MyClass other) { return Val - other.Val; // Now, no cast is needed. } // Реализовать обобщенный интерфейс IEquatable. 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. public static bool IsIn(T what, T[] obs) where T : IEquatable { foreach(T v in obs) if(v.Equals(what)) // Применяется метод Equals return true; return false; } // Требуется обобщенный интерфейс IComparable. В данном методе // предполагается, что массив отсортирован. Он возвращает логическое // значение true, если значение параметра what оказывается среди элементов // массива, передаваемых параметру obs. public static bool InRange(T what, T[] obs) where T : IComparable { 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 { Т ob; public Gen(T о) { ob = о; } // Возвратить значение переменной ob. public Т GetOb { return ob; } } // Класс, производный от класса Gen. class Gen2 : Gen { public Gen2(T o) : base(o) { // ... } } class GenHierDemo { static void Main { Gen2 g2 = new Gen2("Привет"); Console.WriteLine(g2.GetOb); } }

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

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

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

Разумеется, в производный класс можно свободно добавлять его собственные па раметры типа, если в этом есть потребность. В качестве примера ниже приведен ва риант предыдущей иерархии классов, где в класс Gen2 добавлен собственный пара метр типа. // Пример добавления собственных параметров типа в производный класс. using System; // Обобщенный базовый класс. class Gen { Т ob; // объявить переменную типа Т // Передать конструктору ссылку типа Т. public Gen(T о) { ob = о; } // Возвратить значение переменной ob. public Т GetOb { return ob; } } // Класс, производный от класса Gen. В этом классе // определяется второй параметр типа V. class Gen2 : Gen { 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 x = new Gen2("Значение равно: ", 99); Console.Write(x.GetOb); Console.WriteLine(x.GetObj2); } }

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

В этом объявлении Т — это тип, передаваемый базовому классу 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 : 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 w = new Gen("Привет", 47); Console.Write(w.GetOb + " "); Console.WriteLine(w.GetNum); } }

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

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

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

В обобщенном классе виртуальный метод может быть переопределен таким же об разом, как и любой другой метод. В качестве примера рассмотрим следующую про грамму, в которой переопределяется виртуальный метод GetOb. // Пример переопределения виртуального метода в обобщенном классе. using System; // Обобщенный базовый класс. class Gen { protected Т ob; public Gen(T о) { ob = о; } // Возвратить значение переменной ob. Этот метод является виртуальным. public virtual T GetOb { Console.Write("Метод GetOb из класса Gen" + " возвращает результат: "); return ob; } } // Класс, производный от класса Gen. В этом классе // переопределяется метод GetOb. class Gen2 : Gen { 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 iOb = new Gen(88); // Здесь вызывается вариант метода GetOb из класса Gen. Console.WriteLine(iOb.GetOb); // А теперь создать объект класса Gen2 и присвоить // ссылку на него переменной iOb типа Gen. iOb = new Gen2(99); // Здесь вызывается вариант метода GetOb из класса Gen2. Console.WriteLine(iOb.GetOb); } }

Ниже приведен результат выполнения этой программы. Метод GetOb из класса Gen возвращает результат: 88 Метод GetOb из класса Gen2 возвращает результат: 99

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

Обратите внимание на следующую строку кода. iOb = new Gen2(99);

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

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

В качестве примера рассмотрим следующий обобщенный класс. // Пример неоднозначности, к которой может привести // перегрузка методов с параметрами типа. // // Этот код не подлежит компиляции. using System; // Обобщенный класс, содержащий метод Set, перегрузка // которого может привести к неоднозначности. class Gen { Т оb1; V ob2; // ... // В некоторых случаях эти два метода не будут // отличаться своими параметрами типа. public void Set(T о) { ob1 = о; } public void Set(V о) { ob2 = о; } } class AmbiguityDemo { static void Main { Gen ok = new Gen; Gen notOK = new Gen; 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 notOK = new Gen;

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

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

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

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

Для того чтобы стали понятнее последствия применения ковариантности, обратим ся к конкретному примеру. Ниже приведен очень простой интерфейс IMyCoVarGenIF, в котором применяется ковариантность. // В этом обобщенном интерфейсе поддерживается ковариантность. public interface IMyCoVarGenIF { Т GetObject; }

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

Несмотря на свою ковариантность по отношению к обобщенному типу Т, интер фейс IMyCoVarGenIF реализуется аналогично любому другому обобщенному интер фейсу. Ниже приведен пример реализации этого интерфейса в классе MyClass. // Реализовать интерфейс IMyCoVarGenIF. class MyClass : IMyCoVarGenIF { 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. // Это вполне допустимо как при наличии ковариантности, так и без нее. IMyCoVarGenIF AlphaRef = new MyClass(new Alpha("Alpha #1")); Console.WriteLine("Имя объекта, на который ссылается переменная AlphaRef: " + AlphaRef.GetObject.GetName); // А теперь создать объект MyClass и присвоить его переменной AlphaRef. // *** Эта строка кода вполне допустима благодаря ковариантности. *** AlphaRef = new MyClass(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 { Т GetObject; } // Реализовать интерфейс IMyCoVarGenIF. class MyClass : IMyCoVarGenIF { 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. // Это вполне допустимо как при наличии ковариантности, так и без нее. IMyCoVarGenIF AlphaRef = new MyClass(new Alpha("Alpha #1")); Console.WriteLine("Имя объекта, на который ссылается переменная " + "AlphaRef: " + AlphaRef.GetObject.GetName); // А теперь создать объект MyClass и присвоить его // переменной AlphaRef. // *** Эта строка кода вполне допустима благодаря ковариантности. *** AlphaRef = new MyClass(new Beta("Beta #1")); Console.WriteLine("Имя объекта, на который теперь ссылается переменная " + "AlphaRef: " + AlphaRef.GetObject.GetName); } }

Результат выполнения этой программы выглядит следующим образом. Имя объекта, на который ссылается переменная AlphaRef: Alpha #1 Имя объекта, на который теперь ссылается переменная AlphaRef: Beta #1

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

Один обобщенный интерфейс может вполне наследовать от другого. Иными сло вами, обобщенный интерфейс с параметром ковариантного типа можно расширить, как показано ниже. public interface IMyCoVarGenIF2 : IMyCoVarGenIF { // ... }

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

На применение ковариантности накладываются некоторые ограничения. Ковари антность параметра типа может распространяться только на тип, возвращаемый ме тодом. Следовательно, ключевое слово out нельзя применять в параметре типа, слу жащем для объявления параметра метода. Ковариантность оказывается пригодной только для ссылочных типов. Ковариантный тип нельзя использовать в качестве огра ничения в интерфейсном методе. Так, следующий интерфейс считается недопустимым. public interface IMyCoVarGenIF2 { void M where V:T; // Ошибка, ковариантный тип T нельзя // использовать как ограничение } Применение контравариантности в обобщенном интерфейсе

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

Для того чтобы стали понятнее последствия применения ковариантности, вновь обратимся к конкретному примеру. Ниже приведен обобщенный интерфейс IMyContraVarGenIF контравариантного типа. В нем указывается контравариантный параметр обобщенного типа Т, который используется в объявлении метода Show. // Это обобщенный интерфейс, поддерживающий контравариантность. public interface IMyContraVarGenIF { void Show(T obj); }

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

Далее интерфейс IMyContraVarGenIF реализуется в классе MyClass, как показано ниже. // Реализовать интерфейс IMyContraVarGenIF. class MyClass : IMyContraVarGenIF { 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 // на объект типа MyClass. // Это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF AlphaRef = new MyClass; // Создать ссылку из интерфейса IMyContraVarGenIF // на объект типа MyClass. // И это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF BetaRef = new MyClass; // Создать ссылку из интерфейса IMyContraVarGenIF // на объект типа MyClass. // *** Это вполне допустимо благодаря контравариантности. *** IMyContraVarGenIF BetaRef2 = new MyClass; // Этот вызов допустим как при наличии контравариантности, так и без нее. 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 { void Show(T obj); } // Реализовать интерфейс IMyContraVarGenIF. class MyClass : IMyContraVarGenIF { 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 // на объект типа MyClass. // Это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF AlphaRef = new MyClass; // Создать ссылку из интерфейса IMyContraVarGenIF // на объект типа MyClass. // И это вполне допустимо как при наличии контравариантности, // так и без нее. IMyContraVarGenIF BetaRef = new MyClass; // Создать ссылку из интерфейса IMyContraVarGenIF // на объект типа MyClass. // *** Это вполне допустимо благодаря контравариантности. *** IMyContraVarGenIF BetaRef2 = new MyClass; // Этот вызов допустим как при наличии контравариантности, так и без нее. BetaRef.Show(new Beta); // Присвоить переменную AlphaRef переменной BetaRef. // *** Это вполне допустимо благодаря контравариантности. *** BetaRef = AlphaRef; BetaRef.Show(new Beta); } }

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

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

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

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

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

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

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

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

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

В следующем примере программы демонстрируется применение обоих разновид ностей вариантных делегатов на практике. // Продемонстрировать конвариантность и контравариантность // в обобщенных делегатах. using System; // Объявить делегат, контравариантный по отношению к обобщенному типу Т. delegate bool SomeOp(Т obj); // Объявить делегат, ковариантный по отношению к обобщенному типу Т. delegate Т AnotherOp(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 и задать для него метод IsEven. SomeOp checkIt = IsEven; // Объявить делегат SomeOp. SomeOp checkIt2; // А теперь- присвоить делегат SomeOp делегату SomeOp. // *** Это допустимо только благодаря контравариантности. *** checklt2 = checkIt; // Вызвать метод через делегат. Console.WriteLine(checkIt2(objВ)); // Далее, продемонстрировать контравариантность. // Объявить сначала два делегата типа AnotherOp. // Здесь возвращаемым типом является класс Beta, // а параметром типа - класс Alpha. // Обратите внимание на то, что для делегата modifyIt // задается метод ChangeIt. AnotherOp modifyIt = ChangeIt; // Здесь возвращаемым типом является класс Alpha, // а параметром типа — тот же класс Alpha. AnotherOp 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 не может совместно использоваться объектами всех конструируемых типов. Заключительные соображения относительно обобщений

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

Перейти на страницу:

Похожие книги

Основы программирования в Linux
Основы программирования в Linux

В четвертом издании популярного руководства даны основы программирования в операционной системе Linux. Рассмотрены: использование библиотек C/C++ и стан­дартных средств разработки, организация системных вызовов, файловый ввод/вывод, взаимодействие процессов, программирование средствами командной оболочки, создание графических пользовательских интерфейсов с помощью инструментальных средств GTK+ или Qt, применение сокетов и др. Описана компиляция программ, их компоновка c библиотеками и работа с терминальным вводом/выводом. Даны приемы написания приложений в средах GNOME® и KDE®, хранения данных с использованием СУБД MySQL® и отладки программ. Книга хорошо структурирована, что делает обучение легким и быстрым. Для начинающих Linux-программистов

Нейл Мэтью , Ричард Стоунс , Татьяна Коротяева

ОС и Сети / Программирование / Книги по IT
97 этюдов для архитекторов программных систем
97 этюдов для архитекторов программных систем

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

Билл де Ора , Майкл Хайгард , Нил Форд

Программирование, программы, базы данных / Базы данных / Программирование / Книги по IT
Программист-прагматик. Путь от подмастерья к мастеру
Программист-прагматик. Путь от подмастерья к мастеру

Находясь на переднем крае программирования, книга "Программист-прагматик. Путь от подмастерья к мастеру" абстрагируется от всевозрастающей специализации и технических тонкостей разработки программ на современном уровне, чтобы исследовать суть процесса – требования к работоспособной и поддерживаемой программе, приводящей пользователей в восторг. Книга охватывает различные темы – от личной ответственности и карьерного роста до архитектурных методик, придающих программам гибкость и простоту в адаптации и повторном использовании.Прочитав эту книгу, вы научитесь:Бороться с недостатками программного обеспечения;Избегать ловушек, связанных с дублированием знания;Создавать гибкие, динамичные и адаптируемые программы;Избегать программирования в расчете на совпадение;Защищать вашу программу при помощи контрактов, утверждений и исключений;Собирать реальные требования;Осуществлять безжалостное и эффективное тестирование;Приводить в восторг ваших пользователей;Формировать команды из программистов-прагматиков и с помощью автоматизации делать ваши разработки более точными.

А. Алексашин , Дэвид Томас , Эндрю Хант

Программирование / Книги по IT