Слайд 1©Павловская Т.А. (СПбГУ ИТМО)
Интерфейсы
Слайд 2©Павловская Т.А. (СПбГУ ИТМО)
Общие сведения об интерфейсе
Интерфейс является «крайним
случаем» абстрактного класса. В нем задается набор абстрактных методов, свойств
и индексаторов, которые должны быть реализованы в производных классах.
Интерфейс определяет поведение, которое поддерживается реализующими этот интерфейс классами.
Основная идея использования интерфейса состоит в том, чтобы к объектам таких классов можно было обращаться одинаковым образом.
Каждый класс может определять элементы интерфейса по-своему. Так достигается полиморфизм: объекты разных классов по-разному реагируют на вызовы одного и того же метода.
Синтаксис аналогичен синтаксису класса:
[ атрибуты ] [ спецификаторы ] interface имя [ : предки ]
тело_интерфейса [ ; ]
Слайд 3©Павловская Т.А. (СПбГУ ИТМО)
Интерфейс может наследовать свойства нескольких интерфейсов, в
этом случае предки перечисляются через запятую.
Тело интерфейса составляют абстрактные
методы, шаблоны свойств и индексаторов, а также события.
Интерфейс не может содержать константы, поля, операции, конструкторы, деструкторы, типы и любые статические элементы.
interface IAction
{
void Draw();
int Attack(int a);
void Die();
int Power { get; }
}
Общие сведения об интерфейсе
Слайд 4©Павловская Т.А. (СПбГУ ИТМО)
Интерфейсы или наследование классов?
Если некий набор действий
имеет смысл только для какой-то конкретной иерархии классов, реализующих эти
действия разными способами, уместнее задать этот набор в виде виртуальных методов абстрактного базового класса иерархии.
То, что работает в пределах иерархии одинаково, предпочтительно полностью определить в базовом классе.
Интерфейсы чаще используются для задания общих свойств классов, относящихся к различным иерархиям.
Слайд 5©Павловская Т.А. (СПбГУ ИТМО)
Отличия интерфейса от абстрактного класса
элементы интерфейса по
умолчанию имеют спецификатор доступа public и не могут иметь спецификаторов,
заданных явным образом;
интерфейс не может содержать полей и обычных методов — все элементы интерфейса должны быть абстрактными;
класс, в списке предков которого задается интерфейс, должен определять все его элементы, в то время как потомок абстрактного класса может не переопределять часть абстрактных методов предка (в этом случае производный класс также будет абстрактным);
класс может иметь в списке предков несколько интерфейсов, при этом он должен определять все их методы.
Слайд 6©Павловская Т.А. (СПбГУ ИТМО)
Реализация интерфейса
В C# поддерживается одиночное наследование
для классов и множественное — для интерфейсов. Это позволяет придать производному
классу свойства нескольких базовых интерфейсов, реализуя их по своему усмотрению.
Сигнатуры методов в интерфейсе и реализации должны полностью совпадать.
Для реализуемых элементов интерфейса в классе следует указывать спецификатор public.
К этим элементам можно обращаться как через объект класса, так и через объект типа соответствующего интерфейса.
Слайд 7©Павловская Т.А. (СПбГУ ИТМО)
Пример
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; }
}
Monster Vasia = new Monster( 50, 50, "Вася" ); // объект класса Monster
Vasia.Draw(); // результат: Здесь был Вася
IAction Actor = new Monster( 10, 10, "Маша" ); // объект типа интерфейса
Actor.Draw(); // результат: Здесь был Маша
Слайд 8©Павловская Т.А. (СПбГУ ИТМО)
Обращение к реализованному методу через объект типа
интерфейса
Удобство этого способа проявляется при присваивании объектам типа IAction ссылок
на объекты различных классов, поддерживающих этот интерфейс.
Например, есть метод с параметром типа интерфейса. На место этого параметра можно передавать любой объект, реализующий интерфейс:
static void Act( IAction A )
{
A.Draw();
}
static void Main()
{
Monster Vasia = new Monster( 50, 50, "Вася" );
Act( Vasia );
...
}
Слайд 9©Павловская Т.А. (СПбГУ ИТМО)
Второй способ реализации интерфейса
Явное указание имени интерфейса
перед реализуемым элементом.
Спецификаторы доступа не указываются. К таким элементам
можно обращаться в программе только через объект типа интерфейса:
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(); ошибка!
При этом соответствующий метод не входит в интерфейс класса. Это позволяет упростить его в том случае, если какие-то элементы интерфейса не требуются конечному пользователю класса.
Кроме того, этот способ позволяет избежать конфликтов при множественном наследовании
Слайд 10©Павловская Т.А. (СПбГУ ИТМО)
Пример
Пусть класс Monster поддерживает два интерфейса: один
для управления объектами, а другой для тестирования:
interface Itest { void
Draw(); }
interface Iaction { void Draw(); int Attack( int a ); … }
class Monster : IAction, Itest {
void ITest.Draw() {
Console.WriteLine( "Testing " + name ); }
void IAction.Draw() {
Console.WriteLine( "Здесь был " + name ); }
... }
Оба интерфейса содержат метод Draw с одной и той же сигнатурой. Различать их помогает явное указание имени интерфейса.
Обращаются к этим методам, используя операцию приведения типа:
Monster Vasia = new Monster( 50, 50, "Вася" );
((ITest)Vasia).Draw(); // результат: Здесь был Вася
((IAction)Vasia).Draw(); // результат: Testing Вася
Слайд 11©Павловская Т.А. (СПбГУ ИТМО)
Операция is
При работе с объектом через
объект типа интерфейса бывает необходимо убедиться, что объект поддерживает данный
интерфейс.
Проверка выполняется с помощью бинарной операции is. Она определяет, совместим ли текущий тип объекта, находящегося слева от ключевого слова is, с типом, заданным справа.
Результат операции равен true, если объект можно преобразовать к заданному типу, и false в противном случае. Операция обычно используется в следующем контексте:
if ( объект is тип )
{
// выполнить преобразование "объекта" к "типу"
// выполнить действия с преобразованным объектом
}
Слайд 12©Павловская Т.А. (СПбГУ ИТМО)
Операция as
Операция as выполняет преобразование к заданному типу,
а если это невозможно, формирует результат null:
static void Act( object
A )
{
IAction Actor = A as IAction;
if ( Actor != null ) Actor.Draw();
}
Обе рассмотренные операции применяются как к интерфейсам, так и к классам.
Слайд 13©Павловская Т.А. (СПбГУ ИТМО)
Интерфейсы и наследование
Интерфейс может не иметь
или иметь сколько угодно интерфейсов-предков, в последнем случае он наследует
все элементы всех своих базовых интерфейсов, начиная с самого верхнего уровня.
Базовые интерфейсы должны быть доступны в не меньшей степени, чем их потомки.
Как и в обычной иерархии классов, базовые интерфейсы определяют общее поведение, а их потомки конкретизируют и дополняют его.
В интерфейсе-потомке можно также указать элементы, переопределяющие унаследованные элементы с такой же сигнатурой. В этом случае перед элементом указывается ключевое слово new, как и в аналогичной ситуации в классах. С помощью этого слова соответствующий элемент базового интерфейса скрывается.
Слайд 14©Павловская Т.А. (СПбГУ ИТМО)
Пример переопределения
interface IBase { void F( int
i ); }
interface Ileft : IBase {
new void F( int i ); /* переопределение метода F */ }
interface Iright : IBase { void G(); }
interface Iderived : ILeft, IRight {}
class A {
void Test( IDerived d ) {
d.F( 1 ); // Вызывается ILeft.F
((IBase)d).F( 1 ); // Вызывается IBase.F
((ILeft)d).F( 1 ); // Вызывается ILeft.F
((IRight)d).F( 1 ); // Вызывается IBase.F
}
}
Метод F из интерфейса IBase скрыт интерфейсом ILeft, несмотря на то, что в цепочке IDerived — IRight — IBase он не переопределялся.
IBase
Ileft
Iright
Iderived
A
Слайд 15©Павловская Т.А. (СПбГУ ИТМО)
Особенности реализации интерфейсов
Класс, реализующий интерфейс, должен определять
все его элементы, в том числе унаследованные. Если при этом
явно указывается имя интерфейса, оно должно ссылаться на тот интерфейс, в котором был описан соответствующий элемент.
Интерфейс, на собственные или унаследованные элементы которого имеется явная ссылка, должен быть указан в списке предков класса.
Класс наследует все методы своего предка, в том числе те, которые реализовывали интерфейсы. Он может переопределить эти методы с помощью спецификатора new, но обращаться к ним можно будет только через объект класса.
Слайд 16©Павловская Т.А. (СПбГУ ИТМО)
Стандартные интерфейсы .NET
В библиотеке классов .NET
определено множество стандартных интерфейсов, задающих желаемое поведение объектов. Например, интерфейс
IComparable задает метод сравнения объектов на «больше-меньше», что позволяет выполнять их сортировку.
Реализация интерфейсов IEnumerable и IEnumerator дает возможность просматривать содержимое объекта с помощью foreach, а реализация интерфейса ICloneable — клонировать объекты.
Стандартные интерфейсы поддерживаются многими стандартными классами библиотеки. Например, работа с массивами с помощью foreach возможна оттого что тип Array реализует интерфейсы IEnumerable и IEnumerator.
Можно создавать и собственные классы, поддерживающие стандартные интерфейсы, что позволит использовать объекты этих классов стандартными способами.
Слайд 17©Павловская Т.А. (СПбГУ ИТМО)
Сравнение объектов
Интерфейс IComparable определен в пространстве
имен System. Он содержит всего один метод CompareTo, возвращающий результат
сравнения двух объектов — текущего и переданного ему в качестве параметра:
interface IComparable
{
int CompareTo( object obj )
}
Метод должен возвращать:
0, если текущий объект и параметр равны;
отрицательное число, если текущий объект меньше параметра;
положительное число, если текущий объект больше параметра.
Слайд 18©Павловская Т.А. (СПбГУ ИТМО)
Пример реализации интерфейса
class Monster : IComparable
{ public
int CompareTo( object obj ) //
реализация интерфейса
{ Monster temp = (Monster) obj;
if ( this.health > temp.health ) return 1;
if ( this.health < temp.health ) return -1;
return 0; }
... }
class Class1
{ static void Main()
{ const int n = 3;
Monster[] stado = new Monster[n];
stado[0] = new Monster( 50, 50, "Вася" );
stado[1] = new Monster( 80, 80, "Петя" );
stado[2] = new Monster( 40, 10, "Маша" );
Array.Sort( stado ); // сортировка стала возможной
}}
Слайд 19©Павловская Т.А. (СПбГУ ИТМО)
Параметризованные интерфейсы
class Program {
class Elem : IComparable
{ string data;
int key;
...
public int CompareTo( Elem obj )
{ return key - obj.key; }
}
static void Main(string[] args)
{
List list = new List();
for ( int i = 0; i < 10; ++i ) list.Add( new Elem() ); ...
list.Sort();
...
}
}
Слайд 20©Павловская Т.А. (СПбГУ ИТМО)
Клонирование объектов
Клонирование — создание копии объекта. Копия
объекта называется клоном.
Слайд 21©Павловская Т.А. (СПбГУ ИТМО)
Виды клонирования
При присваивании одного объекта ссылочного типа
другому копируется ссылка, а не сам объект (рис. а).
Если необходимо
скопировать в другую область памяти поля объекта, можно воспользоваться методом MemberwiseClone, который объект наследует от класса object. При этом объекты, на которые указывают поля объекта, в свою очередь являющиеся ссылками, не копируются (рис. б). Это называется поверхностным клонированием.
Для создания полностью независимых объектов необходимо глубокое клонирование, когда в памяти создается дубликат всего дерева объектов (рис. в).
Алгоритм глубокого клонирования весьма сложен, поскольку требует рекурсивного обхода всех ссылок объекта и отслеживания циклических зависимостей.
Объект, имеющий собственные алгоритмы клонирования, должен объявляться как наследник интерфейса ICloneable и переопределять его единственный метод Clone.