355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Герберт Шилдт » C# 4.0: полное руководство » Текст книги (страница 47)
C# 4.0: полное руководство
  • Текст добавлен: 6 апреля 2017, 04:00

Текст книги "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 : ISeries {

  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 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.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 : 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 для этой цели определен метод Со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(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(), который определяет равенство одного объекта другому.

Для определения относительного порядка следования двух элементов применяется интерфейс 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(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(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 : 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 {

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

  // Передать конструктору ссылку типа Т.

  public Gen(T о) {

    ob = о;

  }

  // Возвратить значение переменной ob.

  public T GetOb() {

    return ob;

  }

}

// Класс, производный от класса Gen. В этом классе

// определяется второй параметр типа V.

class Gen2 : Gen {

  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("Значение равно: ", 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 o, 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 обычным образом, т.е. без выполнения каких-то особых условий.


    Ваша оценка произведения:

Популярные книги за неделю