Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2458 / 323 | Оценка: 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, дополненный нужными аргументами.

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

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

Добрый день!

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