Текст книги "Полное руководство. С# 4.0"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 38 (всего у книги 58 страниц)
В качестве примера рассмотрим следующий обобщенный класс. // Пример неоднозначности, к которой может привести // перегрузка методов с параметрами типа. // // Этот код не подлежит компиляции. using System; // Обобщенный класс, содержащий метод Set(), перегрузка // которого может привести к неоднозначности. class Gen
Рассмотрим приведенный выше код более подробно. Прежде всего обратите вни мание на то, что класс Gen объявляется с двумя параметрами типа: Т и V. В классе Gen метод Set() перегружается по параметрам типа Т и V, как показано ниже. public void Set(T о) { ob1 = о; } public void Set(V о) { ob2 = о; }
Такой подход кажется вполне обоснованным, поскольку типы Т и V ничем внешне не отличаются. Но подобная перегрузка таит в себе потенциальную неоднозначность. При таком объявлении класса Gen не соблюдается никаких требований к разли чению типов Т и V. Например, нет ничего принципиально неправильного в том, что объект класса Gen будет сконструирован так, как показано ниже. Ger
В данном случае оба типа, Т и V, заменяются типом int. В итоге оба варианта мето да Set() оказываются совершенно одинаковыми, что, разумеется, приводит к ошибке. Следовательно, при последующей попытке вызвать метод Set() для объекта notOK в методе Main() появится сообщение об ошибке вследствие неоднозначности во время компиляции.
Как правило, методы с параметрами типа перегружаются при условии, что объект конструируемого типа не приводит к конфликту. Следует, однако, иметь в виду, что ограничения на типы не учитываются при разрешении конфликтов, возникающих при перегрузке методов. Поэтому ограничения на типы нельзя использовать для исключе ния неоднозначности. Конструкторы, операторы и индексаторы с параметрами типа могут быть перегружены аналогично конструкторам по тем же самым правилам. Ковариантность и контравариантность в параметрах обобщенного типа
В главе 15 ковариантность и контравариантность были рассмотрены в связи с не обобщенными делегатами. Эта форма ковариантности и контравариантности по– прежнему поддерживается в С#, поскольку она очень полезна. Но в версии C# 4.0 воз можности ковариантности и контравариантности были расширены до параметров обобщенного типа, применяемых в обобщенных интерфейсах и делегатах. Ковариант ность и контравариантность применяется, главным образом, для рационального разре шения особых ситуаций, возникающих в связи с применением обобщенных интерфей сов и делегатов, определенных в среде .NET Framework. И поэтому некоторые интер фейсы и делегаты, определенные в библиотеке, были обновлены, чтобы использовать ковариантность и контравариантность параметров типа. Разумеется, преимуществами ковариантности и контравариантности можно также воспользоваться в интерфейсах и делегатах, создаваемых собственными силами. В этом разделе механизмы ковариант ности и контравариантности параметров типа поясняются на конкретных примерах. Применение ковариантности в обобщенном интерфейсе
Применительно к обобщенному интерфейсу ковариантность служит средством, разрешающим методу возвращать тип, производный от класса, указанного в пара метре типа. В прошлом возвращаемый тип должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Кова риантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр ковариантного типа объявляется с помощью ключевого слова out, которое предваряет имя этого параметра.
Для того чтобы стали понятнее последствия применения ковариантности, обратим ся к конкретному примеру. Ниже приведен очень простой интерфейс IMyCoVarGenIF, в котором применяется ковариантность. // В этом обобщенном интерфейсе поддерживается ковариантность. public interface IMyCoVarGenIF
Обратите особое внимание на то, как объявляется параметр обобщенного типа Т. Его имени предшествует ключевое слово out. В данном контексте ключевое слово out обозначает, что обобщенный тип Т является ковариантным. А раз он ковариантный, то метод GetObject() может возвращать ссылку на обобщенный тип Т или же ссылку на любой класс, производный от типа Т.
Несмотря на свою ковариантность по отношению к обобщенному типу Т, интер фейс IMyCoVarGenIF реализуется аналогично любому другому обобщенному интер фейсу. Ниже приведен пример реализации этого интерфейса в классе MyClass. // Реализовать интерфейс IMyCoVarGenIF. class MyClass
Обратите внимание на то, что ключевое слово 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
Прежде всего, переменной AlphaRef типа IMyCoVarGenIF в этом фраг менте кода присваивается ссылка на объект типа MyClass. Это вполне допу стимая операция, поскольку в классе MyClass реализуется интерфейс IMyCoVarGenIF, причем и в том, и в другом в качестве аргумента типа указывается Alpha. Далее имя объекта выводится на экран при вызове метода GetName() для объекта, возвращаемо го методом GetObject(). И эта операция вполне допустима, поскольку Alpha – это и тип, возвращаемый методом GetName(), и обобщенный тип Т. После этого пере менной AlphaRef присваивается ссылка на экземпляр объекта типа MyClass, что также допустимо, потому что класс Beta является производным от класса Alpha, а обобщенный тип Т – ковариантным в интерфейсе IMyCoVarGenIF. Если бы любое из этих условий не выполнялось, данная операция оказалась бы недопустимой. Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу. // Продемонстрировать ковариантность в обобщенном интерфейсе. using System; // Этот обобщенный интерфейс поддерживает ковариантность. public interface IMyCoVarGenIF
Результат выполнения этой программы выглядит следующим образом. Имя объекта, на который ссылается переменная AlphaRef: Alpha #1 Имя объекта, на который теперь ссылается переменная AlphaRef: Beta #1
Следует особо подчеркнуть, что переменной AlphaRef можно присвоить ссылку на объект типа MyClass благодаря только тому, что обобщенный тип Т указан как ковариантный в интерфейсе IMyCoVarGenIF. Для того чтобы убедиться в этом, удалите ключевое слово out из объявления параметра обобщенного типа Т в интер фейсе IMyCoVarGenIF и попытайтесь скомпилировать данную программу еще раз. Компиляция завершится неудачно, поскольку строгая проверка на соответствие типов не разрешит теперь подобное присваивание.
Один обобщенный интерфейс может вполне наследовать от другого. Иными сло вами, обобщенный интерфейс с параметром ковариантного типа можно расширить, как показано ниже. public interface IMyCoVarGenIF2
Обратите внимание на то, что ключевое слово out указано только в объявлении рас ширенного интерфейса. Указывать его в объявлении базового интерфейса не только не нужно, но и не допустимо. И последнее замечание: обобщенный тип Т допускается не указывать как ковариантный в объявлении интерфейса IMyCoVarGenIF2. Но при этом исключается ковариантность, которую может обеспечить расширенный интерфейс IMyCoVarGetIF. Разумеется, возможность сделать интерфейс IMyCoVarGenIF2 инва риантным может потребоваться в некоторых случаях его применения.
На применение ковариантности накладываются некоторые ограничения. Ковари антность параметра типа может распространяться только на тип, возвращаемый ме тодом. Следовательно, ключевое слово out нельзя применять в параметре типа, слу жащем для объявления параметра метода. Ковариантность оказывается пригодной только для ссылочных типов. Ковариантный тип нельзя использовать в качестве огра ничения в интерфейсном методе. Так, следующий интерфейс считается недопустимым. public interface IMyCoVarGenIF2
Применительно к обобщенному интерфейсу контравариантность служит сред ством, разрешающим методу использовать аргумент, тип которого относится к базо вому классу, указанному в соответствующем параметре типа. В прошлом тип аргу мента метода должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Контравариантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр контрава риантного типа объявляется с помощью ключевого слова in, которое предваряет имя этого параметра.
Для того чтобы стали понятнее последствия применения ковариантности, вновь обратимся к конкретному примеру. Ниже приведен обобщенный интерфейс IMyContraVarGenIF контравариантного типа. В нем указывается контравариантный параметр обобщенного типа Т, который используется в объявлении метода Show(). // Это обобщенный интерфейс, поддерживающий контравариантность. public interface IMyContraVarGenIF
Как видите, обобщенный тип Т указывается в данном интерфейсе как контрава риантный с помощью ключевого слова in, предшествующего имени его параметра. Обратите также внимание на то, что Т является параметром типа для аргумента obj в методе Show().
Далее интерфейс IMyContraVarGenIF реализуется в классе MyClass, как показано ниже. // Реализовать интерфейс IMyContraVarGenIF. class MyClass
В данном случае метод Show() просто выводит на экран строковое представление переменной х, получаемое в результате неявного обращения к методу ToString() из метода WriteLine().
После этого объявляется иерархия классов, как показано ниже. // Создать простую иерархию классов. class Alpha { public override string ToString() { return "Это объект класса Alpha."; } // ... } class Beta : Alpha { public override string ToString() { return "Это объект класса Beta."; } // ... }
Ради большей наглядности классы Alpha и Beta несколько отличаются от анало гичных классов из предыдущего примера применения ковариантности. Обратите так же внимание на то, что метод ToString() переопределяется таким образом, чтобы возвращать тип объекта.
С учетом всего изложенного выше, следующая последовательность операций будет считаться вполне допустимой. // Создать ссылку из интерфейса IMyContraVarGenIF
Прежде всего, обратите внимание на создание двух переменных ссылочного типа 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
Выполнение этой программы дает следующий результат. Это объект класса Beta. Это объект класса Beta.
Контравариантный интерфейс может быть расширен аналогично описанному выше расширению ковариантного интерфейса. Для достижения контравариантного характера расширенного интерфейса в его объявлении должен быть указан такой же параметр обобщенного типа, как и у базового интерфейса, но с ключевым словом in, как показано ниже. public interface IMyContraVarGenIF2
Следует иметь в виду, что указывать ключевое слово in в объявлении базового интерфейса не только не нужно, но и не допустимо. Более того, сам расширенный интерфейс IMyContraVarGenIF2 не обязательно должен быть контравариантным. Иными словами, обобщенный тип Т в интерфейсе IMyContraVarGenIF2 не требу ется модифицировать ключевым словом in. Разумеется, все преимущества, которые сулит контравариантность в интерфейсе IMyContraVarGen, при этом будут утрачены в интерфейсе IMyContraVarGenIF2.
Контравариантность оказывается пригодной только для ссылочных типов, а пара метр контравариантного типа можно применять только к аргументам методов. Сле довательно, ключевое слово in нельзя указывать в параметре типа, используемом в качестве возвращаемого типа. Вариантные делегаты
Как пояснялось в главе 15, ковариантность и контравариантность поддерживается в необобщенных делегатах в отношении типов, возвращаемых методами, и типов, ука зываемых при объявлении параметров. Начиная с версии C# 4.0, возможности кова риантности и контравариантности были распространены и на обобщенные делегаты. Подобные возможности действуют таким же образом, как было описано выше в от ношении обобщенных интерфейсов.
Ниже приведен пример контравариантного делегата. // Объявить делегат, контравариантный по отношению к обобщенному типу Т. delegate bool SomeOp
Этому делегату можно присвоить метод с параметром обобщенного типа Т или же класс, производный от типа Т.
А вот пример ковариантного делегата. // Объявить делегат, ковариантный по отношению к обобщенному типу Т. delegate Т AnotherOp
Этому делегату можно присвоить метод, возвращающий обобщенный тип Т, или же класс, производный от типа Т. В данном случае V оказывается просто параметром инвариантного типа.
В следующем примере программы демонстрируется применение обоих разновид ностей вариантных делегатов на практике. // Продемонстрировать конвариантность и контравариантность // в обобщенных делегатах. using System; // Объявить делегат, контравариантный по отношению к обобщенному типу Т. delegate bool SomeOp
Выполнение этой программы приводит к следующему результату. False 6
Каждая операция достаточно подробно поясняется в комментариях к данной про грамме. Следует особо подчеркнуть, для успешной компиляции программы в объяв лении обоих типов делегатов SomeOp and AnotherOp должны быть непременно ука заны ключевые слова in и out соответственно. Без этих модификаторов компиляция программы будет выполнена с ошибками из-за отсутствия неявных преобразований типов в означенных строках кода. Создание экземпляров объектов обобщенных типов
Когда приходится иметь дело с обобщениями, то нередко возникает вопрос: не приведет ли применение обобщенного класса к неоправданному раздуванию кода? Ответ на этот вопрос прост: не приведет. Дело в том, что в C# обобщения реализованы весьма эффективным образом: новые объекты конструируемого типа создаются лишь по мере надобности. Этот процесс описывается ниже.
Когда обобщенный класс компилируется в псевдокод MSIL, он сохраняет все свои параметры типа в их обобщенной форме. А когда конкретный экземпляр класса по требуется во время выполнения программы, то JIT-компилятор сконструирует кон кретный вариант этого класса в исполняемом коде, в котором параметры типа заме няются аргументами типа. В каждом экземпляре с теми же самыми аргументами типа будет использоваться один и тот же вариант данного класса в исполняемом коде.
Так, если имеется некоторый обобщенный класс Gen, то во всех объектах типа Gen будет использоваться один и тот же исполняемый код данного класса. Сле довательно, раздувание кода исключается благодаря тому, что в программе создают ся только те варианты класса, которые действительно требуются. Когда же возникает потребность сконструировать объект другого типа, то компилируется новый вариант класса в исполняемом коде.
Как правило, новый исполняемый вариант обобщенного класса создается для каж дого объекта конструируемого типа, в котором аргумент имеет тип значения, например int или double. Следовательно, в каждом объекте типа Gen будет использовать ся один исполняемый вариант класса Gen, а в каждом объекте типа Gen – другой вариант класса Gen, причем каждый вариант приспосабливается к конкретно му типу значения. Но во всех случаях, когда аргумент оказывается ссылочного типа, используется только один вариант обобщенного класса, поскольку все ссылки имеют одинаковую длину (в байтах). Такая оптимизация также исключает раздувание кода. Некоторые ограничения, присущие обобщениям
Ниже перечислен ряд ограничений, которые следует иметь в виду при использова нии обобщений.
Свойства, операторы, индексаторы и события не могут быть обобщенными. Но эти элементы могут использоваться в обобщенном классе, причем с параметрами обобщенного типа этого класса.
К обобщенному методу нельзя применять модификатор extern.
Типы указателей нельзя использовать в аргументах типа.
Если обобщенный класс содержит поле типа static, то в объекте каждого конструируемого типа должна быть своя копия этого поля. Эго означает, что во всех экземплярах объектов одного конструируемого типа совместно используется одно и то же поле типа static. Но в экземплярах объектов другого конструируемого типа совместно используется другая копия этого поля. Следовательно, поле типа static не может совместно использоваться объектами всех конструируемых типов. Заключительные соображения относительно обобщений
Обобщения являются весьма эффективным дополнением С#, поскольку они упро щают создание типизированного, повторно используемого кода. Несмотря на несколь ко усложненный, на первый взгляд, синтаксис обобщений, их применение быстро входит в привычку. Аналогично, умение применять ограничения к месту требует не которой практики и со временем не вызывает особых затруднений. Обобщения теперь стали неотъемлемой частью программирования на С#. Поэтому освоение этого важно го языкового средства стоит затраченных усилий.