Основы объектного программирования в классах на C# 3.0

Интерфейсы. Множественное наследование

Разбить на страницы
Показывать лекцию целиком

Проект к данной лекции Вы можете скачать здесь.

Интерфейсы

Слово " интерфейс " - многозначное, и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс - это частный случай класса. Интерфейс представляет собой полностью абстрактный класс, все методы которого абстрактны. От абстрактного класса интерфейс отличается некоторыми деталями в синтаксисе и поведении. Синтаксическое отличие состоит в том, что методы интерфейса объявляются без указания модификатора доступа. Отличие в поведении заключается в более жестких требованиях к потомкам. Класс, наследующий интерфейс, обязан полностью реализовать все методы интерфейса. В этом отличие от класса, наследующего абстрактный класс, где потомок может реализовать лишь некоторые методы родительского абстрактного класса, оставаясь абстрактным классом. Но, конечно, не ради этих отличий были введены интерфейсы в язык C#. У них своя важная роль.

Введение в язык частных случаев усложняет его и свидетельствует о некоторых изъянах, для преодоления которых и вводятся частные случаи. Например, введение структур в язык C# позволило определять классы как развернутые типы. Конечно, проще было бы ввести в объявление класса соответствующий модификатор, позволяющий любой класс объявлять развернутым. Но этого сделано не было.

Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования сталкивается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей - один полноценный класс, а остальные в виде интерфейсов, - в этом и состоит основное назначение интерфейсов.

Отметим еще одно важное назначение интерфейсов,отличающее их от абстрактных классов. Абстрактный класс представляет собой начальный этап проектирования класса, который в будущем получит конкретную реализацию. Интерфейсы задают дополнительные свойства класса. Один и тот же интерфейс позволяет описывать свойства, которыми могут обладать разные классы. Они похожи на сказочных фей, каждая из которых наделяет принцессу при ее рождении тем или иным свойством. В библиотеке FCL имеется большое число интерфейсов, наследуя которые, классы получают полезные дополнительные свойства. С некоторыми из них мы познакомимся в этой лекции.

Две стратегии реализации интерфейса

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

interface IStrings
 {
   /// <summary>
   /// Преобразование 
   /// </summary>
   /// <returns>результат преобразования</returns>
   string Convert();
   
   /// <summary>
   /// Шифрование 
   /// </summary>
   /// <param name="code">код шифра</param>
   /// <returns>результат шифрования</returns>
   string Cipher(string[] code);
 }

У этого интерфейса два метода, которые и должны будут реализовать все классы, наследники интерфейса. Как следует из спецификации, заданной в тегах summary, метод Convert должен, следуя алгоритму, выбранному потомком, преобразовать объект, возвращая строку, а метод Chiper, возвращающий строку, рассматривается как шифрование, в алгоритме которого используется массив строк code, переданный методу.

Реализация методов интерфейса как общедоступных методов

Класс, наследующий интерфейс и реализующий его методы, может объявить соответствующие методы класса открытыми. Заметьте, у методов интерфейса не заданы модификаторы доступа. Построим пример класса, наследующего интерфейс IStrings:

/// <summary>
    /// Наследует интерфейс IStrings,
    /// реализуя его методы как общедоступные (public)
    /// </summary>
    class SimpleText : IStrings
    {
   //поля класса
   string text;
   static string[] codeTable =
   {
     "абвгдеёжзийклмнопрстуфхцчшщыьъэюя ,.!?;:",
     "ъьыщшчцхфуэюя ,.!?;:тсрпонмлкйабвгдеёжзи"
   };
   //Конструкторы
   public SimpleText()
   {
       text = "Простой текст!";
   }
   public SimpleText(string txt)
   {
       text = txt;
   }
   public string Text
   {
       get { return text; }
   }
}

Построенный класс является обычным классом, содержащим текстовое поле text, конструкторы, метод - свойство, обеспечивающее доступ к закрытому полю. Но поскольку класс объявил себя наследником интерфейса IStrings, он обязан реализовать методы интерфейса, предложив некоторую их реализацию. Вот пример подобной реализации:

//Реализация интерфейсов
   /// <summary>
   /// Удаление пробелов в поле text, 
   /// преобразование к нижнему регистру 
   /// </summary>
   /// <returns>преобразованная строка</returns>
   public string Convert()
   {         
      string res = "";
       foreach (char sym in text)       
      if (sym != ' ') res += sym.ToString();
       res = res.ToLower();
       return res;
   }
   /// <summary>
   /// шифрование поля text
   /// с использованием таблицы кодировки символов
   /// </summary>
   /// <param name="code">таблица кодировки</param>
   /// <returns>зашифрованный текст</returns>
   public string Cipher(string[] code)
   {
       string res = "";
       foreach (char sym in text)
       {
      int k = code[0].IndexOf(sym);
      if (k >= 0) res += code[1][k];
      else res += sym.ToString();
       }
       return res;
   }

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

/// <summary>
   /// Проверка поля text,является ли он палиндромом
   ///  после преобразования Convert
   /// </summary>
   /// <returns> true, если палиндром</returns>
   public bool IsPalindrom()
   {
       string txt = Convert();
       for(int i=0, j = txt.Length-1; i<j; i++, j--)
      if(txt[i] != txt[j]) return false;
       return true;
   }
   /// <summary>
   /// Шифрование, заданное собственной таблицей кодировки 
   /// </summary>
   /// <returns>шифрованный текст</returns>
   public string Coding()
   {
       return Cipher(codeTable);
   }

Рассмотрим класс Testing, методы которого позволят выполнить тестирование объектов создаваемых нами классов:

/// <summary>
    /// тестирующий класс
    /// </summary>
    class Testing
    {
   //поля класса        
   const string PAL = 
       "А роза упала на лапу Азора";
   static string[] CODE =
   {
       "абвгдежзиклмнопрстуфхцчшыъьэюя",   
       "abvgdejziklmnoprstyfhc461w'qux"
   };
   /// <summary>
   /// Тестирование класса SimpleText
   /// </summary>
   public void TestText()
   {
       Console.WriteLine("Работа с объектом класса SimpleText! ");
       SimpleText simpleText = new SimpleText(PAL);
       Console.WriteLine("Исходный текст : " + PAL); 
       string text;
       text = simpleText.Convert();
       Console.WriteLine("Преобразованный текст : " + text); 
       if(simpleText.IsPalindrom())
      Console.WriteLine("Это палиндром!");
       text = simpleText.Coding();
       Console.WriteLine("Шифрованный текст : " + text);       

       Console.WriteLine("Работа с объектом интерфейса IStrings! ");
       IStrings istrings;
       text = "Это простой текст!";
       Console.WriteLine("Исходный текст : " + text);
       simpleText = new SimpleText(text);
       istrings = (IStrings)simpleText;       
       text = istrings.Convert();
       Console.WriteLine("Преобразованный текст : " + text);
       text = istrings.Cipher(CODE);
       Console.WriteLine("Шифрованный текст : " + text);       
   }
    }

Заметьте: в классе Testing объявлен как объект класса SimpleText, так и объект istrings интерфейса IStrings. В методе TestText объект simpleText создается обычным способом при вызове конструктора класса. Затем этот объект вызывает как открытый метод Convert, наследованный от интерфейса, так и открытые методы класса IsPalindrom, Coding, использующие методы интерфейса. Все это совершено понятно и ожидаемо.

Более интересен тот факт, что можно работать с реализованными в классе методами интерфейса через объект istrings. Создать объект интерфейса путем вызова конструктора невозможно, но по отношению к классу SimpleText это объект родительского класса, и потому объекту интерфейса можно присвоить объект класса после соответствующего приведения типа. Такому объекту доступны методы, заданные интерфейсом, и только они. Результаты работы теста показаны на рис. 5.1.

(рис 5.1) Реализация методов интерфейса как открытых методов класса

Реализация методов интерфейса как закрытых методов

Другая стратегия реализации состоит в том, чтобы все или некоторые методы интерфейса сделать закрытыми. Для реализации этой стратегии класс, наследующий интерфейс, объявляет методы без модификатора доступа, что по умолчанию соответствует модификатору private, и уточняет имя метода именем интерфейса. Давайте создадим класс с именем PrivateInterfaces, являющийся аналогом только что рассмотренного класса SimpleText. В этом классе методы наследуемого интерфейса будут реализованы как закрытые для клиентов методы. Полного описания этого класса приводить не буду, ограничусь лишь некоторыми фрагментами. Вот как выглядят заголовки методов, реализующих методы интерфейса:

//Реализация интерфейсов как закрытых методов   
   string IStrings.Convert()
   string IStrings.Cipher(string[] code)

Модификатор доступа стал по умолчанию private, а имя метода уточнено именем интерфейса. Интересно то, как теперь эти методы вызываются в методах класса. Их нельзя, как ранее, вызвать по имени или даже по полному имени. Нужно явно задать цель вызова. В качестве цели следует взять текущий объект this и привести его к типу интерфейса, так что получается вот такая сложная конструкция целевого объекта:

((IStrings)this)

Соответствующие строки кода в методах класса IsPalindrom и Coding выглядят теперь так:

string txt = ((IStrings)this).Convert();
return ((IStrings)this).Cipher(codeTable);

Класс PrivateInterface реализовал методы интерфейса IStrings, но сделал их закрытыми и недоступными для клиентов и наследников класса. Как же им получить доступ к закрытым методам? Есть два способа решения этой проблемы.

  • Обертывание. В классе, реализующем интерфейс, создается открытый метод, являющийся оберткой закрытого метода.
  • Кастинг. У клиента создается объект интерфейсного класса IStrings, полученный приведением (кастингом) объекта класса PrivateInterface. Этому объекту доступны закрытые методы интерфейса.
  • Обертывание - это широко распространенный прием в ООП. Под обертыванием понимают создание открытых методов класса, содержащих вызовы старых или недоступных клиентам методов. Цели обертывания могут быть различными. Например, имеется библиотека процедур, написанная на языке С или другом не объектном языке программирования. Эта библиотека весьма полезна, но недоступна в существующем виде. Использование обертывания позволяет обернуть ее в одежды класса, а каждую процедуру библиотеки превратить в метод класса. Иногда обертывание старого метода делается с целью изменения его интерфейса, сделав его более современным.

    Фактически обертывание уже применялось при создании методов IsPalindrom и Coding. Давайте выполним обертку закрытых методов интерфейса, полностью сохранив их интерфейс, но изменив имена:

    //Обертка методов интерфейса с переименованием
       public string ClassConvert()
       {
           return ((IStrings)this).Convert();
       }
       public string ClassCipher(string[] code)
       {
           return ((IStrings)this).Cipher(code);
       }

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

    У клиентов класса есть возможность добраться до закрытых методов интерфейса, даже если класс и не создал открытых методов путем обертки. Для этой цели клиент может использовать объявление объектов интерфейса. Создать объект класса интерфейса обычным путем с использованием конструктора и операции new нельзя. Тем не менее можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми.

    Построим в классе Testing тест, аналогичный тесту TestText, для работы с объектом класса PrivateInterface.

    /// <summary>
       /// тестирование класса PrivateInterface
       /// </summary>
       public void TestTextPrivateInterface()
       {
           Console.WriteLine("Работа с объектом privateInterface! ");
           PrivateInterface privateInterface = new PrivateInterface(PAL);
           Console.WriteLine("Исходный текст : " + PAL);
           string text;
           text = privateInterface.ClassConvert();
           Console.WriteLine("Преобразованный текст : " + text);
           if (privateInterface.IsPalindrom())
               Console.WriteLine("Это палиндром!");
           text = privateInterface.Coding();
           Console.WriteLine("Шифрованный текст : " + text);
    
           Console.WriteLine("Работа с объектом интерфейса IStrings! ");
           IStrings istrings;
           text = "Это простой текст!";
           Console.WriteLine("Исходный текст : " + text);
           privateInterface = new PrivateInterface(text);
           istrings = (IStrings)privateInterface;
           text = istrings.Convert();
           Console.WriteLine("Преобразованный текст : " + text);
           text = istrings.Cipher(CODE);
           Console.WriteLine("Шифрованный текст : " + text);
       }

    В этом тесте объект класса privateInterface вызывает метод интерфейса под новым именем ClassConvert, а интерфейсный объект istrings вызывает методы интерфейса, несмотря на то, что они закрыты, под собственными именами. Результаты работы теста показаны на. рис. 5.2.

    (рис 5.2) Реализация методов интерфейса как закрытых методов класса

    Множественное наследование и его проблемы

    Множественным наследованием называется ситуация, когда класс объявляет N классов ( N > 1 ) своими непосредственными родителями. В языке C# есть ограничения на множественное наследование. Ситуация здесь такая. Во-первых, у каждого класса родителем, хотя не всегда непосредственным, является класс object. Во-вторых, каждый класс может явно объявить один класс в качестве непосредственного родителя, а также объявить непосредственными родителями один или несколько интерфейсов. Таким образом, в C# допускается множественное наследование интерфейсов и одиночное (не считая наследования от класса object ) наследование классов.

    Во многом ограничение множественного наследования классов связано с тем, что оно создает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы - коллизию имен и наследование от общего предка.

    Коллизия имен

    Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Если сигнатуры разные, то это не приводит к конфликтам. Класс реализует методы обоих интерфейсов, и у него просто появляются перегруженные методы.

    Но что следует делать классу наследнику в тех случаях, когда сигнатуры методов совпадают? Возможны две стратегии - склеивание методов и переименование.

    Стратегия склеивания применяется тогда, когда класс - наследник интерфейсов - полагает, что методы разных интерфейсов, имеющие одинаковое имя и сигнатуру, задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную ( public ) реализацию, соответствующую методам всех интерфейсов c единой сигнатурой.

    Стратегия переименования исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей - они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы уже знаем, как производить переименование метода интерфейса в самом классе наследника. Для этого достаточно реализовать в классе методы разных интерфейсов как закрытые, а затем открыть их в классе с переименованием.

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

    Наследование от общего предка

    Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B, а те, в свой черед, являются наследниками класса P, то класс наследует свойства и методы своего предка P дважды: один раз получая их от класса A, другой - от B. Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации. Ситуация дублирующего наследования показана на рис. 5.3.

    (рис 5.3) Дублирующее наследование

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

    Склеивание и переименование

    Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса - наследника этих интерфейсов, применяющего разные стратегии для конфликтующих методов. У нас уже определен интерфейс IStrings. Предположим, что существует интерфейс ITransform, подобный Istrings:

    interface ITransform
      {
       /// <summary>
       /// Преобразование 
       /// </summary>
       /// <returns>результат преобразования</returns>
       string Convert();
    
       /// <summary>
       /// Шифрование  
       /// </summary>
       /// <param name="code">код </param>
       /// <returns>результат шифрования</returns>
       string Cipher(string[] code);        
      }

    У этих интерфейсов имена и сигнатуры методов совпадают. Вот класс, наследующий оба интерфейса:

    /// <summary>
        /// Наследник двух интерфейсов,
        /// у методов которых Convert и Cipher
        /// сигнатуры совпадают.
        /// Методы Cipher склеиваются, 
        /// Convert - переименовываются
        /// </summary>
        class TwoInterfaces:IStrings,ITransform
        {
            //Опущена часть класса, общая с классом SimpleText
            //Реализация интерфейсов                
            string IStrings.Convert()
            {            
                string res = "";
                foreach (char sym in text)            
                    if (sym != ' ') res += sym.ToString();
                res = res.ToLower();
                return res;
            }
            string ITransform.Convert()
            {            
                string res = "";
                for (int i = text.Length - 1; i >= 0; i--)
                    res += text[i];
                return res;
            }
            //Переименование закрытых методов
            public string ConvertOne()
            {
                return ((IStrings)this).Convert();
            }
            public string ConvertTwo()
            {
                return ((ITransform)this).Convert();
            }
            //Склеивание метода Cipher двух интерфейсов
            public string Cipher(string[] code)
            {
                string s = text;
                string res = "";
                foreach (char sym in s)
                {
                    int k = code[0].IndexOf(sym);
                    if (k >= 0) res += code[1][k];
                    else res += sym.ToString();
                }
                return res;
            }
        }

    Для методов Cipher двух интерфейсов выбрана стратегия склеивания. Для методов Convert выбрана стратегия переименования. Методы интерфейсов реализованы как закрытые методы, а затем в классе объявлены два новых метода с разными именами, являющиеся обертками закрытых методов класса.

    Приведу пример работы с объектами класса и интерфейсными объектами:

    public void TestTextTwoInterfaces()
      {
          Console.WriteLine("Работа с объектом класса TwoInterfaces! ");
          TwoInterfaces twoInterfaces = new TwoInterfaces(PAL);
          Console.WriteLine("Исходный текст : " + PAL);
          string text;
          text = twoInterfaces.ConvertOne();
          Console.WriteLine("Первое преобразование  : " + text);
          if (twoInterfaces.IsPalindrom())
              Console.WriteLine("Это палиндром!");
          text = twoInterfaces.ConvertTwo();
          Console.WriteLine("Второе преобразование  : " + text);
          text = twoInterfaces.Coding();
          Console.WriteLine("Шифрованный текст : " + text);
          
          text = "Это простой текст!";
          Console.WriteLine("Исходный текст : " + text);
          twoInterfaces = new TwoInterfaces(text);
          IStrings istrings;
          ITransform itransform;
          istrings = (IStrings)twoInterfaces;
          itransform = (ITransform)twoInterfaces;
    
          Console.WriteLine("Работа с объектом интерфейса IStrings!");
          text = istrings.Convert();
          Console.WriteLine("Преобразованный текст : " + text);
          text = istrings.Cipher(CODE);
          Console.WriteLine("Шифрованный текст : " + text);
    
          Console.WriteLine("Работа с объектом интерфейса ITransform!");      
          text = itransform.Convert();
          Console.WriteLine("Преобразованный текст : " + text);
          text = itransform.Cipher(CODE);
          Console.WriteLine("Шифрованный текст : " + text);
      }

    Результаты работы показаны на рис. 5.4.

    (рис 5.4) Решение проблемы коллизии имен

    Интерфейсы и поля

    Можно ли в интерфейсе объявлять не только методы, но и поля, обязательные для реализации потомками интерфейса? Ответ не однозначный: и да, и нет. Нет - потому что в явном виде в интерфейсе нельзя объявить поле. Да - потому что в интерфейсе можно объявить метод-свойство с процедурами get и set, обеспечивающими доступ к полю.

    Вот пример такого интерфейса:

    /// <summary>
        /// Доступ к полям Name и Age 
        /// </summary>
        interface IFields
        {
            string Name { get; set; }
            int Age {get;}
        }

    Создадим класс, наследующий этот интерфейс:

    /// <summary>
        /// Класс, наследующий интерфейс IFields
        /// Имеет поля Name и Age,
        /// доступ к полю Name открыт клиентам класса,
        /// доступ к полю Age закрыт и открыт с переименованием!  
        /// </summary>
        class TwoFields:IFields
        {
            string name;
            int age;
            public TwoFields()
            {
                name = "Nemo"; age = 37;
            }
            public TwoFields(string name, int age)
            {
                this.name = name; this.age = age;
            }
            public string Name
            {
                get { return name; }
                set { name = value; }
            }
            int IFields.Age
            {
                get { return age; }
            }
            public int WhatAge()
            {
                return age;
            }
        }

    Обратите внимание: методы доступа к полям, наследуемые от интерфейса, можно реализовать как открытые и как закрытые, открывая их затем под другим именем. У класса всегда должна быть возможность переименовывать имена методов и полей, наследуемых от интерфейса.

    Добавим в класс Testing новый метод, позволяющий протестировать работу с наследуемыми полями.

    public void TestFields()
      {
          Console.WriteLine("Работа с объектом класса TwoFields!");
          TwoFields twofields = new TwoFields();
          Console.WriteLine("Имя: {0}, Возраст: {1}",
              twofields.Name, twofields.WhatAge());
          twofields.Name = "Captain Nemo";
          Console.WriteLine("Работа с интерфейсным объектом  IFields!");
          IFields ifields = (IFields)twofields;
          Console.WriteLine("Имя: {0}, Возраст: {1}",
              ifields.Name,ifields.Age);
      }

    Результаты работы теста показаны на рис. 5.5.

    (рис 5.5) Поля, наследуемые от интерфейса IFields

    Встроенные интерфейсы

    Рассмотрим несколько встроенных интерфейсов, являющихся частью библиотеки FCL. Они используются многими классами библиотеками, так же, как и классами, создаваемыми пользователем.

    Упорядоченность объектов и интерфейс IComparable

    Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса IComparable. Этот интерфейс имеет всего один метод CompareTo (object obj), возвращающий целочисленное значение, положительное, отрицательное или равное нулю, в зависимости от выполнения отношения "больше", "меньше" или "равно".

    Как правило, в классе вначале определяют метод CompareTo, а после этого вводят перегруженные операции, чтобы выполнять сравнение объектов привычным образом с использованием знаков операций отношения.

    Давайте введем отношение порядка на классе Person, рассмотренном в лекции 1, сделав этот класс наследником интерфейса IComparable. Реализуем в этом классе метод интерфейса CompareTo:

    public class Person:IComparable
       {
          public int CompareTo( object pers)
          {
             const string s = "Сравниваемый объект не принадлежит классу Person";
             Person p = pers as Person;
             if (!p.Equals(null))
                return (fam.CompareTo(p.fam));
             throw new ArgumentException (s);
          }
          // другие компоненты класса
       }

    Поскольку аргумент в методе интерфейса принадлежит типу object, перед выполнением сравнения его нужно привести к типу Person. Для приведения используется операция as, позволяющая проверить корректность выполнения приведения. Если приведение невозможно, то невозможно выполнить и сравнение объектов. В этом случае выбрасывается исключение, которое может обработать разумным образом только клиент класса, пытавшийся выполнить сравнение. В самом классе Person можно только пояснить ситуацию, передав информацию объекту исключения.

    Заметьте также, что при проверке на значение null используется отношение Equals, а не обычное равенство, которое будет переопределено.

    Приведение типов и операции as и is

    Давно пора привести отложенное пояснение операций is и as, полезных при работе с объектами, тип которых может быть не определен. Операция is используется в логических выражениях. Логическое выражение

    obj is T

    истинно, если объект obj принадлежит типу T, и ложно в противном случае.

    Оператор присваивания

    obj = P as T;

    присваивает объекту obj объект P, приведенный к типу T, если такое приведение возможно, иначе объекту присваивается значение null. Семантику as можно выразить следующим условным выражением:

    (P is T) ? (T)P : (T)null

    Порядок на классе Person

    Определив метод CompareTo для класса Person, мы тем самым ввели отношение порядка для объектов этого класса. Конечно, сравнение персон может выполняться по разным критериям: возрасту, росту, зарплате. В данном случае отношение порядка на объектах класса Person задается как отношение порядка на фамилиях персон. Так как строки наследуют интерфейс IComparable, для фамилий персон вызывается метод CompareTo, его результат и возвращается в качестве результата метода CompareTo для персон.

    Введем теперь в нашем классе Person перегрузку операций отношения:

    public static bool operator <(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) < 0);
     }
     public static bool operator >(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) > 0);
     }
     public static bool operator <=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) <= 0);
     }
     public static bool operator >=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) >=0);
     }
     public static bool operator ==(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) == 0);
     }
     public static bool operator !=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) != 0);
     }

    Как обычно, приведу тестовый пример, проверяющий работу с введенными методами:

    public void TestCompare()
     {
        Person poet1 = new Person("Пушкин");
        Person poet2 = new Person("Лермонтов");
        Person poet3 = new Person("Пастернак");
        Person poet4 = new Person("Мандельштам");
        Person poet5 = new Person("Ахматова");
        Person poet6 = new Person("Цветаева");
        Console.WriteLine("{0} > {1} = {2}", poet1.Fam,
           poet2.Fam, (poet1 > poet2));
        Console.WriteLine("{0} >= {1} = {2}", poet3.Fam,
           poet4.Fam, (poet3 >= poet4));
        Console.WriteLine("{0} != {1} = {2}", poet5.Fam,
           poet6.Fam, (poet5 != poet6));
      }

    Вот результаты работы этого теста.

    (рис 5.6) Сравнение персон

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

    Клонирование и интерфейс ICloneable

    При ссылочном присваивании x = y, где x и y - объекты класса T, как уже много раз говорилось, происходит присваивание ссылок. Если до присваивания ссылка y была связана с объектом в динамической памяти, то после присваивания x будет ссылаться на этот же объект. С самим объектом ничего не происходит, никакая копия этого объекта не создается. Иногда требуется создать копию объекта, так, чтобы x и y ссылались на разные объекты.

    Клонированием называется процесс создания копии объекта, а копия объекта называется клоном. Различают два типа клонирования: поверхностное ( shallow ) и глубокое ( deep ). При поверхностном клонировании копируется только один объект, копию которого необходимо создать. Все значимые поля клона получают значения, совпадающие со значениями полей объекта; все ссылочные поля клона являются ссылками на те же объекты, на которые ссылается и сам объект.

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

    Глубокое клонирование требует рекурсивной процедуры обхода существующей структуры объектов, тщательно отработанной во избежание проблемы зацикливания. В общем случае, когда есть несколько классов, являющихся взаимными клиентами, глубокое клонирование требует наличия в каждом классе рекурсивной процедуры. Эти процедуры взаимно вызывают друг друга. Хорошим упражнением является создание проекта, реализующего глубокое клонирование для ситуации, когда объекты разных классов связаны взаимными ссылками.

    Поверхностное клонирование можно выполнить достаточно просто, поскольку оно автоматически поддерживается для всех классов, в том числе, создаваемых программистом. Для создания поверхностного клона достаточно вызвать метод MemberwiseClone, наследуемый от прародителя object. Единственное, что нужно помнить: этот метод защищен, он не может быть вызван непосредственно клиентом класса. Если класс хочет обеспечить возможность создания клона клиентами класса, ему необходимо в классе создать метод, представляющий собой обертку метода MemberwiseClone.

    Давайте обеспечим эту возможность для класса Person, создав в нем соответствующий метод:

    public Person StandartClone()
     {
        Person p = (Person)this.MemberwiseClone();
         return(p);
      }

    Теперь клиенты класса могут легко создавать поверхностные клоны. Вот пример:

    public void TestStandartClone()
      {
         Person mother = new Person("Петрова Анна");
         Person daughter = new Person("Петрова Ольга");
         Person son = new Person("Петров Игорь");
         mother[0] = daughter;
         mother[1] = son;
         Person mother_clone = mother.StandartClone();
         Console.WriteLine("Дети матери: {0}",mother.Fam);
         Console.WriteLine (mother[0].Fam);
         Console.WriteLine (mother[1].Fam);
         Console.WriteLine("Дети клона: {0}",mother_clone.Fam);
         Console.WriteLine (mother_clone[0].Fam);
         Console.WriteLine (mother_clone[1].Fam);   
       }

    При создании клона будет создана копия только одного объекта mother. Обратите внимание: при работе с полем children, задающим детей, используется индексатор класса Person, выполняющий индексацию по этому полю. Вот как выглядят результаты работы теста.

    (рис 5.7) Поверхностное клонирование

    Если стандартное поверхностное клонирование нас не устраивает, то класс можно объявить наследником интерфейса ICloneable и реализовать метод Clone - единственный метод этого интерфейса. В нем можно реализовать полное глубокое клонирование или подходящую для данного случая модификацию.

    Давайте расширим класс Person, сделав его наследником интерфейса ICloneable. Реализация метода Clone будет отличаться от стандартной реализации тем, что к имени объекта - полю Fam - будет приписываться слово " clone ". Вот как выглядит этот метод:

    public object Clone()
        {
        Person clone = (Person)this.MemberwiseClone();
        clone.fam = "clone_" + fam;
        return clone;
        }

    Эта реализация является слегка модифицированной версией стандартного поверхностного клонирования. Я добавил несколько строчек в тестирующую процедуру для проверки работы этой версии клона:

    Person mother_clone2 = (Person)mother.Clone();
      Console.WriteLine("Дети клона_2: {0}",mother_clone2.Fam);
      Console.WriteLine (mother_clone2[0].Fam);
      Console.WriteLine (mother_clone2[1].Fam);

    Все работает должным образом.

    Перечислимость объектов и интерфейсы

    Если класс можно рассматривать как некоторый контейнер перечислимых объектов, то для того, чтобы класс мог возвращать эти объекты в цикле for each, класс должен быть наследником интерфейса IEnumerable или иметь в своем составе итераторы - методы, возвращающие результат типа IEnumerable.

    Интерфейс перечислимости IEnumerable

    У этого интерфейса всего один метод - GetEnumerator(). У метода нет никаких входных аргументов, так что, кажется, организовать перечислимость объектов класса достаточно просто - написать реализацию одного метода. Это действительно просто, но требует понимания процесса перечислимости. Первая сложность состоит в том, что метод GetEnumerator синтаксически определен следующим образом:

    IEnumerator GetEnumerator()

    Это означает, что в результате вызова метода должен возвращаться интерфейсный объект, принадлежащий интерфейсу IEnumerator - еще одному интерфейсу, связанному с перечислимостью. Метод GetEnumerator требует создания объекта перечислителя - объекта, реализующего методы интерфейса IEnumerator. У интерфейса IEnumerator несколько методов, и в совокупности именно они и позволяют организовать процесс перечисления.

    Как же получить интерфейсный объект типа IEnumerator? В принципе эту задачу мы уже умеем решать, что неоднократно демонстрировалось в примерах этой лекции. Нужно объявить интерфейсный объект, затем создать объект класса, являющегося наследником интерфейса IEnumerator, и далее интерфейсный объект связать с объектом класса, используя приведение типа. Но такой подход все равно требует создания объекта, реализующего методы интерфейса IEnumerator, что не так просто.

    К счастью, во многих ситуациях есть более простой путь. Пусть нам нужно организовать перечислимость объектов некоторого класса. У этого класса есть поля, являющиеся объектами, для которых перечислимость уже обеспечена, например, одним из полей класса является массив. Все классы массивы независимо от типа элементов реализуют перечислимость элементов массива, и для них определен метод GetEnumerator. Тогда метод GetEnumerator, который необходимо создать для класса, будет сводиться к вызову метода GetEnumerator для поля класса, представляющего собой перечислимый контейнер элементов. В этом случае все будет совсем просто.

    Давайте построим пример, моделирующий такой подход. Добавим в наш проект новый класс, одним из полей которого будет массив объектов Person. Организуем перечислимость в этом классе, сводящуюся к перечислимости персон массива.

    /// <summary>
        /// Класс - наследник интерфейса IEnumerable,
        /// допускающий перечислимость объектов.
        /// Перечислимость сводится к перечислимости персон,
        /// заданных полем container
        /// </summary>
        class Persons:IEnumerable
        {
        protected int size;
        protected Person[] container;
        Random rnd = new Random(); 
        /// <summary>
        /// Конструктор по умолчанию.
        /// Создает массив из 10 персон
        /// </summary>
        public Persons()
        {
        size = 10;
        container = new Person[size];
        FillContainer(); 
       }
        /// <summary>
        /// Конструктор. Создает массив заданной размерности
        /// Создает его элементы, используя рандомизацию.
        /// </summary>
        /// <param name="size">размерность массива</param>
        public Persons(int size)
        {
        this.size = size;
        container = new Person[size];
        FillContainer();
        }
        /// <summary>
        /// Конструктор, которому передается массив персон
        /// </summary>
        /// <param name="container"> массив Person</param>
        public Persons(Person[] container)
        {
        this.container = container;
        size = container.Length;
        }
        /// <summary>
        /// Заполнение массива person 
        /// </summary>
        void FillContainer()
        {
        for(int i = 0; i <size; i++)
        {
            int num = rnd.Next(3*size);
            int age = rnd.Next(27, 46);
            container[i] = new Person("агент_" + num, age);
        }
        }    
        }

    Созданный класс Persons устроен просто. У него есть поле, названное container и представляющее собой массив с элементами класса Person. Набор конструкторов класса позволяет передать классу массив персон либо поручить самому классу его формирование, используя метод FillContainer. Поскольку класс объявлен наследником интерфейса IEnumerable, необходимо реализовать метод GetEnumerator, что обеспечит перечислимость объектов класса и даст возможность использовать цикл for each при работе с объектами класса. В данном случае реализовать метод интерфейса несложно, и вот как выглядит его реализация:

    /// <summary>
        /// Реализация метода интерфейса IEnumerable
        /// Сводится к вызову соответствующего метода 
        /// для поля container - массива,
        /// для которого этот метод реализован в библиотеке FCL 
        /// </summary>
        /// <returns>перечислитель - интерфейсный объект</returns>
        public IEnumerator GetEnumerator()
        {
        return container.GetEnumerator();
        }

    На описание метода в теге summary потребовалось больше времени и строчек текста, чем на тело метода, состоящее из одной строчки. Но на описание не стоит жалеть усилий! Задача решена, и можно попробовать протестировать работу с объектами класса. Добавим в класс Testing соответствующий метод:

    public void TestEnumeration()
      { 
       int size = 10;    
       Persons agents = new Persons(size);
       foreach (Person agent in agents)
           Console.WriteLine(agent.ToString());
      }

    В тесте создается объект класса Persons, и в цикле foreach объекты перебираются, возвращая каждый раз очередной объект класса Person. Метод ToString, определенный в классе Person, позволяет выводить информацию об объектах. Результаты работы теста показаны на рис. 5.8.

    (рис 5.8) Перечислимость и метод GetEnumerator

    Итераторы

    Нам удалось достаточно просто организовать перечислимость в классе за счет того, что в классе был определен контейнер со встроенным методом GetEnumerator. А что, если такого контейнера нет, или таких контейнеров несколько и хотелось бы организовать перечисление по каждому из контейнеров? Для решения подобных задач в язык C#, начиная с версии 2.0, встроен мощный механизм итераторов, существенно облегчающих задачу перечислимости объектов класса и открывающих новые возможности, которые нельзя реализовать только за счет наследования интерфейса IEnumerable.

    Итератором называется метод класса, возвращающий в качестве результата интерфейсный объект типа IEnumerable. В теле итератора должен присутствовать оператор языка yield, имеющий следующий синтаксис:

    yield return <выражение> ;

    При выполнении итератора автоматически создается контейнер, в который добавляется объект при каждом выполнении оператора yield. Добавляемый в контейнер элемент определяется выражением оператора yield. Порядок выполнения операторов yield определяет порядок перечислимости элементов контейнера. Оператор yield может быть задан в форме

    yield break;

    В такой форме он сигнализирует об окончании заполнения контейнера элементами.

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

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

    /// <summary>
     /// Перечисление элементов массива container
     /// в обратном порядке
     /// </summary>
     /// <returns>интерфейсный объект</returns>
     public IEnumerable ReverseIterator()
     {
         for (int i = container.Length - 1; i >= 0; i--)
             yield return container[i];
     }

    Оператор yield, работающий в цикле, первым возвратит последний элемент массива, а последним - первый.

    Создадим еще один итератор, никак не связанный с элементами массива поля container. Зададим перечисление, содержащее имена известных языков программирования:

    public enum ProgrammingLanguages
        {
            Fortran, Algol, Cobol, Simula, Pascal,
            Ada, C, CPlusPlus, Eiffel, Java, CSharp
        }

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

    /// <summary>
     /// Итератор. Создает объект перечисления
     /// и строит контейнер, 
     /// помещая в него элементы перечисления
     /// </summary>
     /// <returns>интерфейсный объект</returns>
     public IEnumerable LanguagesIterator()
     {
         ProgrammingLanguages lang;
         int i = 0;
         while (true)
         {
             lang = (ProgrammingLanguages)i;
      yield return lang;
      if (lang.ToString() == "CSharp") yield break;  
      i++;
         }
     }

    В цикле оператор yield возвращает элементы перечисления до тех пор, пока не встретится элемент со значением "CSharp". Давайте теперь протестируем то, что у нас получилось. Добавим несколько строк кода в уже рассмотренный метод TestEnumeration:

    Console.WriteLine("Обратный порядок перечисления");
     foreach(Person agent in agents.ReverseIterator())
         Console.WriteLine(agent.ToString());
     Console.WriteLine("Перечисление языков программирования");
             foreach (ProgrammingLanguages lang in
                               agents.LanguagesIterator())
         Console.WriteLine(lang.ToString());

    Обратите внимание: когда клиент организует цикл foreach, он указывает не только имя объекта класса, но и имя итератора, организующего перечислимость. Результаты работы теста показаны на рис. 5.9.

    (рис 5.9) Перечислимость и итераторы

    Интерфейс IEnumerator

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

    Реализация в классе свойств и методов этого интерфейса позволяет клиентам класса самостоятельно организовать работу с объектами класса как с коллекцией. Обычно это не делается, а используются возможности, предоставляемые циклом for each, который скрывает детали применения свойств и методов интерфейса.

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

    Метод bool MoveNext () передвигает курсор к следующему элементу списка, так что после его первого вызова курсор будет указывать на первый элемент списка. После его вызова, когда курсор указывает на последний элемент списка, текущий элемент снова становится неопределенным, а позиция курсора смещается за последний элемент. Метод MoveNext возвращает значение true, когда курсор указывает на элемент списка, и возвращает false, когда курсор вне списка элементов.

    Метод void Reset() возвращает курсор в начальную позицию перед первым элементом списка.

    Метод-свойство Object Current { get } возвращает текущий элемент списка. Выбрасывается исключение InvalidOperationException,если при вызове свойства Current курсор установлен перед или после списка элементов.

    Приведу теперь пример класса, реализующего интерфейс IEnumerator. Давайте построим класс People, который будет наследником класса Persons и интерфейса IEnumerator. Будучи наследником класса Persons, новый класс будет обладать всеми возможностями родительского класса, в том числе перечислимостью объектов. Но как наследник интерфейса IEnumerator он реализует методы интерфейса и предоставит своим клиентам новые возможности.

    /// <summary>
        /// Наследник класса Persons
        /// Реализует методы интерфейса IEnumerator
        /// Предоставляет клиентам класса возможность
        /// собственной организации перебора объектов
        /// </summary>
        class People:Persons,IEnumerator
        {        
            int position = -1;
            public People() : base() { }
            public People(int size): base(size) { }
            public People(Person[] people) : base(people) { }
    
            /// <summary>
            /// Передвигает курсор к следующему элементу в контейнере
            /// Возвращает true, если курсор установлен на элементе списка,
            /// false - в противном случае
            /// </summary>
            /// <returns></returns>
            public bool MoveNext()
            {
                if (position < size) position++;
                return (position < size);
            }
            /// <summary>
            /// Восстанавливает текущее положение курсора 
            /// в контейнере объектов 
            /// </summary>
            public void Reset()
            {
                position = -1;
            }
            /// <summary>
            /// Возвращает текущий объект, если курсор установлен 
            /// на элементе списка
            /// Выбрасывает исключение InvalidOperationException,
            /// когда курсор вне списка
            /// </summary>        
            public object Current
            {
                get
                {
                    try
                    {
                        return container[position];
                    }
    
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
                set
                {
                    try
                    {
                        container[position] = (Person)value;
                    }
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
            }

    У класса три собственных конструктора, которые сводятся к вызову конструкторов родителя, поле position, необходимое для организации доступа, и три открытых метода, реализующих методы интерфейса IEnumerator. Методы устроены достаточно просто и в особых комментариях не нуждаются. Но стоит обратить внимание на реализацию метода-свойства Current. В самом интерфейсе для этого свойства указан только метод get. В реализации я добавил и реализацию метода set. Это сразу же расширяет возможности клиента. Если при работе с методом foreach объекты доступны только для чтения, то теперь свойство Current позволяет изменять элементы коллекции. Тест, построенный для работы с новым классом People, демонстрирует все эти возможности.

    public void TestPeople()
     {     
         People people = new People();     
         foreach (Person person in people)
             Console.WriteLine(person.ToString());
         
         Console.WriteLine("Работа клиента с коллекцией!");     
         try
         {
             Person person;
             person = new Person("James Bond", 44);
             people.MoveNext();
             people.Current = person;
             people.Reset();
             while (true)
                    {
                 people.MoveNext();
                 person = (Person)people.Current;
                 Console.WriteLine(person.ToString());
             }
         }
         catch (InvalidOperationException)
         {
             people.Reset();
         }
     }

    Результаты работы этого теста показаны на рис. 5.10.

    (рис 5.10) Реализация интерфейса IEnumerator

    Обратите внимание: клиент сумел изменить объект, полученный в результате перечисления, добавив "Джеймса Бонда" в список агентов. На этой оптимистической ноте закончим тему организации перечислимости в классе.

    Задачи

  • Постройте DLL c классом Student, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классом Student, наследующим интерфейс IEnumerable (например, для перечисления оценок студента). Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классом Student, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классами Student и Students. В классе Students задайте несколько итераторов, позволяющих перечислять студентов по разным критериям - фамилиям, рейтингу. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Student и Teacher, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классом Car, наследующим интерфейс IComparable. Постройте Windows- проект для работы с классом Car.
  • Постройте DLL c классом Car, наследующим интерфейс IEnumerable (например, для перечисления характеристик машины). Постройте Windows-проект для работы с классом Car.
  • Постройте DLL c классом Car, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Car.
  • Постройте DLL c классами Car и SaleOfCars. В классе SaleOfCars задайте несколько итераторов, позволяющих перечислять машины по разным критериям - маркам, стоимости. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Car и OwnerOfCar, связанных отношением клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классом Employee, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классом Employee, наследующим интерфейс IEnumerable (например, для перечисления послужного списка). Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классом Employee, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классами Employee и Firm. В классе Firm задайте несколько итераторов, позволяющих перечислять служащих по разным критериям - фамилиям, должности. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Employee и Firm, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классами Врач, Космонавт, Альпинист. Постройте классы-потомки Врач_Космонавт, Врач_Альпинист, наследующие свойства родительских классов. Используйте механизм интерфейсов. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Учитель, Шофер, Пилот. Постройте классы-потомки Инструктор_Водитель, Инструктор_Летчиков, наследующие свойства родительских классов. Используйте механизм интерфейсов. Постройте=Windows проект для работы с классами.
  • Страницы:

    Проект к данной лекции Вы можете скачать здесь.

    Интерфейсы

    Слово " интерфейс " - многозначное, и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс - это частный случай класса. Интерфейс представляет собой полностью абстрактный класс, все методы которого абстрактны. От абстрактного класса интерфейс отличается некоторыми деталями в синтаксисе и поведении. Синтаксическое отличие состоит в том, что методы интерфейса объявляются без указания модификатора доступа. Отличие в поведении заключается в более жестких требованиях к потомкам. Класс, наследующий интерфейс, обязан полностью реализовать все методы интерфейса. В этом отличие от класса, наследующего абстрактный класс, где потомок может реализовать лишь некоторые методы родительского абстрактного класса, оставаясь абстрактным классом. Но, конечно, не ради этих отличий были введены интерфейсы в язык C#. У них своя важная роль.

    Введение в язык частных случаев усложняет его и свидетельствует о некоторых изъянах, для преодоления которых и вводятся частные случаи. Например, введение структур в язык C# позволило определять классы как развернутые типы. Конечно, проще было бы ввести в объявление класса соответствующий модификатор, позволяющий любой класс объявлять развернутым. Но этого сделано не было.

    Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования сталкивается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей - один полноценный класс, а остальные в виде интерфейсов, - в этом и состоит основное назначение интерфейсов.

    Отметим еще одно важное назначение интерфейсов,отличающее их от абстрактных классов. Абстрактный класс представляет собой начальный этап проектирования класса, который в будущем получит конкретную реализацию. Интерфейсы задают дополнительные свойства класса. Один и тот же интерфейс позволяет описывать свойства, которыми могут обладать разные классы. Они похожи на сказочных фей, каждая из которых наделяет принцессу при ее рождении тем или иным свойством. В библиотеке FCL имеется большое число интерфейсов, наследуя которые, классы получают полезные дополнительные свойства. С некоторыми из них мы познакомимся в этой лекции.

    Две стратегии реализации интерфейса

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

    interface IStrings
     {
       /// <summary>
       /// Преобразование 
       /// </summary>
       /// <returns>результат преобразования</returns>
       string Convert();
       
       /// <summary>
       /// Шифрование 
       /// </summary>
       /// <param name="code">код шифра</param>
       /// <returns>результат шифрования</returns>
       string Cipher(string[] code);
     }

    У этого интерфейса два метода, которые и должны будут реализовать все классы, наследники интерфейса. Как следует из спецификации, заданной в тегах summary, метод Convert должен, следуя алгоритму, выбранному потомком, преобразовать объект, возвращая строку, а метод Chiper, возвращающий строку, рассматривается как шифрование, в алгоритме которого используется массив строк code, переданный методу.

    Реализация методов интерфейса как общедоступных методов

    Класс, наследующий интерфейс и реализующий его методы, может объявить соответствующие методы класса открытыми. Заметьте, у методов интерфейса не заданы модификаторы доступа. Построим пример класса, наследующего интерфейс IStrings:

    /// <summary>
        /// Наследует интерфейс IStrings,
        /// реализуя его методы как общедоступные (public)
        /// </summary>
        class SimpleText : IStrings
        {
       //поля класса
       string text;
       static string[] codeTable =
       {
         "абвгдеёжзийклмнопрстуфхцчшщыьъэюя ,.!?;:",
         "ъьыщшчцхфуэюя ,.!?;:тсрпонмлкйабвгдеёжзи"
       };
       //Конструкторы
       public SimpleText()
       {
           text = "Простой текст!";
       }
       public SimpleText(string txt)
       {
           text = txt;
       }
       public string Text
       {
           get { return text; }
       }
    }

    Построенный класс является обычным классом, содержащим текстовое поле text, конструкторы, метод - свойство, обеспечивающее доступ к закрытому полю. Но поскольку класс объявил себя наследником интерфейса IStrings, он обязан реализовать методы интерфейса, предложив некоторую их реализацию. Вот пример подобной реализации:

    //Реализация интерфейсов
       /// <summary>
       /// Удаление пробелов в поле text, 
       /// преобразование к нижнему регистру 
       /// </summary>
       /// <returns>преобразованная строка</returns>
       public string Convert()
       {         
          string res = "";
           foreach (char sym in text)       
          if (sym != ' ') res += sym.ToString();
           res = res.ToLower();
           return res;
       }
       /// <summary>
       /// шифрование поля text
       /// с использованием таблицы кодировки символов
       /// </summary>
       /// <param name="code">таблица кодировки</param>
       /// <returns>зашифрованный текст</returns>
       public string Cipher(string[] code)
       {
           string res = "";
           foreach (char sym in text)
           {
          int k = code[0].IndexOf(sym);
          if (k >= 0) res += code[1][k];
          else res += sym.ToString();
           }
           return res;
       }

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

    /// <summary>
       /// Проверка поля text,является ли он палиндромом
       ///  после преобразования Convert
       /// </summary>
       /// <returns> true, если палиндром</returns>
       public bool IsPalindrom()
       {
           string txt = Convert();
           for(int i=0, j = txt.Length-1; i<j; i++, j--)
          if(txt[i] != txt[j]) return false;
           return true;
       }
       /// <summary>
       /// Шифрование, заданное собственной таблицей кодировки 
       /// </summary>
       /// <returns>шифрованный текст</returns>
       public string Coding()
       {
           return Cipher(codeTable);
       }

    Рассмотрим класс Testing, методы которого позволят выполнить тестирование объектов создаваемых нами классов:

    /// <summary>
        /// тестирующий класс
        /// </summary>
        class Testing
        {
       //поля класса        
       const string PAL = 
           "А роза упала на лапу Азора";
       static string[] CODE =
       {
           "абвгдежзиклмнопрстуфхцчшыъьэюя",   
           "abvgdejziklmnoprstyfhc461w'qux"
       };
       /// <summary>
       /// Тестирование класса SimpleText
       /// </summary>
       public void TestText()
       {
           Console.WriteLine("Работа с объектом класса SimpleText! ");
           SimpleText simpleText = new SimpleText(PAL);
           Console.WriteLine("Исходный текст : " + PAL); 
           string text;
           text = simpleText.Convert();
           Console.WriteLine("Преобразованный текст : " + text); 
           if(simpleText.IsPalindrom())
          Console.WriteLine("Это палиндром!");
           text = simpleText.Coding();
           Console.WriteLine("Шифрованный текст : " + text);       
    
           Console.WriteLine("Работа с объектом интерфейса IStrings! ");
           IStrings istrings;
           text = "Это простой текст!";
           Console.WriteLine("Исходный текст : " + text);
           simpleText = new SimpleText(text);
           istrings = (IStrings)simpleText;       
           text = istrings.Convert();
           Console.WriteLine("Преобразованный текст : " + text);
           text = istrings.Cipher(CODE);
           Console.WriteLine("Шифрованный текст : " + text);       
       }
        }

    Заметьте: в классе Testing объявлен как объект класса SimpleText, так и объект istrings интерфейса IStrings. В методе TestText объект simpleText создается обычным способом при вызове конструктора класса. Затем этот объект вызывает как открытый метод Convert, наследованный от интерфейса, так и открытые методы класса IsPalindrom, Coding, использующие методы интерфейса. Все это совершено понятно и ожидаемо.

    Более интересен тот факт, что можно работать с реализованными в классе методами интерфейса через объект istrings. Создать объект интерфейса путем вызова конструктора невозможно, но по отношению к классу SimpleText это объект родительского класса, и потому объекту интерфейса можно присвоить объект класса после соответствующего приведения типа. Такому объекту доступны методы, заданные интерфейсом, и только они. Результаты работы теста показаны на рис. 5.1.

    (рис 5.1) Реализация методов интерфейса как открытых методов класса

    Реализация методов интерфейса как закрытых методов

    Другая стратегия реализации состоит в том, чтобы все или некоторые методы интерфейса сделать закрытыми. Для реализации этой стратегии класс, наследующий интерфейс, объявляет методы без модификатора доступа, что по умолчанию соответствует модификатору private, и уточняет имя метода именем интерфейса. Давайте создадим класс с именем PrivateInterfaces, являющийся аналогом только что рассмотренного класса SimpleText. В этом классе методы наследуемого интерфейса будут реализованы как закрытые для клиентов методы. Полного описания этого класса приводить не буду, ограничусь лишь некоторыми фрагментами. Вот как выглядят заголовки методов, реализующих методы интерфейса:

    //Реализация интерфейсов как закрытых методов   
       string IStrings.Convert()
       string IStrings.Cipher(string[] code)

    Модификатор доступа стал по умолчанию private, а имя метода уточнено именем интерфейса. Интересно то, как теперь эти методы вызываются в методах класса. Их нельзя, как ранее, вызвать по имени или даже по полному имени. Нужно явно задать цель вызова. В качестве цели следует взять текущий объект this и привести его к типу интерфейса, так что получается вот такая сложная конструкция целевого объекта:

    ((IStrings)this)

    Соответствующие строки кода в методах класса IsPalindrom и Coding выглядят теперь так:

    string txt = ((IStrings)this).Convert();
    return ((IStrings)this).Cipher(codeTable);

    Класс PrivateInterface реализовал методы интерфейса IStrings, но сделал их закрытыми и недоступными для клиентов и наследников класса. Как же им получить доступ к закрытым методам? Есть два способа решения этой проблемы.

  • Обертывание. В классе, реализующем интерфейс, создается открытый метод, являющийся оберткой закрытого метода.
  • Кастинг. У клиента создается объект интерфейсного класса IStrings, полученный приведением (кастингом) объекта класса PrivateInterface. Этому объекту доступны закрытые методы интерфейса.
  • Обертывание - это широко распространенный прием в ООП. Под обертыванием понимают создание открытых методов класса, содержащих вызовы старых или недоступных клиентам методов. Цели обертывания могут быть различными. Например, имеется библиотека процедур, написанная на языке С или другом не объектном языке программирования. Эта библиотека весьма полезна, но недоступна в существующем виде. Использование обертывания позволяет обернуть ее в одежды класса, а каждую процедуру библиотеки превратить в метод класса. Иногда обертывание старого метода делается с целью изменения его интерфейса, сделав его более современным.

    Фактически обертывание уже применялось при создании методов IsPalindrom и Coding. Давайте выполним обертку закрытых методов интерфейса, полностью сохранив их интерфейс, но изменив имена:

    //Обертка методов интерфейса с переименованием
       public string ClassConvert()
       {
           return ((IStrings)this).Convert();
       }
       public string ClassCipher(string[] code)
       {
           return ((IStrings)this).Cipher(code);
       }

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

    У клиентов класса есть возможность добраться до закрытых методов интерфейса, даже если класс и не создал открытых методов путем обертки. Для этой цели клиент может использовать объявление объектов интерфейса. Создать объект класса интерфейса обычным путем с использованием конструктора и операции new нельзя. Тем не менее можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми.

    Построим в классе Testing тест, аналогичный тесту TestText, для работы с объектом класса PrivateInterface.

    /// <summary>
       /// тестирование класса PrivateInterface
       /// </summary>
       public void TestTextPrivateInterface()
       {
           Console.WriteLine("Работа с объектом privateInterface! ");
           PrivateInterface privateInterface = new PrivateInterface(PAL);
           Console.WriteLine("Исходный текст : " + PAL);
           string text;
           text = privateInterface.ClassConvert();
           Console.WriteLine("Преобразованный текст : " + text);
           if (privateInterface.IsPalindrom())
               Console.WriteLine("Это палиндром!");
           text = privateInterface.Coding();
           Console.WriteLine("Шифрованный текст : " + text);
    
           Console.WriteLine("Работа с объектом интерфейса IStrings! ");
           IStrings istrings;
           text = "Это простой текст!";
           Console.WriteLine("Исходный текст : " + text);
           privateInterface = new PrivateInterface(text);
           istrings = (IStrings)privateInterface;
           text = istrings.Convert();
           Console.WriteLine("Преобразованный текст : " + text);
           text = istrings.Cipher(CODE);
           Console.WriteLine("Шифрованный текст : " + text);
       }

    В этом тесте объект класса privateInterface вызывает метод интерфейса под новым именем ClassConvert, а интерфейсный объект istrings вызывает методы интерфейса, несмотря на то, что они закрыты, под собственными именами. Результаты работы теста показаны на. рис. 5.2.

    (рис 5.2) Реализация методов интерфейса как закрытых методов класса

    Множественное наследование и его проблемы

    Множественным наследованием называется ситуация, когда класс объявляет N классов ( N > 1 ) своими непосредственными родителями. В языке C# есть ограничения на множественное наследование. Ситуация здесь такая. Во-первых, у каждого класса родителем, хотя не всегда непосредственным, является класс object. Во-вторых, каждый класс может явно объявить один класс в качестве непосредственного родителя, а также объявить непосредственными родителями один или несколько интерфейсов. Таким образом, в C# допускается множественное наследование интерфейсов и одиночное (не считая наследования от класса object ) наследование классов.

    Во многом ограничение множественного наследования классов связано с тем, что оно создает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы - коллизию имен и наследование от общего предка.

    Коллизия имен

    Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Если сигнатуры разные, то это не приводит к конфликтам. Класс реализует методы обоих интерфейсов, и у него просто появляются перегруженные методы.

    Но что следует делать классу наследнику в тех случаях, когда сигнатуры методов совпадают? Возможны две стратегии - склеивание методов и переименование.

    Стратегия склеивания применяется тогда, когда класс - наследник интерфейсов - полагает, что методы разных интерфейсов, имеющие одинаковое имя и сигнатуру, задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную ( public ) реализацию, соответствующую методам всех интерфейсов c единой сигнатурой.

    Стратегия переименования исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей - они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы уже знаем, как производить переименование метода интерфейса в самом классе наследника. Для этого достаточно реализовать в классе методы разных интерфейсов как закрытые, а затем открыть их в классе с переименованием.

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

    Наследование от общего предка

    Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B, а те, в свой черед, являются наследниками класса P, то класс наследует свойства и методы своего предка P дважды: один раз получая их от класса A, другой - от B. Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации. Ситуация дублирующего наследования показана на рис. 5.3.

    (рис 5.3) Дублирующее наследование

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

    Склеивание и переименование

    Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса - наследника этих интерфейсов, применяющего разные стратегии для конфликтующих методов. У нас уже определен интерфейс IStrings. Предположим, что существует интерфейс ITransform, подобный Istrings:

    interface ITransform
      {
       /// <summary>
       /// Преобразование 
       /// </summary>
       /// <returns>результат преобразования</returns>
       string Convert();
    
       /// <summary>
       /// Шифрование  
       /// </summary>
       /// <param name="code">код </param>
       /// <returns>результат шифрования</returns>
       string Cipher(string[] code);        
      }

    У этих интерфейсов имена и сигнатуры методов совпадают. Вот класс, наследующий оба интерфейса:

    /// <summary>
        /// Наследник двух интерфейсов,
        /// у методов которых Convert и Cipher
        /// сигнатуры совпадают.
        /// Методы Cipher склеиваются, 
        /// Convert - переименовываются
        /// </summary>
        class TwoInterfaces:IStrings,ITransform
        {
            //Опущена часть класса, общая с классом SimpleText
            //Реализация интерфейсов                
            string IStrings.Convert()
            {            
                string res = "";
                foreach (char sym in text)            
                    if (sym != ' ') res += sym.ToString();
                res = res.ToLower();
                return res;
            }
            string ITransform.Convert()
            {            
                string res = "";
                for (int i = text.Length - 1; i >= 0; i--)
                    res += text[i];
                return res;
            }
            //Переименование закрытых методов
            public string ConvertOne()
            {
                return ((IStrings)this).Convert();
            }
            public string ConvertTwo()
            {
                return ((ITransform)this).Convert();
            }
            //Склеивание метода Cipher двух интерфейсов
            public string Cipher(string[] code)
            {
                string s = text;
                string res = "";
                foreach (char sym in s)
                {
                    int k = code[0].IndexOf(sym);
                    if (k >= 0) res += code[1][k];
                    else res += sym.ToString();
                }
                return res;
            }
        }

    Для методов Cipher двух интерфейсов выбрана стратегия склеивания. Для методов Convert выбрана стратегия переименования. Методы интерфейсов реализованы как закрытые методы, а затем в классе объявлены два новых метода с разными именами, являющиеся обертками закрытых методов класса.

    Приведу пример работы с объектами класса и интерфейсными объектами:

    public void TestTextTwoInterfaces()
      {
          Console.WriteLine("Работа с объектом класса TwoInterfaces! ");
          TwoInterfaces twoInterfaces = new TwoInterfaces(PAL);
          Console.WriteLine("Исходный текст : " + PAL);
          string text;
          text = twoInterfaces.ConvertOne();
          Console.WriteLine("Первое преобразование  : " + text);
          if (twoInterfaces.IsPalindrom())
              Console.WriteLine("Это палиндром!");
          text = twoInterfaces.ConvertTwo();
          Console.WriteLine("Второе преобразование  : " + text);
          text = twoInterfaces.Coding();
          Console.WriteLine("Шифрованный текст : " + text);
          
          text = "Это простой текст!";
          Console.WriteLine("Исходный текст : " + text);
          twoInterfaces = new TwoInterfaces(text);
          IStrings istrings;
          ITransform itransform;
          istrings = (IStrings)twoInterfaces;
          itransform = (ITransform)twoInterfaces;
    
          Console.WriteLine("Работа с объектом интерфейса IStrings!");
          text = istrings.Convert();
          Console.WriteLine("Преобразованный текст : " + text);
          text = istrings.Cipher(CODE);
          Console.WriteLine("Шифрованный текст : " + text);
    
          Console.WriteLine("Работа с объектом интерфейса ITransform!");      
          text = itransform.Convert();
          Console.WriteLine("Преобразованный текст : " + text);
          text = itransform.Cipher(CODE);
          Console.WriteLine("Шифрованный текст : " + text);
      }

    Результаты работы показаны на рис. 5.4.

    (рис 5.4) Решение проблемы коллизии имен

    Интерфейсы и поля

    Можно ли в интерфейсе объявлять не только методы, но и поля, обязательные для реализации потомками интерфейса? Ответ не однозначный: и да, и нет. Нет - потому что в явном виде в интерфейсе нельзя объявить поле. Да - потому что в интерфейсе можно объявить метод-свойство с процедурами get и set, обеспечивающими доступ к полю.

    Вот пример такого интерфейса:

    /// <summary>
        /// Доступ к полям Name и Age 
        /// </summary>
        interface IFields
        {
            string Name { get; set; }
            int Age {get;}
        }

    Создадим класс, наследующий этот интерфейс:

    /// <summary>
        /// Класс, наследующий интерфейс IFields
        /// Имеет поля Name и Age,
        /// доступ к полю Name открыт клиентам класса,
        /// доступ к полю Age закрыт и открыт с переименованием!  
        /// </summary>
        class TwoFields:IFields
        {
            string name;
            int age;
            public TwoFields()
            {
                name = "Nemo"; age = 37;
            }
            public TwoFields(string name, int age)
            {
                this.name = name; this.age = age;
            }
            public string Name
            {
                get { return name; }
                set { name = value; }
            }
            int IFields.Age
            {
                get { return age; }
            }
            public int WhatAge()
            {
                return age;
            }
        }

    Обратите внимание: методы доступа к полям, наследуемые от интерфейса, можно реализовать как открытые и как закрытые, открывая их затем под другим именем. У класса всегда должна быть возможность переименовывать имена методов и полей, наследуемых от интерфейса.

    Добавим в класс Testing новый метод, позволяющий протестировать работу с наследуемыми полями.

    public void TestFields()
      {
          Console.WriteLine("Работа с объектом класса TwoFields!");
          TwoFields twofields = new TwoFields();
          Console.WriteLine("Имя: {0}, Возраст: {1}",
              twofields.Name, twofields.WhatAge());
          twofields.Name = "Captain Nemo";
          Console.WriteLine("Работа с интерфейсным объектом  IFields!");
          IFields ifields = (IFields)twofields;
          Console.WriteLine("Имя: {0}, Возраст: {1}",
              ifields.Name,ifields.Age);
      }

    Результаты работы теста показаны на рис. 5.5.

    (рис 5.5) Поля, наследуемые от интерфейса IFields

    Встроенные интерфейсы

    Рассмотрим несколько встроенных интерфейсов, являющихся частью библиотеки FCL. Они используются многими классами библиотеками, так же, как и классами, создаваемыми пользователем.

    Упорядоченность объектов и интерфейс IComparable

    Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса IComparable. Этот интерфейс имеет всего один метод CompareTo (object obj), возвращающий целочисленное значение, положительное, отрицательное или равное нулю, в зависимости от выполнения отношения "больше", "меньше" или "равно".

    Как правило, в классе вначале определяют метод CompareTo, а после этого вводят перегруженные операции, чтобы выполнять сравнение объектов привычным образом с использованием знаков операций отношения.

    Давайте введем отношение порядка на классе Person, рассмотренном в лекции 1, сделав этот класс наследником интерфейса IComparable. Реализуем в этом классе метод интерфейса CompareTo:

    public class Person:IComparable
       {
          public int CompareTo( object pers)
          {
             const string s = "Сравниваемый объект не принадлежит классу Person";
             Person p = pers as Person;
             if (!p.Equals(null))
                return (fam.CompareTo(p.fam));
             throw new ArgumentException (s);
          }
          // другие компоненты класса
       }

    Поскольку аргумент в методе интерфейса принадлежит типу object, перед выполнением сравнения его нужно привести к типу Person. Для приведения используется операция as, позволяющая проверить корректность выполнения приведения. Если приведение невозможно, то невозможно выполнить и сравнение объектов. В этом случае выбрасывается исключение, которое может обработать разумным образом только клиент класса, пытавшийся выполнить сравнение. В самом классе Person можно только пояснить ситуацию, передав информацию объекту исключения.

    Заметьте также, что при проверке на значение null используется отношение Equals, а не обычное равенство, которое будет переопределено.

    Приведение типов и операции as и is

    Давно пора привести отложенное пояснение операций is и as, полезных при работе с объектами, тип которых может быть не определен. Операция is используется в логических выражениях. Логическое выражение

    obj is T

    истинно, если объект obj принадлежит типу T, и ложно в противном случае.

    Оператор присваивания

    obj = P as T;

    присваивает объекту obj объект P, приведенный к типу T, если такое приведение возможно, иначе объекту присваивается значение null. Семантику as можно выразить следующим условным выражением:

    (P is T) ? (T)P : (T)null

    Порядок на классе Person

    Определив метод CompareTo для класса Person, мы тем самым ввели отношение порядка для объектов этого класса. Конечно, сравнение персон может выполняться по разным критериям: возрасту, росту, зарплате. В данном случае отношение порядка на объектах класса Person задается как отношение порядка на фамилиях персон. Так как строки наследуют интерфейс IComparable, для фамилий персон вызывается метод CompareTo, его результат и возвращается в качестве результата метода CompareTo для персон.

    Введем теперь в нашем классе Person перегрузку операций отношения:

    public static bool operator <(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) < 0);
     }
     public static bool operator >(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) > 0);
     }
     public static bool operator <=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) <= 0);
     }
     public static bool operator >=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) >=0);
     }
     public static bool operator ==(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) == 0);
     }
     public static bool operator !=(Person p1, Person p2)
     {
        return (p1.CompareTo(p2) != 0);
     }

    Как обычно, приведу тестовый пример, проверяющий работу с введенными методами:

    public void TestCompare()
     {
        Person poet1 = new Person("Пушкин");
        Person poet2 = new Person("Лермонтов");
        Person poet3 = new Person("Пастернак");
        Person poet4 = new Person("Мандельштам");
        Person poet5 = new Person("Ахматова");
        Person poet6 = new Person("Цветаева");
        Console.WriteLine("{0} > {1} = {2}", poet1.Fam,
           poet2.Fam, (poet1 > poet2));
        Console.WriteLine("{0} >= {1} = {2}", poet3.Fam,
           poet4.Fam, (poet3 >= poet4));
        Console.WriteLine("{0} != {1} = {2}", poet5.Fam,
           poet6.Fam, (poet5 != poet6));
      }

    Вот результаты работы этого теста.

    (рис 5.6) Сравнение персон

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

    Клонирование и интерфейс ICloneable

    При ссылочном присваивании x = y, где x и y - объекты класса T, как уже много раз говорилось, происходит присваивание ссылок. Если до присваивания ссылка y была связана с объектом в динамической памяти, то после присваивания x будет ссылаться на этот же объект. С самим объектом ничего не происходит, никакая копия этого объекта не создается. Иногда требуется создать копию объекта, так, чтобы x и y ссылались на разные объекты.

    Клонированием называется процесс создания копии объекта, а копия объекта называется клоном. Различают два типа клонирования: поверхностное ( shallow ) и глубокое ( deep ). При поверхностном клонировании копируется только один объект, копию которого необходимо создать. Все значимые поля клона получают значения, совпадающие со значениями полей объекта; все ссылочные поля клона являются ссылками на те же объекты, на которые ссылается и сам объект.

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

    Глубокое клонирование требует рекурсивной процедуры обхода существующей структуры объектов, тщательно отработанной во избежание проблемы зацикливания. В общем случае, когда есть несколько классов, являющихся взаимными клиентами, глубокое клонирование требует наличия в каждом классе рекурсивной процедуры. Эти процедуры взаимно вызывают друг друга. Хорошим упражнением является создание проекта, реализующего глубокое клонирование для ситуации, когда объекты разных классов связаны взаимными ссылками.

    Поверхностное клонирование можно выполнить достаточно просто, поскольку оно автоматически поддерживается для всех классов, в том числе, создаваемых программистом. Для создания поверхностного клона достаточно вызвать метод MemberwiseClone, наследуемый от прародителя object. Единственное, что нужно помнить: этот метод защищен, он не может быть вызван непосредственно клиентом класса. Если класс хочет обеспечить возможность создания клона клиентами класса, ему необходимо в классе создать метод, представляющий собой обертку метода MemberwiseClone.

    Давайте обеспечим эту возможность для класса Person, создав в нем соответствующий метод:

    public Person StandartClone()
     {
        Person p = (Person)this.MemberwiseClone();
         return(p);
      }

    Теперь клиенты класса могут легко создавать поверхностные клоны. Вот пример:

    public void TestStandartClone()
      {
         Person mother = new Person("Петрова Анна");
         Person daughter = new Person("Петрова Ольга");
         Person son = new Person("Петров Игорь");
         mother[0] = daughter;
         mother[1] = son;
         Person mother_clone = mother.StandartClone();
         Console.WriteLine("Дети матери: {0}",mother.Fam);
         Console.WriteLine (mother[0].Fam);
         Console.WriteLine (mother[1].Fam);
         Console.WriteLine("Дети клона: {0}",mother_clone.Fam);
         Console.WriteLine (mother_clone[0].Fam);
         Console.WriteLine (mother_clone[1].Fam);   
       }

    При создании клона будет создана копия только одного объекта mother. Обратите внимание: при работе с полем children, задающим детей, используется индексатор класса Person, выполняющий индексацию по этому полю. Вот как выглядят результаты работы теста.

    (рис 5.7) Поверхностное клонирование

    Если стандартное поверхностное клонирование нас не устраивает, то класс можно объявить наследником интерфейса ICloneable и реализовать метод Clone - единственный метод этого интерфейса. В нем можно реализовать полное глубокое клонирование или подходящую для данного случая модификацию.

    Давайте расширим класс Person, сделав его наследником интерфейса ICloneable. Реализация метода Clone будет отличаться от стандартной реализации тем, что к имени объекта - полю Fam - будет приписываться слово " clone ". Вот как выглядит этот метод:

    public object Clone()
        {
        Person clone = (Person)this.MemberwiseClone();
        clone.fam = "clone_" + fam;
        return clone;
        }

    Эта реализация является слегка модифицированной версией стандартного поверхностного клонирования. Я добавил несколько строчек в тестирующую процедуру для проверки работы этой версии клона:

    Person mother_clone2 = (Person)mother.Clone();
      Console.WriteLine("Дети клона_2: {0}",mother_clone2.Fam);
      Console.WriteLine (mother_clone2[0].Fam);
      Console.WriteLine (mother_clone2[1].Fam);

    Все работает должным образом.

    Перечислимость объектов и интерфейсы

    Если класс можно рассматривать как некоторый контейнер перечислимых объектов, то для того, чтобы класс мог возвращать эти объекты в цикле for each, класс должен быть наследником интерфейса IEnumerable или иметь в своем составе итераторы - методы, возвращающие результат типа IEnumerable.

    Интерфейс перечислимости IEnumerable

    У этого интерфейса всего один метод - GetEnumerator(). У метода нет никаких входных аргументов, так что, кажется, организовать перечислимость объектов класса достаточно просто - написать реализацию одного метода. Это действительно просто, но требует понимания процесса перечислимости. Первая сложность состоит в том, что метод GetEnumerator синтаксически определен следующим образом:

    IEnumerator GetEnumerator()

    Это означает, что в результате вызова метода должен возвращаться интерфейсный объект, принадлежащий интерфейсу IEnumerator - еще одному интерфейсу, связанному с перечислимостью. Метод GetEnumerator требует создания объекта перечислителя - объекта, реализующего методы интерфейса IEnumerator. У интерфейса IEnumerator несколько методов, и в совокупности именно они и позволяют организовать процесс перечисления.

    Как же получить интерфейсный объект типа IEnumerator? В принципе эту задачу мы уже умеем решать, что неоднократно демонстрировалось в примерах этой лекции. Нужно объявить интерфейсный объект, затем создать объект класса, являющегося наследником интерфейса IEnumerator, и далее интерфейсный объект связать с объектом класса, используя приведение типа. Но такой подход все равно требует создания объекта, реализующего методы интерфейса IEnumerator, что не так просто.

    К счастью, во многих ситуациях есть более простой путь. Пусть нам нужно организовать перечислимость объектов некоторого класса. У этого класса есть поля, являющиеся объектами, для которых перечислимость уже обеспечена, например, одним из полей класса является массив. Все классы массивы независимо от типа элементов реализуют перечислимость элементов массива, и для них определен метод GetEnumerator. Тогда метод GetEnumerator, который необходимо создать для класса, будет сводиться к вызову метода GetEnumerator для поля класса, представляющего собой перечислимый контейнер элементов. В этом случае все будет совсем просто.

    Давайте построим пример, моделирующий такой подход. Добавим в наш проект новый класс, одним из полей которого будет массив объектов Person. Организуем перечислимость в этом классе, сводящуюся к перечислимости персон массива.

    /// <summary>
        /// Класс - наследник интерфейса IEnumerable,
        /// допускающий перечислимость объектов.
        /// Перечислимость сводится к перечислимости персон,
        /// заданных полем container
        /// </summary>
        class Persons:IEnumerable
        {
        protected int size;
        protected Person[] container;
        Random rnd = new Random(); 
        /// <summary>
        /// Конструктор по умолчанию.
        /// Создает массив из 10 персон
        /// </summary>
        public Persons()
        {
        size = 10;
        container = new Person[size];
        FillContainer(); 
       }
        /// <summary>
        /// Конструктор. Создает массив заданной размерности
        /// Создает его элементы, используя рандомизацию.
        /// </summary>
        /// <param name="size">размерность массива</param>
        public Persons(int size)
        {
        this.size = size;
        container = new Person[size];
        FillContainer();
        }
        /// <summary>
        /// Конструктор, которому передается массив персон
        /// </summary>
        /// <param name="container"> массив Person</param>
        public Persons(Person[] container)
        {
        this.container = container;
        size = container.Length;
        }
        /// <summary>
        /// Заполнение массива person 
        /// </summary>
        void FillContainer()
        {
        for(int i = 0; i <size; i++)
        {
            int num = rnd.Next(3*size);
            int age = rnd.Next(27, 46);
            container[i] = new Person("агент_" + num, age);
        }
        }    
        }

    Созданный класс Persons устроен просто. У него есть поле, названное container и представляющее собой массив с элементами класса Person. Набор конструкторов класса позволяет передать классу массив персон либо поручить самому классу его формирование, используя метод FillContainer. Поскольку класс объявлен наследником интерфейса IEnumerable, необходимо реализовать метод GetEnumerator, что обеспечит перечислимость объектов класса и даст возможность использовать цикл for each при работе с объектами класса. В данном случае реализовать метод интерфейса несложно, и вот как выглядит его реализация:

    /// <summary>
        /// Реализация метода интерфейса IEnumerable
        /// Сводится к вызову соответствующего метода 
        /// для поля container - массива,
        /// для которого этот метод реализован в библиотеке FCL 
        /// </summary>
        /// <returns>перечислитель - интерфейсный объект</returns>
        public IEnumerator GetEnumerator()
        {
        return container.GetEnumerator();
        }

    На описание метода в теге summary потребовалось больше времени и строчек текста, чем на тело метода, состоящее из одной строчки. Но на описание не стоит жалеть усилий! Задача решена, и можно попробовать протестировать работу с объектами класса. Добавим в класс Testing соответствующий метод:

    public void TestEnumeration()
      { 
       int size = 10;    
       Persons agents = new Persons(size);
       foreach (Person agent in agents)
           Console.WriteLine(agent.ToString());
      }

    В тесте создается объект класса Persons, и в цикле foreach объекты перебираются, возвращая каждый раз очередной объект класса Person. Метод ToString, определенный в классе Person, позволяет выводить информацию об объектах. Результаты работы теста показаны на рис. 5.8.

    (рис 5.8) Перечислимость и метод GetEnumerator

    Итераторы

    Нам удалось достаточно просто организовать перечислимость в классе за счет того, что в классе был определен контейнер со встроенным методом GetEnumerator. А что, если такого контейнера нет, или таких контейнеров несколько и хотелось бы организовать перечисление по каждому из контейнеров? Для решения подобных задач в язык C#, начиная с версии 2.0, встроен мощный механизм итераторов, существенно облегчающих задачу перечислимости объектов класса и открывающих новые возможности, которые нельзя реализовать только за счет наследования интерфейса IEnumerable.

    Итератором называется метод класса, возвращающий в качестве результата интерфейсный объект типа IEnumerable. В теле итератора должен присутствовать оператор языка yield, имеющий следующий синтаксис:

    yield return <выражение> ;

    При выполнении итератора автоматически создается контейнер, в который добавляется объект при каждом выполнении оператора yield. Добавляемый в контейнер элемент определяется выражением оператора yield. Порядок выполнения операторов yield определяет порядок перечислимости элементов контейнера. Оператор yield может быть задан в форме

    yield break;

    В такой форме он сигнализирует об окончании заполнения контейнера элементами.

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

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

    /// <summary>
     /// Перечисление элементов массива container
     /// в обратном порядке
     /// </summary>
     /// <returns>интерфейсный объект</returns>
     public IEnumerable ReverseIterator()
     {
         for (int i = container.Length - 1; i >= 0; i--)
             yield return container[i];
     }

    Оператор yield, работающий в цикле, первым возвратит последний элемент массива, а последним - первый.

    Создадим еще один итератор, никак не связанный с элементами массива поля container. Зададим перечисление, содержащее имена известных языков программирования:

    public enum ProgrammingLanguages
        {
            Fortran, Algol, Cobol, Simula, Pascal,
            Ada, C, CPlusPlus, Eiffel, Java, CSharp
        }

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

    /// <summary>
     /// Итератор. Создает объект перечисления
     /// и строит контейнер, 
     /// помещая в него элементы перечисления
     /// </summary>
     /// <returns>интерфейсный объект</returns>
     public IEnumerable LanguagesIterator()
     {
         ProgrammingLanguages lang;
         int i = 0;
         while (true)
         {
             lang = (ProgrammingLanguages)i;
      yield return lang;
      if (lang.ToString() == "CSharp") yield break;  
      i++;
         }
     }

    В цикле оператор yield возвращает элементы перечисления до тех пор, пока не встретится элемент со значением "CSharp". Давайте теперь протестируем то, что у нас получилось. Добавим несколько строк кода в уже рассмотренный метод TestEnumeration:

    Console.WriteLine("Обратный порядок перечисления");
     foreach(Person agent in agents.ReverseIterator())
         Console.WriteLine(agent.ToString());
     Console.WriteLine("Перечисление языков программирования");
             foreach (ProgrammingLanguages lang in
                               agents.LanguagesIterator())
         Console.WriteLine(lang.ToString());

    Обратите внимание: когда клиент организует цикл foreach, он указывает не только имя объекта класса, но и имя итератора, организующего перечислимость. Результаты работы теста показаны на рис. 5.9.

    (рис 5.9) Перечислимость и итераторы

    Интерфейс IEnumerator

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

    Реализация в классе свойств и методов этого интерфейса позволяет клиентам класса самостоятельно организовать работу с объектами класса как с коллекцией. Обычно это не делается, а используются возможности, предоставляемые циклом for each, который скрывает детали применения свойств и методов интерфейса.

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

    Метод bool MoveNext () передвигает курсор к следующему элементу списка, так что после его первого вызова курсор будет указывать на первый элемент списка. После его вызова, когда курсор указывает на последний элемент списка, текущий элемент снова становится неопределенным, а позиция курсора смещается за последний элемент. Метод MoveNext возвращает значение true, когда курсор указывает на элемент списка, и возвращает false, когда курсор вне списка элементов.

    Метод void Reset() возвращает курсор в начальную позицию перед первым элементом списка.

    Метод-свойство Object Current { get } возвращает текущий элемент списка. Выбрасывается исключение InvalidOperationException,если при вызове свойства Current курсор установлен перед или после списка элементов.

    Приведу теперь пример класса, реализующего интерфейс IEnumerator. Давайте построим класс People, который будет наследником класса Persons и интерфейса IEnumerator. Будучи наследником класса Persons, новый класс будет обладать всеми возможностями родительского класса, в том числе перечислимостью объектов. Но как наследник интерфейса IEnumerator он реализует методы интерфейса и предоставит своим клиентам новые возможности.

    /// <summary>
        /// Наследник класса Persons
        /// Реализует методы интерфейса IEnumerator
        /// Предоставляет клиентам класса возможность
        /// собственной организации перебора объектов
        /// </summary>
        class People:Persons,IEnumerator
        {        
            int position = -1;
            public People() : base() { }
            public People(int size): base(size) { }
            public People(Person[] people) : base(people) { }
    
            /// <summary>
            /// Передвигает курсор к следующему элементу в контейнере
            /// Возвращает true, если курсор установлен на элементе списка,
            /// false - в противном случае
            /// </summary>
            /// <returns></returns>
            public bool MoveNext()
            {
                if (position < size) position++;
                return (position < size);
            }
            /// <summary>
            /// Восстанавливает текущее положение курсора 
            /// в контейнере объектов 
            /// </summary>
            public void Reset()
            {
                position = -1;
            }
            /// <summary>
            /// Возвращает текущий объект, если курсор установлен 
            /// на элементе списка
            /// Выбрасывает исключение InvalidOperationException,
            /// когда курсор вне списка
            /// </summary>        
            public object Current
            {
                get
                {
                    try
                    {
                        return container[position];
                    }
    
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
                set
                {
                    try
                    {
                        container[position] = (Person)value;
                    }
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
            }

    У класса три собственных конструктора, которые сводятся к вызову конструкторов родителя, поле position, необходимое для организации доступа, и три открытых метода, реализующих методы интерфейса IEnumerator. Методы устроены достаточно просто и в особых комментариях не нуждаются. Но стоит обратить внимание на реализацию метода-свойства Current. В самом интерфейсе для этого свойства указан только метод get. В реализации я добавил и реализацию метода set. Это сразу же расширяет возможности клиента. Если при работе с методом foreach объекты доступны только для чтения, то теперь свойство Current позволяет изменять элементы коллекции. Тест, построенный для работы с новым классом People, демонстрирует все эти возможности.

    public void TestPeople()
     {     
         People people = new People();     
         foreach (Person person in people)
             Console.WriteLine(person.ToString());
         
         Console.WriteLine("Работа клиента с коллекцией!");     
         try
         {
             Person person;
             person = new Person("James Bond", 44);
             people.MoveNext();
             people.Current = person;
             people.Reset();
             while (true)
                    {
                 people.MoveNext();
                 person = (Person)people.Current;
                 Console.WriteLine(person.ToString());
             }
         }
         catch (InvalidOperationException)
         {
             people.Reset();
         }
     }

    Результаты работы этого теста показаны на рис. 5.10.

    (рис 5.10) Реализация интерфейса IEnumerator

    Обратите внимание: клиент сумел изменить объект, полученный в результате перечисления, добавив "Джеймса Бонда" в список агентов. На этой оптимистической ноте закончим тему организации перечислимости в классе.

    Задачи

  • Постройте DLL c классом Student, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классом Student, наследующим интерфейс IEnumerable (например, для перечисления оценок студента). Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классом Student, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Student.
  • Постройте DLL c классами Student и Students. В классе Students задайте несколько итераторов, позволяющих перечислять студентов по разным критериям - фамилиям, рейтингу. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Student и Teacher, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классом Car, наследующим интерфейс IComparable. Постройте Windows- проект для работы с классом Car.
  • Постройте DLL c классом Car, наследующим интерфейс IEnumerable (например, для перечисления характеристик машины). Постройте Windows-проект для работы с классом Car.
  • Постройте DLL c классом Car, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Car.
  • Постройте DLL c классами Car и SaleOfCars. В классе SaleOfCars задайте несколько итераторов, позволяющих перечислять машины по разным критериям - маркам, стоимости. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Car и OwnerOfCar, связанных отношением клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классом Employee, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классом Employee, наследующим интерфейс IEnumerable (например, для перечисления послужного списка). Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классом Employee, наследующим интерфейсы IComparable и IEnumerable. Постройте Windows-проект для работы с классом Employee.
  • Постройте DLL c классами Employee и Firm. В классе Firm задайте несколько итераторов, позволяющих перечислять служащих по разным критериям - фамилиям, должности. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Employee и Firm, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.
  • Постройте DLL c классами Врач, Космонавт, Альпинист. Постройте классы-потомки Врач_Космонавт, Врач_Альпинист, наследующие свойства родительских классов. Используйте механизм интерфейсов. Постройте Windows-проект для работы с классами.
  • Постройте DLL c классами Учитель, Шофер, Пилот. Постройте классы-потомки Инструктор_Водитель, Инструктор_Летчиков, наследующие свойства родительских классов. Используйте механизм интерфейсов. Постройте=Windows проект для работы с классами.
  • Вернуться к учебному плану