Опубликован: 02.12.2009 | Уровень: специалист | Доступ: свободно
Лекция 6:

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

< Лекция 5 || Лекция 6: 12345 || Лекция 7 >
Аннотация: Многозначное слово интерфейс в данной лекции задает еще один важный частный случай класса – класс, заданный заголовками методов. Интерфейсы важны не только потому, что позволяют задать определенное поведение у потомков интерфейса, но и справиться с проблемой множественного наследования. Лекция сопровождается задачами.

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

Интерфейсы

Слово " интерфейс " - многозначное, и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом 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. Реализация методов интерфейса как закрытых методов класса
< Лекция 5 || Лекция 6: 12345 || Лекция 7 >
Федор Антонов
Федор Антонов
Оплата и обучение
Илья Ардов
Илья Ардов
Зачисление
Михаил Алексеев
Михаил Алексеев
Россия, Уфа, УГАТУ, 2002
Олег Корсак
Олег Корсак
Латвия, Рига