Интерфейсы

В статье Абстрактные классы, методы и свойства был рассмотрен пример наследования от абстрактного класса классов-потомков и использования абстрактных методов, как шаблонов для реализации их в классах-потомках. Было отмечено, что другой вариант реализации — использование интерфейсов.

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

Пример использования интерфейса

В пространство ИнтерфейсФигур добавим новый элемент — интерфейс IFigureInfo. Это можно сделать через Меню Проект/Добавить новый элемент/Интерфейс или Ctrl+Shift+A/Интерфейс (тогда добавится файл Interface1.cs) со следующим содержанием или же вставить это объявление в текст основного кода приложения (см. ниже).

namespace ИнтерфейсФигур
{
   interface IFigureInfo
   {
      double area();
      double perimeter();
   }
}

Это означает, что во всех классах, поддерживающих этот интерфейс, должны быть реализованы эти два метода без параметров, возвращающих тип double.

Создадим три независимых класса: Circle, Square, Triangle. У каждого из этих классов есть свой набор полей и методов, связанных с их особенностями, в том числе — характерный размер — Length. Для круга это, например, его радиус, для квадрата и равностороннего треугольника — длина его стороны. Нам в каждый из классов необходимо гарантированно добавить методы, которые будут находить площадь (area) и периметр (perimeter)  этих фигур. Вот тут то и могут пригодиться интерфейсы. Для этого после имени класса через двоеточие укажем имя интерфейса. Сделаем это и для других классов  Square и Triangle.

class Circle : IFigureInfo
{ ... }

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

Ошибка «ИнтерфейсФигур.Square» не содержит определения для «area» и не был найден метод расширения «area», принимающий тип «ИнтерфейсФигур.Square» в качестве первого аргумента.

В класс Prpogram добавим статический метод InfoFigure для контроля работы методов класса. Тогда код файла Program.cs будет следующим:

using System;
namespace ИнтерфейсФигур
{
   interface IFigureInfo
   {
      double area();
      double perimeter();
  }

  class Circle : IFigureInfo
  {
     double Length; // радиус круга
     public Circle(double len) 
     { Length = len; }
     public double area()
     {
        return Math.PI * Length * Length;
     } 
     public double perimeter()
     {
        return 2 * Math.PI * Length;
     }
 }

 class Square : IFigureInfo 
 {
    double Length; // сторона квадрата
    public Square(double len)
    {
       Length = len; }
       public double area()
       {
          return Length * Length;
       }
       public double perimeter()
       {
          return 4 * Length;
       } 
    }

    class Triangle : IFigureInfo  
    {
       double Length; // сторона треугольника
       public Triangle(double len)
       {  Length = len; }
       public double area()
       {
          return Length * Length * Math.Sqrt(3) / 4;
       }
       public double perimeter()
       {
          return 3 * Length;
       }  
   }

   class Program
   {
      static public void InfoFigure(string fig, double s, double p)
      {
         Console.WriteLine("{0} площадь = {1:##.###}, периметр = {2:##.###}",fig, s, p);
      }
      static void Main(string[] args)
      {
         Console.Write("Характерный размер фигуры = ");
         double len = Convert.ToDouble(Console.ReadLine());

         Circle c = new Circle(len);
         InfoFigure("Круг: ", c.area(), c.perimeter());

         Square q = new Square(len);
         InfoFigure("Квадрат: ", q.area(), q.perimeter());
           
         Triangle t = new Triangle(len);
         InfoFigure("Треугольник: ", t.area(), t.perimeter());

         Console.ReadKey();
      }
   }
}

Результат совпадает с ранее рассмотренным примером(проверьте!).

Теперь немного теории.

Зачем нужны интерфейсы?

В языке С++ есть возможность множественного наследования. Разработчики C# решили отказаться от этого и придумали интерфейсы. Получается, что класс не может быть унаследован от нескольких классов, но при этом он может унаследовать несколько интерфейсов.

Часто бывает необходимо реализовать несколько классов, при этом у них будут одинаковые методы (по названию!), но они по-разному должны быть реализованы.

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

Интерфейс в практическом смысле дает возможность указать из чего именно должен состоять тот или иной объект разрабатываемой модели без описания поведения объекта.

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

Свойства интерфейсов

Интерфейс (interface) представляет собой не более чем просто именованный набор абстрактных членов. Абстрактные методы являются чистым протоколом, поскольку не имеют никакой стандартной реализации. Конкретные члены, определяемые интерфейсом, зависят от того, какое поведение моделируется с его помощью.

Интерфейс выражает поведение, которое данный класс или структура может избрать для поддержки. Более того, каждый класс (или структура) может поддерживать столько интерфейсов, сколько необходимо, и, следовательно, тем самым поддерживать множество поведений.

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

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

Благодаря поддержке интерфейсов в C# может быть в полной мере реализован главный принцип полиморфизма: один интерфейс — множество методов.

Объявление интерфейсов

Интерфейсы объявляются с помощью ключевого слова interface. Ниже приведена упрощенная форма объявления интерфейса:

interface имя{    возвращаемый_тип имя_метода_1 (список_параметров);    возвращаемый_тип имя_метода_2 (список_параметров);    // …    возвращаемый_тип имя_метода_N (список_параметров);}

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

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

Помимо методов, в интерфейсах можно также указывать свойства, индексаторы и события. Интерфейсы не могут содержать член-данные (кроме событий?). В них нельзя также определить конструкторы, деструкторы или операторные методы. Кроме того, ни один из членов интерфейса не может быть объявлен как static.

Реализация интерфейсов

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

class имя_класса : имя_интерфейса{    // тело класса}

где имя_интерфейса — это конкретное имя реализуемого интерфейса. Если уж интерфейс реализуется в классе, то это должно быть сделано полностью. В частности, реализовать интерфейс выборочно и только по частям нельзя.

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

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

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

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

Наследование интерфейсов

Интерфейсы, как и классы, могут наследоваться:

interface IAction
{
   void Move();
}
interface IRunAction : IAction
{
   void Run();
}
class BaseAction : IRunAction
{
   public void Move()
   {
      Console.WriteLine("Move");
   }
   public void Run()
   {
      Console.WriteLine("Run");
   }
}

При применении этого интерфейса класс BaseAction должен будет реализовать как методы и свойства интерфейса IRunAction, так и методы и свойства базового интерфейса IAction.

Однако в отличие от классов мы не можем применять к интерфейсам модификатор sealed (а также к методам интерфейсов), чтобы запретить наследование интерфейсов.

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

Однако методы интерфейсов могут использовать ключевое слово new для сокрытия методов из базового интерфейса:

class RunAction : IRunAction
{
   public void Move()
   {
       Console.WriteLine("I am running");
   }
}
interface IAction
{
   void Move();
}
interface IRunAction : IAction
{
   new void Move();
}

Здесь метод Move из IRunAction скрывает метод Move из базового интерфейса IAction. Большого смысла в этом нет, так как в данном случае нечего скрывать, то тем не менее мы так можем делать. А класс RunAction реализует метод Move сразу для обоих интерфейсов.

Модификаторы доступа интерфейсов

Как и классы, интерфейсы по умолчанию имеют уровень доступа internal, то есть такой интерфейс доступен только в рамках текущего проекта. Но с помощью модификатора public мы можем сделать интерфейс общедоступным:

public interface IAction
{
   void Move();
}

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

public interface IAction
{
   void Move();
}
internal interface IRunAction : IAction
{
   void Run();
}

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

internal interface IAction
{
   void Move();
}
public interface IRunAction : IAction  
           // ошибка IRunAction может быть только internal
{
   void Run();
}

Далее в примерах применение интерфейсов будет рассмотрено более подробно.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Пролистать наверх