Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-7

.pdf
Скачиваний:
11
Добавлен:
05.02.2023
Размер:
4.54 Mб
Скачать

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

public interface ITest2

{

int Count();

}

class MyClass3 : ITest2

{

public int ITest2.Count() { return 3; } // Ошибка int ITest2.Count() { return 3; } // ОК

}

class MyClass4 : ITest2

{

public int Count() { return 4; } // ОК

}

static int Main()

{

MyClass3 c3 = new MyClass3(); MyClass4 c4 = new MyClass4();

Console.WriteLine(c3.Count()); // Ошибка Console.WriteLine(c4.Count()); // 4 return 0;

}

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

class MyClass5 : ITest, ITest2

{

public void F(out string s)

{

s = "MyClass5";

}

int ITest.Count

{

get { return 5; }

}

public string this[int i]

{

get { return "MyClass5.ITest." + i; } set { }

331

}

int ITest2.Count()

{

return 5;

}

public event TestD TestEvent;

}

В данном примере без явной квалификации наблюдался бы конфликт имен для членов Count.

Доступ к явной реализации члена интерфейса можно получить, используя преобразование ссылки на экземпляр класса к типу интерфейса:

static int Main()

{

MyClass3 c3 = new MyClass3();

Console.WriteLine(((ITest2)c3).Count()); // 3 return 0;

}

4.9.2.2. Запрос о реализации интерфейса

Что будет, если клиент попытается использовать класс так, как если бы в нем был реализован метод, на самом деле в нем не реализованный? Проверим:

MyClass2 c2 = new MyClass2();

Console.WriteLine(((ITest2)c2).Count());

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

static void F1(object x)

{

if (x is ITest2) Console.WriteLine(((ITest2)x).Count()); else Console.WriteLine("Интерфейс ITest2 не реализован");

}

static void F2(object x)

{

ITest2 intf = x as ITest2;

if (intf != null) Console.WriteLine(intf.Count());

else Console.WriteLine("Интерфейс ITest2 не реализован");

}

static int Main()

{

332

MyClass2 c2 = new MyClass2();

MyClass4 c4 = new MyClass4();

F1(c2); // Интерфейс ITest2 не реализован F2(c2); // Интерфейс ITest2 не реализован

F2(c4); // 4 return 0;

}

4.9.2.3. Доступ к членам интерфейса

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

Пример:

static void F3(ITest i)

{

string s; i.F(out s);

Console.WriteLine(s);

Console.WriteLine(i.Count);

Console.WriteLine(i[5]);

}

static int Main()

{

MyClass2 c2 = new MyClass2();

F3(c2); return 0;

}

Вывод на консоль:

MyClass2

2

MyClass2.ITest.5

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

interface ITestComb : ITest, ITest2

{

}

class MyClass5 : ITestComb

// и т.д.

А чтобы не возникала неоднозначность при доступе к члену Count, ис-

333

пользуем преобразование типа ссылки на реализацию интерфейса:

static void F4(ITestComb i)

{

string s; i.F(out s);

Console.WriteLine(s); Console.WriteLine(((ITest)i).Count); Console.WriteLine(((ITest2)i).Count()); Console.WriteLine(i[7]);

}

static int Main()

{

MyClass5 c5 = new MyClass5();

F4(c5); return 0;

}

Вывод на консоль:

MyClass5

5

5

MyClass5.ITest.7

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

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

Пример: Samples\4.9\4_9_3_inherit.

4.9.3.1. Наследование реализаций интерфейсов

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

interface ITest

{

void Method1(); void Method2();

}

class Base : ITest

{

public void Method1()

334

{

Console.WriteLine("Base.Method1");

}

public virtual void Method2()

{

Console.WriteLine("Base.Method2");

}

}

class Child : Base

{

public new void Method1()

{

Console.WriteLine("Child.Method1");

}

public override void Method2()

{

Console.WriteLine("Child.Method2");

}

}

static int Main()

{

Base b = new Base(); Child c = new Child();

b.Method1(); // Base.Method1 c.Method1(); // Child.Method1 ((ITest)b).Method1(); // Base.Method1 ((ITest)c).Method1(); // Base.Method1

b.Method2(); // Base.Method2 c.Method2(); // Child.Method2 ((ITest)b).Method2(); // Base.Method2 ((ITest)c).Method2(); // Child.Method2 return 0;

}

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

4.9.3.2. Повторная реализация интерфейсов

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

335

interface ITest2

{

string A(); string B(); string C(); string D();

}

class Base2 : ITest2

{

public string A() { return "B.A"; } public string B() { return "B.B"; } string ITest2.C() { return "B.C"; } string ITest2.D() { return "B.D"; }

}

class Child2 : Base2, ITest2

{

string ITest2.A() { return "C.A"; } public string D() { return "C.D"; }

}

static int Main()

{

Child2 c2 = new Child2();

Console.WriteLine(c2.A()); // B.A Console.WriteLine(c2.B()); // B.B Console.WriteLine(((ITest2)c2).C()); // B.C Console.WriteLine(((ITest2)c2).A()); // C.A Console.WriteLine(c2.D()); // C.D

return 0;

}

Вывести на консоль строку «B.D», имея экземпляр дочернего класса, не получится, т.к. соответствующая реализация является явной, а при преобразовании к ссылке на реализацию интерфейса будет вызываться метод D() дочернего класса.

4.9.3.3. Абстрактные классы и интерфейсы

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

public delegate void TestD();

interface ITest3

{

event TestD TestEvent;

}

abstract class AbsClass : ITest2

336

{

public abstract string A(); public string B() { return ""; } public abstract string C(); string ITest2.D() { return ""; }

public abstract event TestD TestEvent;

}

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

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

Как мы помним, структуры и классы типов данных реализуют следующие интерфейсы:

Типы по значению (кроме структур) – IComparable, IFormattable (кроме логического и символьного типа), IConvertible, IEquatable (кроме перечислений);

Структура DateTime – IComparable, IFormattable, IConvertible;

Класс String – IComparable, ICloneable, IConvertible, IEnumerable;

Класс Array – ICloneable, IList, ICollection, IEnumerable.

Еще мы говорили об интерфейсе IDisposable, использующемся в операторе выделения ресурсов using. Что это им дает?

Пример: Samples\4.9\4_9_4_samples.

4.9.4.1. Ранжирование объектов

Интерфейс IComparable описывает всего один член:

int CompareTo(object obj);

Реализация этого метода должна возвращать –1, если логика приложения определяет, что текущий экземпляр объекта предшествует экземпляру obj, +1, если текущий экземпляр следует за obj, и 0, если их ранг совпадает. Для чисел, дат, строк и т.п. можно провести соответствие реализации данного интерфейса и операций отношения (табл. 4.4).

Табл. 4.4 – Операции отношения и метод CompareTo

Операция отношения

Результат метода CompareTo

 

 

Равно (x == y)

x.CompateTo(y) == 0

 

 

 

337

Не равно (x != y)

x.CompateTo(y) != 0

 

 

 

Больше (x > y)

x.CompateTo(y) >

0

 

 

Больше или равно (x >= y)

x.CompateTo(y) >= 0

 

 

 

Меньше (x < y)

x.CompateTo(y) <

0

 

 

Меньше или равно (x <= y)

x.CompateTo(y) <= 0

 

 

 

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

Пример:

class Rational : IComparable

{

public int Numerator = 0; public int Denominator = 1;

public Rational (int num, int den)

{

Numerator = num; Denominator = den;

}

public int CompareTo(object obj)

{

if (obj is Rational)

{

Rational r = (Rational)obj;

return Math.Sign((double)Numerator / Denominator – (double)r.Numerator / r.Denominator);

}

throw new ArgumentException("Нельзя дробь сравнить с " +

(obj != null ? obj.GetType().ToString() : "null"));

}

public override string ToString()

{

return String.Format("{0}/{1}", Numerator, Denominator);

}

}

static int Main()

{

Rational[] r = new Rational[3];

r[0] = new Rational(11, 5); r[1] = new Rational(-1, 6); r[2] = new Rational(3, 8);

Array.Sort(r);

foreach (Rational x in r) Console.WriteLine(x);

338

return 0;

}

Данный класс позволяет сортировать рациональные дроби. Вывод на консоль:

-1/6 3/8 11/5

Если не реализовать в классе Rational интерфейс IComparable, при вы-

полнении получим исключение System.InvalidOperationException.

Более простой вариант ранжирования обеспечивает обобщенный интерфейс IEquatable<T>. Он также описывает только один член:

bool Equals(T other);

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

ных коллекциях типа Dictionary<TKey, TValue>, List<T> и т.п., некоторые перегрузки метода Array.BinarySearch и т.д.

Пример:

class Rational : IEquatable<Rational>

{

/* описанное ранее тело класса */

public bool Equals(Rational r)

{

return (long)Numerator * r.Denominator == (long)r.Numerator * Denominator;

}

}

static int Main()

{

/* описанное ранее использование класса */

List<Rational> list = new List<Rational>();

list.AddRange(r);

Console.WriteLine("Индекс элемента 11/5: " + list.IndexOf(new

Rational(11, 5))); // 2 return 0;

}

339

4.9.4.2. Перечисления и коллекции

Перечислители

Основное назначение интерфейса IEnumerable – предоставление перечислителя, который поддерживает простой перебор элементов коллекции (итератор). Его реализации чаще всего используются в цикле foreach.

Интерфейс IEnumerable содержит объявление единственного метода:

IEnumerator GetEnumerator();

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

странстве имен System.Collections. Члены интерфейса IEnumerator:

void Reset(); bool MoveNext(); object Current;

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

foreach (<тип> v in x) <внедряемый оператор>

развертывается следующим образом:

{

IEnumerator e = x.GetEnumerator(); try

{

<тип> v; e.Reset();

while (e.MoveNext())

{

v = (<тип>)e.Current;

<внедряемый оператор>

}

}

finally

{

// Удаление ресурсов

}

}

Пример:

class VowelEnum : IEnumerator

{

private string S; private int pos;

340