Текст книги "Полное руководство. С# 4.0"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 37 (всего у книги 58 страниц)
Вот к какому результату приводит выполнение этой программы. Содержимое массива nums: 1 2 3 Содержимое массива nums2: 1 2 99 3 Содержимое массива strs: Обобщения весьма эффективны. Содержимое массива strs2: Обобщения в C# весьма эффективны.
Внимательно проанализируем метод CopyInsert(). Прежде всего обратите вни мание на объявление этого метода в следующей строке кода. public static bool CopyInsert
Параметр типа объявляется после имени метода, но перед списком его параметров. Обратите также внимание на то, что метод 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
Тип передаваемых аргументов необходимо указывать явно в том случае, если ком пилятор не сможет вывести тип параметра Т или если требуется отменить выводи мость типов. Применение ограничений в обобщенных методах
На аргументы обобщенного метода можно наложить ограничения, указав их после списка параметров. В качестве примера ниже приведен вариант метода CopyInsert() для обработки данных только ссылочных типов. public static bool CopyInsert
Если попробовать применить этот вариант в предыдущем примере программы об работки массивов, то приведенный ниже вызов метода CopyInsert() не будет ском пилирован, поскольку int является типом значения, а не ссылочным типом. // Теперь неправильно, поскольку параметр Т должен быть ссылочного типа! ArrayUtils.Copylnsert(99, 2, nums, nums2); // Теперь недопустимо! Обобщенные делегаты
Как и методы, делегаты также могут быть обобщенными. Ниже приведена общая форма объявления обобщенного делегата. delegate возврашдемый_тип имя_делегата<список_параметров_типа>(список_аргументов);
Обратите внимание на расположение списка параметров типа. Он следует непо средственно после имени делегата. Преимущество обобщенных делегатов заключается в том, что их допускается определять в типизированной обобщенной форме, которую можно затем согласовать с любым совместимым методом.
В приведенном ниже примере программы демонстрируется применение делегата SomeOp с одним параметром типа Т. Этот делегат возвращает значение типа Т и при нимает аргумент типа Т. // Простой пример обобщенного делегата. using System; // Объявить обобщенный делегат. delegate Т SomeOp
Эта программа дает следующий результат. 6 тевирП
Рассмотрим эту программу более подробно. Прежде всего обратите внимание на следующее объявление делегата SomeOp. delegate Т SomeOp
Как видите, тип Т может служить в качестве возвращаемого типа, несмотря на то, что параметр типа Т указывается после имени делегата SomeOp.
Далее в классе GenDelegateDemo объявляются методы Sum() и Reflect(), как по казано ниже. static int Sum(int v) { static string Reflect(string str) {
Метод Sum() возвращает результат суммирования целого значения, передаваемого в качестве аргумента, а метод Reflect() – символьную строку, которая получается об ращенной по отношению к строке, передаваемой в качестве аргумента.
В методе Main() создается экземпляр intDel делегата, которому присваивается ссылка на метод Sum(). SomeOp
Метод Sum() принимает аргумент типа int и возвращает значение типа int, поэ тому он совместим с целочисленным экземпляром делегата SomeOp.
Аналогичным образом создается экземпляр strDel делегата, которому присваива ется ссылка на метод Reflect(). SomeOp
Метод Reflect() принимает аргумент типа string и возвращает результат типа string, поэтому он совместим со строковым экземпляром делегата SomeOp. В силу присущей обобщениям типовой безопасности обобщенным делегатам нель зя присваивать несовместимые методы. Так, следующая строка кода оказалась бы оши бочной в рассматриваемой здесь программе. SomeOp
Ведь метод Reflect() принимает аргумент типа string и возвращает результат типа string, а следовательно, он несовместим с целочисленным экземпляром деле гата SomeOp. Обобщенные интерфейсы
Помимо обобщенных классов и методов, в C# допускаются обобщенные интерфей сы. Такие интерфейсы указываются аналогично обобщенным классам. Ниже приведен измененный вариант примера из главы 12, демонстрирующего интерфейс ISeries. (Напомним, что ISeries является интерфейсом для класса, генерирующего последо вательный ряд числовых значений.) Тип данных, которым оперирует этот интерфейс, теперь определяется параметром типа. // Продемонстрировать применение обобщенного интерфейса. using System; public interface ISeries
Этот код выдает следующий результат. 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
Параметр типа Т указывается не только при объявлении класса 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
Если реализуется именно такой вариант интерфейса ISeries, в реализующем его классе следует указать то же самое ограничение на параметр типа Т, как показано ниже. class ByTwos
В силу ограничения ссылочного типа этот вариант интерфейса ISeries нельзя применять к типам значений. Поэтому если реализовать его в рассматриваемом здесь примере программы, то допустимым окажется только объявление ByTwos, но не объявления ByTwos и ByTwos. Сравнение экземпляров параметра типа
Иногда возникает потребность сравнить два экземпляра параметра типа. Допу стим, что требуется написать обобщенный метод IsIn(), возвращающий логическое значение true, если в массиве содержится некоторое значение. Для этой цели сначала можно попробовать сделать следующее. // Не годится! public static bool IsIn
К сожалению, эта попытка не пройдет. Ведь параметр Т относится к обобщенному типу, и поэтому компилятору не удастся выяснить, как сравнивать два объекта. Требу ется ли для этого поразрядное сравнение или же только сравнение отдельных полей? А возможно, сравнение ссылок? Вряд ли компилятор сможет найти ответы на эти во просы. Правда, из этого положения все же имеется выход.
Для сравнения двух объектов параметра обобщенного типа они должны реализовы вать интерфейс 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
Обратите внимание в приведенном выше примере на применение следующего ограничения. where Т : IEquatable
Это ограничение гарантирует, что только те типы, в которых реализован интерфейс IEquatable, являются действительными аргументами типа для метода IsIn(). Вну три этого метода применяется метод Equals(), который определяет равенство одного объекта другому.
Для определения относительного порядка следования двух элементов применяется интерфейс IComparable. У этого интерфейса имеются две формы: обобщенная и не обобщенная. Обобщенная форма данного интерфейса обладает преимуществом обе спечения типовой безопасности, и поэтому мы рассмотрим здесь именно ее. Обоб щенный интерфейс IComparable объявляется следующим образом. public interface IComparable
Сравниваемый тип данных передается ему в качестве аргумента типа Т. В этом ин терфейсе определяется метод CompareTo(), как показано ниже. int CompareTo(Т other)
В этом методе сравниваются вызывающий объект и другой объект, определяемый параметром other. В итоге возвращается нуль, если вызывающий объект оказывается больше, чем объект other; и отрицательное значение, если вызывающий объект ока зывается меньше, чем объект other.
Для того чтобы воспользоваться методом CompareTo(), необходимо указать огра ничение, которое требуется наложить на аргумент типа для реализации обобщенного интерфейса IComparable. А затем достаточно вызвать метод CompareTo(), чтобы сравнить два экземпляра параметра типа.
Ниже приведен пример применения обобщенного интерфейса IComparable. В этом примере вызывается метод InRange(), возвращающий логическое значение true, если объект оказывается среди элементов отсортированного массива. // Требуется обобщенный интерфейс IComparable
В приведенном ниже примере программы демонстрируется применение обоих методов IsIn() и InRange() на практике. // Продемонстрировать применение обобщенных // интерфейсов IComparable
Выполнение этой программы приводит к следующему результату. Найдено значение 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
В этой иерархии класс Gen2 наследует от обобщенного класса Gen. Обратите вни мание на объявление класса Gen2 в следующей строке кода. class Gen2
Параметр типа Т указывается в объявлении класса Gen2 и в то же время передается классу Gen. Это означает, что любой тип, передаваемый классу Gen2, будет передавать ся также классу Gen. Например, в следующем объявлении: Gen2
параметр типа string передается классу Gen. Поэтому переменная ob в той части класса Gen2, которая относится к классу Gen, будет иметь тип string. Обратите также внимание на то, что в классе Gen2 параметр типа Т не использует ся, а только передается вверх по иерархии базовому классу Gen. Это означает, что в производном классе следует непременно указывать параметры типа, требующиеся его обобщенному базовому классу, даже если этот производный класс не обязательно должен быть обобщенным.
Разумеется, в производный класс можно свободно добавлять его собственные па раметры типа, если в этом есть потребность. В качестве примера ниже приведен ва риант предыдущей иерархии классов, где в класс Gen2 добавлен собственный пара метр типа. // Пример добавления собственных параметров типа в производный класс. using System; // Обобщенный базовый класс. class Gen
Обратите внимание на приведенное ниже объявление класса Gen2 в данном вари анте иерархии классов. class Gen2
В этом объявлении Т – это тип, передаваемый базовому классу 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
Эта программа дает следующий результат. Привет 47
В данной программе обратите внимание на то, как класс Gen наследует от класса NonGen в следующем объявлении. class Gen
Класс NonGen не является обобщенным, и поэтому аргумент типа для него не ука зывается. Это означает, что параметр т, указываемый в объявлении обобщенного про изводного класса Gen, не требуется для указания базового класса NonGen и даже не может в нем использоваться. Следовательно, класс Gen наследует от класса NonGen обычным образом, т.е. без выполнения каких-то особых условий. Переопределение виртуальных методов в обобщенном классе
В обобщенном классе виртуальный метод может быть переопределен таким же об разом, как и любой другой метод. В качестве примера рассмотрим следующую про грамму, в которой переопределяется виртуальный метод GetOb(). // Пример переопределения виртуального метода в обобщенном классе. using System; // Обобщенный базовый класс. class Gen
Ниже приведен результат выполнения этой программы. Метод GetOb() из класса Gen возвращает результат: 88 Метод GetOb() из класса Gen2 возвращает результат: 99
Как следует из результата выполнения приведенной выше программы, переопреде ляемый вариант метода GetOb() вызывается для объекта типа Gen2, а его вариант из базового класса вызывается для объекта типа Gen.
Обратите внимание на следующую строку кода. iOb = new Gen2
Такое присваивание вполне допустимо, поскольку iOb является переменной типа Gen. Следовательно, она может ссылаться на любой объект типа Gen или же объект класса, производного от Gen, включая и Gen2. Разумеется, пере менную iOb нельзя использовать, например, для ссылки на объект типа Gen2, поскольку это может привести к несоответствию типов. Перегрузка методов с несколькими параметрами типа
Методы, параметры которых объявляются с помощью параметров типа, могут быть перегружены. Но правила их перегрузки упрощаются по сравнению с методами без параметров типа. Как правило, метод, в котором параметр типа служит для указания типа данных параметра этого метода, может быть перегружен при условии, что сиг натуры обоих его вариантов отличаются. Это означает, что оба варианта перегружае мого метода должны отличаться по типу или количеству их параметров. Но типовые различия должны определяться не по параметру обобщенного типа, а исходя из ар гумента типа, подставляемого вместо параметра типа при конструировании объекта этого типа. Следовательно, метод с параметрами типа может быть перегружен таким образом, что он окажется пригодным не для всех возможных случаев, хотя и будет вы глядеть верно.