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

Интерфейсы. Контейнерные классы

Аннотация: Описание и использование интерфейсов. Применение стандартных интерфейсов .NET для сравнения, перебора, сортировки и клонирования объектов. Понятие контейнера (коллекции). Использование стандартных коллекций .NET.

Интерфейсы

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

Синтаксис интерфейса

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

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

Синтаксис интерфейса аналогичен синтаксису класса:

[ атрибуты ] [ спецификаторы ] interface имя_интерфейса [ : предки ]
     тело_интерфейса [ ; ]

Для интерфейса могут быть указаны спецификаторы new, public, protected, internal и private. Спецификатор new применяется для вложенных интерфейсов и имеет такой же смысл, как и соответствующий модификатор метода класса. Остальные спецификаторы управляют видимостью интерфейса. По умолчанию интерфейс доступен только из сборки, в которой он описан ( internal ).

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

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

interface IAction
{
    void Draw();
    int Attack(int a);
    void Die();
    int Power { get; }
}

В интерфейсе IAction заданы заголовки трех методов и шаблон свойства Power, доступного только для чтения. Если бы требовалось обеспечить возможность установки свойства, в шаблоне следовало указать ключевое слово set, например:

int Power { get; set; }

Отличия интерфейса от абстрактного класса:

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

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

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

using System;
namespace ConsoleApplication1
{
    interface IAction
    {
        void Draw();
        int Attack( int a );
        void Die();
        int Power { get; }
    }

    class Monster : IAction
    {
        public void Draw()
        {
            Console.WriteLine( "Здесь был " + name );

        }
        public int Attack( int ammo_ )
        {
            ammo -= ammo_;
            if ( ammo > 0 ) Console.WriteLine( "Ба-бах!" );
            else            ammo = 0;
            return ammo;
        }

        public void Die()
        {
            Console.WriteLine( "Monster " + name + " RIP" );
            health = 0;
        }

        public int Power 
        { 
            get
            {
                return ammo * health; 
            }
        }

        …
}

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

Monster Vasia = new Monster( 50, 50, "Вася" );  // объект класса Monster
Vasia.Draw();                                   // результат: Здесь был Вася
IAction Actor = new Monster( 10, 10, "Маша" );  // объект типа интерфейса
Actor.Draw();                                   // результат: Здесь был Маша

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

class Monster : IAction
    {
        int IAction.Power 
        { 
            get
            {
                return ammo * health; 
            }
        }

        void IAction.Draw()
        {
            Console.WriteLine( "Здесь был " + name );
        }
        ...
    }
...
IAction Actor = new Monster( 10, 10, "Маша" );
Actor.Draw();                      // обращение через объект типа интерфейса

// Monster Vasia = new Monster( 50, 50, "Вася" );    
// Vasia.Draw();

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

Работа с объектами через интерфейсы. Операции is и as

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

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

{
    // выполнить преобразование "объекта" к "типу"
    // выполнить действия с преобразованным объектом
}

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

static void Act( object A )
{
    if ( A is IAction )
    {
        IAction Actor = (IAction) A;
        Actor.Draw();
    }
}

В метод Act можно передавать любые объекты, но на экран будут выведены только те, которые поддерживают интерфейс IAction.

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

static void Act( object A )
{
    IAction Actor = A as IAction;
    if ( Actor != null ) Actor.Draw();
}

Обе рассмотренные операции применяются как к интерфейсам, так и к классам.

Георгий Кузнецов
Георгий Кузнецов
Инкапсуляция
Ольга Притоманова
Ольга Притоманова
не могу повторно пройти тест1 по курсу Программирование на языке выокого уровня C#: после нажатия кнопки "продолжить" нет реакци
Stefan Berzan
Stefan Berzan
Молдова, Кишинев
Дмитрий Казимиров
Дмитрий Казимиров
Россия, Омск