Текст книги "C# 4.0: полное руководство"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 47 (всего у книги 83 страниц)
Помимо обобщенных классов и методов, в C# допускаются обобщенные интерфейсы. Такие интерфейсы указываются аналогично обобщенным классам. Ниже приведен измененный вариант примера из главы 12, демонстрирующего интерфейс ISeries
. (Напомним, что ISeries
является интерфейсом для класса, генерирующего последовательный ряд числовых значений.) Тип данных, которым оперирует этот интерфейс, теперь определяется параметром типа.
// Продемонстрировать применение обобщенного интерфейса.
using System;
public interface ISeries
T GetNext(); // возвратить следующее по порядку число
void Reset(); // генерировать ряд последовательных чисел с самого начала
void SetStart(T v); // задать начальное значение
}
//Реализовать интерфейс ISeries.
class ByTwos
T start;
T val;
// Этот делегат определяет форму метода,
// вызываемого для генерирования
// очередного элемента в ряду последовательных значений.
public delegate T 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;
y = b;
z = c;
}
}
class GenIntfDemo {
// Определить метод увеличения на два каждого
// последующего значения типа 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
for(int i=0; i < 5; i++)
Console.Write(intBT.GetNext() + " ");
Console.WriteLine();
// Продемонстрировать генерирование
// последовательного ряда значений типа double.
ByTwos
dblBT.SetStart(11.4);
for(int i=0; i < 5; i++)
Console.Write(dblBT.GetNext() + " ");
Console.WriteLine();
// Продемонстрировать генерирование последовательного ряда
// значений координат объекта типа ThreeD.
ByTwos
ThreeD coord;
for(int i=0; i < 5; i++) {
coord = ThrDBT.GetNext();
Console.Write(coord.x + "," +
coord.y + "," +
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
Параметр типа Т указывается не только при объявлении класса 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
foreach(T v in obs)
if(v == what) // Ошибка! return true;
return false;
}
К сожалению, эта попытка не пройдет. Ведь параметр Т относится к обобщенному типу, и поэтому компилятору не удастся выяснить, как сравнивать два объекта. Требуется ли для этого поразрядное сравнение или же только сравнение отдельных полей? А возможно, сравнение ссылок? Вряд ли компилятор сможет найти ответы на эти вопросы. Правда, из этого положения все же имеется выход.
Для сравнения двух объектов параметра обобщенного типа они должны реализовывать интерфейс IComparable
или IComparable
и/или интерфейс IEquatable
. В обоих вариантах интерфейса IComparable
для этой цели определен метод СоmрагеТо()
, а в интерфейсе 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
foreach(T v in obs)
if(v.Equals(what)) // Применяется метод Equals().
return true;
return false;
}
Обратите внимание в приведенном выше примере на применение следующега ограничения.
where Т : IEquatable
Это ограничение гарантирует, что только те типы, в которых реализован интерфейс IEquatable
, являются действительными аргументами типа для метода IsIn()
. Внутри этого метода применяется метод Equals()
, который определяет равенство одного объекта другому.
Для определения относительного порядка следования двух элементов применяется интерфейс ICompагable
. У этого интерфейса имеются две формы: обобщенная и необобщенная. Обобщенная форма данного интерфейса обладает преимуществом обеспечения типовой безопасности, и поэтому мы рассмотрим здесь именно ее. Обобщенный интерфейс 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
if(what.CompareTo(obs[0]) < 0 ||
what.CompareTo(obs[obs.Length-1]) > 0) return false;
return true;
}
В приведенном ниже примере программы демонстрируется применение обоих методов IsIn()
и InRange()
на практике.
// Продемонстрировать применение обобщенных
// интерфейсов IComparable
using System;
// Теперь в классе MyClass реализуются обобщенные
// интерфейсы IComparable
class MyClass : IComparable
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
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
if (what.CompareTo(obs[0]) < 0 ||
what.CompareTo(obs[obs.Length – 1]) > 0)
return false;
return true;
}
// Продемонстрировать операции сравнения,
static void Main() {
// Применить метод Isln() к данным типа int.
int[] nums = { 1, 2, 3, 4, 5 };
if (IsIn(2, nums))
Console.WriteLine(«Найдено значение 2.»);
if (IsIn(99, nums))
Console.WriteLine(«He подлежит выводу.»);
// Применить метод Isln() к объектам класса MyClass.
MyClass[] mcs = { new MyClass(1), new MyClass(2),
new MyClass(3), new MyClass(4) };
if (IsIn(new MyClass(3), mcs))
Console.WriteLine(«Найден объект MyClass(3).»);
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
и IEquatable
.
–
Обобщенные классы могут входить в иерархию классов аналогично необобщенным классам. Следовательно, обобщенный класс может действовать как базовый или производный класс. Главное отличие между иерархиями обобщенных и необобщенных классов заключается в том, что в первом случае аргументы типа, необходимые обобщенному базовому классу, должны передаваться всеми производными классами вверх по иерархии аналогично передаче аргументов конструктора.
Применение обобщенного базового класса
Ниже приведен простой пример иерархии, в которой используется обобщенный базовый класс.
// Простая иерархия обобщенных классов,
using System;
// Обобщенный базовый класс,
class Gen
T ob;
public Gen(T о) {
ob = о;
}
// Возвратить значение переменной ob.
public T GetOb() {
return ob;
}
}
// Класс, производный от класса Gen.
class Gen2
public Gen2(T o) : base(o) {
// ...
}
}
class GenHierDemo {
static void Main() {
Gen2
Console.WriteLine(g2.GetOb());
}
}
В этой иерархии класс Gen2
наследует от обобщенного класса Gen
. Обратите внимание на объявление класса Gen2
в следующей строке кода.
class Gen2
Параметр типа Т указывается в объявлении класса Gen2
и в то же время передается классу Gen
. Это означает, что любой тип, передаваемый классу Gen2
, будет передаваться также классу Gen
. Например, в следующем объявлении:
Gen2
параметр типа string
передается классу Gen
. Поэтому переменная ob в той части класса Gen2
, которая относится к классу Gen
, будет иметь тип string
.
Обратите также внимание на то, что в классе Gen2
параметр типа Т не используется, а только передается вверх по иерархии базовому классу Gen
. Это означает, что в производном классе следует непременно указывать параметры типа, требующиеся его обобщенному базовому классу, даже если этот производный класс не обязательно должен быть обобщенным.
Разумеется, в производный класс можно свободно добавлять его собственные параметры типа, если в этом есть потребность. В качестве примера ниже приведен вариант предыдущей иерархии классов, где в класс Gen2
добавлен собственный параметр типа.
// Пример добавления собственных параметров типа в производный класс,
using System;
// Обобщенный базовый класс,
class Gen
T ob; // объявить переменную типа Т
// Передать конструктору ссылку типа Т.
public Gen(T о) {
ob = о;
}
// Возвратить значение переменной ob.
public T GetOb() {
return ob;
}
}
// Класс, производный от класса Gen. В этом классе
// определяется второй параметр типа V.
class Gen2
V ob2;
public Gen2(T o, V o2) : base(o) {
ob2 = o2;
}
public V GetObj2() {
return ob2;
}
}
// Создать объект класса Gen2.
class GenHierDemo2 {
static void Main() {
// Создать объект класса Gen2 с параметрами
// типа string и int.
Gen2 < string, int > x =
new Gen2
Console.Write(x.GetOb());
Console.WriteLine(x.GetObj2());
}
}
Обратите внимание на приведенное ниже объявление класса 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
T ob;
public Gen(T o, int i) : base(i) {
ob = o;
}
// Возвратить значение переменной ob.
public T GetOb() {
return ob;
}
}
// Создать объект класса Gen.
class HierDemo3 {
static void Main() {
// Создать объект класса Gen с параметром типа string.
Gen
Console.Write(w.GetOb() + " ");
Console.WriteLine(w.GetNum());
}
}
Эта программа дает следующий результат.
Привет 47
В данной программе обратите внимание на то, как класс Gen
наследует от класса NonGen
в следующем объявлении.
class Gen
Класс NonGen
не является обобщенным, и поэтому аргумент типа для него не указывается. Это означает, что параметр Т, указываемый в объявлении обобщенного производного класса Gen, не требуется для указания базового класса NonGen
и даже не может в нем использоваться. Следовательно, класс Gen наследует от класса NonGen
обычным образом, т.е. без выполнения каких-то особых условий.