Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2375 / 262 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 9:

Универсальность. Классы с родовыми параметрами

Наследование и встраивание. Совместное использование

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

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

Вот схема возможного решения этой проблемы:

public class Home
    {
        // описывает свойства дома        
    }
    public abstract class Transport
    {
        // описывает свойства транспортных средств 
    }
    public class Car : Transport
    {
        // описывает свойства автомобиля
    }
    public class Carriage : Transport
    {
        // описывает свойства вагона
    }
    public class Spaceship : Transport
    {
        // описывает свойства космического корабля
    }

Имея этот набор классов, нетрудно определить новые классы, объединяющие свойства существующих классов:

public class Home_Car : Home
    {
        Transport transport;
        public Home_Car(Transport transport)
        {
            this.transport = transport;
        }
    }
    public class Home_Carriage : Home
    {
        Transport transport;
        public Home_Carriage(Transport transport)
        {
            this.transport = transport;
        }
    }
    public class Home_Spaceship
    {
        Transport transport;
        public Home_Spaceship(Transport transport)
        {
            this.transport = transport;
        }
    }

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

public void TestSpace()
  {
      Spaceship spaceship = new Spaceship();
      Home_Spaceship home_ship = 
          new Home_Spaceship(spaceship);
      Console.WriteLine("И корабль плывет!");
  }

Предложение using

До сих пор рассматривалась ситуация родового порождения экземпляров универсального класса. Фактические типы задавались в момент создания экземпляра. Это наглядно показывает преимущества применяемой технологии, поскольку очевидно, что не создается дублирующий код для каждого класса, порожденного универсальным классом. И все-таки остается естественный вопрос: можно ли объявить класс путем подстановки фактических параметров в универсальный класс, а потом спокойно использовать этот класс обычным образом? Такая вещь возможна. Это можно сделать не совсем обычным путем - не в программном коде, а в предложении using, назначение которого и состоит в выполнении подобных подстановок. Предложение using не создает реальный класс. Это лишь форма сокращения записи, но содержательно его можно рассматривать как объявление конкретного класса.

Давайте вернемся к универсальному классу OneLinkStack<T>, введенному в начале этой лекции, и объявим класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:

using IntStack = ConsoleGeneric.OneLinkStack<int>;

Вот тест, в котором создаются несколько объектов этого класса:

public void TestIntStack()
      {
         IntStack stack1 = new IntStack();
         IntStack stack2 = new IntStack();
         IntStack stack3 = new IntStack();

         stack1.put(11); stack1.put(22);
         int x1 = stack1.item(), x2 = stack1.item();
         if ((x1 == x2) && (x1 == 22)) Console.WriteLine("OK!");
         stack1.remove(); x2 = stack1.item();
         if ((x1 != x2) && (x2 == 11)) Console.WriteLine("OK!");
         stack1.remove(); x2 = (stack1.empty()) ? 77 : stack1.item();
         if ((x1 != x2) && (x2 == 77)) Console.WriteLine("OK!");

         stack2.put(55); stack2.put(66);
         stack2.remove(); int s = stack2.item();
         if (!stack2.empty()) Console.WriteLine(s);

         stack3.put(333); stack3.put((int)Math.Sqrt(Math.PI));
         int res = stack3.item();
         stack3.remove(); res += stack3.item();
         Console.WriteLine("res= {0}", res);
      }

Все работает заданным образом, можете поверить.

Универсальность и специальные случаи классов

Универсальность - это механизм, воздействующий на все элементы языка. Поэтому он применим ко всем частным случаям классов C# .

Универсальные структуры

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

public struct Point<T>
   {
       T x, y;//координаты точки, тип которых задан параметром
      // другие свойства и методы структуры
   }

Универсальные интерфейсы

Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах универсальных интерфейсов библиотеки FCL - IComparable<T> и других. Введение универсальности, в первую очередь, сказалось на библиотеке FCL - внутренних классов, определяющих поведение системы. В частности, для большинства интерфейсов появились универсальные двойники с параметрами. Если бы в наших примерах мы использовали не универсальный интерфейс, а обычный, то потеряли бы в эффективности, поскольку сравнение объектов потребовало бы создание временных объектов типа object, выполнения операций boxing и unboxing.

Универсальные делегаты

Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в универсальном классе и использует в своем объявлении параметры универсального класса. Давайте рассмотрим ситуацию с делегатами более подробно. Вот объявление универсального класса, названного UniversalDelegates, в котором объявляется функциональный тип:

class UniversalDelegates<T>
    {
        public delegate T Unidel(T a, T b);
    }

Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется параметром класса Delegate.

Добавим в класс функцию высшего порядка FunAr, одним из аргументов которой будет функция типа Unidel, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:

public T FunAr(T[] arr, T a0, Unidel f)
      {
         T temp = a0;
         for(int i =0; i<arr.Length; i++)
         {
            temp = f(temp, arr[i]);
         }
         return (temp);
      }

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

Рассмотрим теперь клиентский класс Testing, в котором определен набор функций:

public int max2(int a, int b)
      { return (a > b) ? a : b; }
public double min2(double a, double b)
      { return (a < b) ? a : b; }
public string sum2(string a, string b)
      { return a + b; }
public float prod2(float a, float b)
      { return a * b; }

Хотя все функции имеют разные типы, все они соответствуют определению класса Unidel - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса Testing:

public void TestFun()
  {
      int[] ar1 = { 3, 5, 7, 9 };
      double[] ar2 = { 3.5, 5.7, 7.9 };
      string[] ar3 = { "Мама ", "мыла ", "Машу ", "мылом." };
      float[] ar4 = { 5f, 7f, 9f, 11f };
      UniversalDelegates<int> d1 = 
          new UniversalDelegates<int>();
      UniversalDelegates<int>.Unidel del1;
      del1 = this.max2;
      int max = d1.FunAr(ar1, ar1[0], del1);
      Console.WriteLine("max= {0}", max);

      UniversalDelegates<double> d2 = 
          new UniversalDelegates<double>();
      UniversalDelegates<double>.Unidel del2;
      del2 = this.min2;
      double min = d2.FunAr(ar2, ar2[0], del2);
      Console.WriteLine("min= {0}", min);

      UniversalDelegates<string> d3 = 
          new UniversalDelegates<string>();
      UniversalDelegates<string>.Unidel del3;
      del3 = this.sum2;
      string sum = d3.FunAr(ar3, "", del3);
      Console.WriteLine("concat= {0}", sum);

      UniversalDelegates<float> d4 = 
          new UniversalDelegates<float>();
      UniversalDelegates<float>.Unidel del4;
      del4 = this.prod2;
      float prod = d4.FunAr(ar4, 1f, del4);
      Console.WriteLine("prod= {0}", prod);
  }

Обратите внимание на объявление экземпляра делегата:

UniversalDelegates<int>.Unidel del1;

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

del1= this.max2;

При выполнении этого присваивания проверяется соответствие сигнатуры функции в правой части и экземпляра делегата, в случае успеха экземпляр делегата связывается с функцией.

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

public void TestLanbdaFun()
{
    int[] ar1 = { 3, 5, 7, 9 };
    double[] ar2 = { 3.5, 5.7, 7.9 };
    string[] ar3 = 
    { "Мама ", "мыла ", "Машу ", "мылом." };
    float[] ar4 = { 5f, 7f, 9f, 11f };
 
    UniversalDelegates<int> d1 =
        new UniversalDelegates<int>();   
    int max = d1.FunAr(ar1, ar1[0],
      (int a, int b)=> { return (a > b) ? a : b; });
    Console.WriteLine("max= {0}", max);

    UniversalDelegates<double> d2 =
        new UniversalDelegates<double>();    
    double min = d2.FunAr(ar2, ar2[0],
     (double a, double b)=> {return (a < b) ? a : b;});       
    Console.WriteLine("min= {0}", min);

    UniversalDelegates<string> d3 =
        new UniversalDelegates<string>();   
    string sum = d3.FunAr(ar3, "",
        (string a, string b)=> {return a + b; });
    Console.WriteLine("concat= {0}", sum);

    UniversalDelegates<float> d4 =
        new UniversalDelegates<float>();   
    float prod = d4.FunAr(ar4, 1f,
        (float a, float b) => {return a*b;});       
    Console.WriteLine("prod= {0}", prod);
}

Покажем, что и сам функциональный тип - делегат - можно объявлять с родовыми параметрами. Вот пример такого объявления:

public delegate T1 FunOneArg<T1>(T1 a);

Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:

FunOneArg<double> F = (double x) => 
    {return Math.Sin(x) + Math.Cos(x);};
    Console.WriteLine("x = {0}, Sin(x) + Cos(x) = {1}",
        5.0, F(5.0));

Вот как выглядят результаты работы тестового примера.

Результаты работы с универсальными делегатами

Рис. 8.6. Результаты работы с универсальными делегатами

Универсальные делегаты с успехом используются при определении событий. В частности класс EventHandler, применяемый для всех событий, которые не имеют собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:

public void delegate EventHandler<T> (object sender, T args)
               where T:EventArgs

Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дарья Федотова
Дарья Федотова
Юлия Кожина
Юлия Кожина
Украина
Анатолий Малоземов
Анатолий Малоземов
Россия, Магнитогрск