Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 12 Интерфейсы, структуры и перечисления 320 Часть I. Язык C# та глава посвящена одному из самых важных средств языка C#: интерфейсу. Интерфейс определяет набор методов, которые будут реализованы классом. Сам интерфейс не реализует методы. Таким образом, интерфейс — это логическая конструкция, которая описывает методы, не устанавливая жестко способ их реализации. В этой главе рассматриваются еще два типа данных C#: структуры и перечисления. Структуры подобны классам, за исключением того, что они обрабатываются как типы значений, а не как ссылочные типы. Перечисления — это списки именованных целочисленных констант. Структуры и перечисления вносят существенный вклад в общую копилку средств и инструментов, составляющих среду программирования C#. Интерфейсы В объектно-ориентированном программировании иногда требуется определить, что класс должен делать, а не как он будет это делать. Вы уже видели такой подход на примере абстрактного метода. Абстрактный метод определяет сигнатуру для метода, но не обеспечивает его реализации. В производном классе каждый абстрактный метод, определенный базовым классом, реализуется по-своему. Таким образом, абстрактный метод задает интерфейс для метода, но не способ его реализации. Несмотря на всю полезность абстрактных классов и методов, эту идею можно развить. В C# предусмотрена возможность полностью отделить интерфейс класса от его реализации с помощью ключевого слова interface Интерфейсы синтаксически подобны абстрактным классам. Однако в интерфейсе ни один метод не может включать тело, т.е. интерфейс в принципе не предусматривает какой бы то ни было реализации. Он определяет, что должно быть сделано, но не уточняет, как. Коль скоро интерфейс определен, его может реализовать любое количество классов. При этом один класс может реализовать любое число интерфейсов. Для реализации интерфейса класс должен обеспечить тела (способы реализации) методов, описанных в интерфейсе. Каждый класс может определить собственную реализацию. Таким образом, два класса могут реализовать один и тот же интерфейс различными способами, но все классы поддерживают одинаковый набор методов. Следовательно, код, “осведомленный” о наличии интерфейса, может использовать объекты любого класса, поскольку интерфейс для всех объектов одинаков. Предоставляя программистам возможность применения такого средства программирования, как интерфейс, C# позволяет в полной мере использовать аспект полиморфизма, выражаемый как “один интерфейс — много методов”. Интерфейсы объявляются с помощью ключевого слова interface . Вот как выглядит упрошенная форма объявления интерфейса: interface имя { тип_возврата имя_метода1 ( список_параметров ); тип_возврата имя_метода2 ( список_параметров ); // тип_возврата имя_методаN ( список_параметров ); } Имя интерфейса задается элементом имя . Методы объявляются с использованием лишь типа возвращаемого ими значения и сигнатуры. Все эти методы, по сути, — абстрактные. Как упоминалось выше, для методов в интерфейсе не предусмотрены способы реализации. Следовательно, каждый класс, который включает интерфейс, должен реализовать все его методы. В интерфейсе методы неявно являются открытыми ( public - методами), при этом не разрешается явным образом указывать спецификатор доступа. Э Глава 12. Интерфейсы, структуры и перечисления 321 Рассмотрим пример интерфейса для класса, который генерирует ряд чисел, public interface ISeries { int getNext(); // Возвращает следующее число ряда. void reset(); // Выполняет перезапуск. void setStart(int x); // Устанавливает начальное // значение. } Этот интерфейс имеет имя ISeries . Хотя префикс “ I ” необязателен, многие программисты его используют, чтобы отличать интерфейсы от классов. Интерфейс ISeries объявлен открытым, поэтому он может быть реализован любым классом в любой программе. Помимо сигнатур методов интерфейсы могут объявлять сигнатуры свойств, индексаторов и событий. События рассматриваются в главе 15, поэтому здесь мы остановимся на методах, индексаторах и свойствах. Интерфейсы не могут иметь членов данных. Они не могут определять конструкторы, деструкторы или операторные методы. Кроме того, ни один член интерфейса не может быть объявлен статическим. Реализация интерфейсов Итак, если интерфейс определен, один или несколько классов могут его реализовать. Чтобы реализовать интерфейс, нужно указать его имя после имени класса подобно тому, как при создании производного указывается базовый класс. Формат записи класса, который реализует интерфейс, таков: class имя_класса : имя_интерфейса { // тело класса } Нетрудно догадаться, что имя реализуемого интерфейса задается с помощью элемента имя_интерфейса . Если класс реализует интерфейс, он должен это сделать в полном объеме, т.е. реализация интерфейса не может быть выполнена частично. Классы могут реализовать несколько интерфейсов. В этом случае имена интерфейсов отделяются запятыми. Класс может наследовать базовый класс и реализовать один или несколько интерфейсов. В этом случае список интерфейсов должно возглавлять имя базового класса. Методы, которые реализуют интерфейс, должны быть объявлены открытыми. Дело в том, что методы внутри интерфейса неявно объявляются открытыми, поэтому их реализации также должны быть открытыми. Кроме того, сигнатура типа в реализации метода должна в точности совпадать с сигнатурой типа, заданной в определении интерфейса. Рассмотрим пример реализации интерфейса ISeries , объявление которого приведено выше. Здесь создается класс с именем ByTwos , генерирующий ряд чисел, в котором каждое следующее число больше предыдущего на два. // Реализация интерфейса ISeries. class ByTwos : ISeries { int start; int val; public ByTwos() { start = 0; val = 0; } public int getNext() { 322 Часть I. Язык C# val +=2; return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = start; } } Как видите, класс ByTwos реализует все три метода, определенные интерфейсом ISeries . Иначе и быть не может, поскольку классу не разрешается создавать частичную реализацию интерфейса. Рассмотрим пример, демонстрирующий использование класса ByTwos , Вот его код: // Демонстрация использования интерфейса, // реализованного классом ByTwos. using System; class SeriesDemo { public static void Main() { ByTwos ob = new ByTwos(); for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.getNext() ); Console.WriteLine("\nПереход в исходное состояние."); ob.reset(); for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.getNext() ); Console.WriteLine("\nНачинаем с числа 100."); ob.setStart(100); for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.getNext()); } } Чтобы скомпилировать программу SeriesDemo , необходимо включить в процесс компиляции файлы, которые содержат классы ISeries , ByTwos и SeriesDemo . Для создания выполняемой программы компилятор автоматически скомпилирует все три файла. Если эти файлы называются, например, ISeries.cs , ByTwos.cs и SeriesDemo.cs , то программа скомпилируется посредством выполнения такой командной строки: >csc SeriesDemo.cs ISeries.cs ByTwos.cs Если вы используете интегрированную среду (IDE) Visual Studio, добавьте все эти три файла в свой C#-проект. Вполне допустимо также поместить их в один файл. Глава 12. Интерфейсы, структуры и перечисления 323 Вот результаты выполнения этой программы:. Следующее значение равно 2 Следующее значение равно 4 Следующее значение равно 6 Следующее значение равно 8 Следующее значение равно 10 Переход в исходное состояние. Следующее значение равно 2 Следующее значение равно 4 Следующее значение равно 6 Следующее значение равно 8 Следующее значение равно 10 Начинаем с числа 100. Следующее значение равно 102 Следующее значение равно 104 Следующее значение равно 106 Следующее значение равно 108 Следующее значение равно 110 В классах, которые реализуют интерфейсы, можно определять дополнительные члены. Например, в представленную ниже версию класса ByTwos добавлен метод getPrevious() , который возвращает предыдущее значение ряда. // Реализация интерфейса ISeries с дополнительно // определяемым методом getPrevious(). class ByTwos : ISeries { int start; int val; int prev; public ByTwos() { start = 0; val = 0; prev = -2; } public int getNext() { prev = val; val += 2; return val; } public void reset() { val = start; prev = start = 2; } public void setStart(int x) { start = x; val = start; prev = val - 2; } // Метод, не объявленный в интерфейсе ISeries. public int getPrevious() { return prev; } } 324 Часть I. Язык C# Обратите внимание на то, что добавление метода getPrevious() потребовало внесения изменений в реализацию методов, определенных интерфейсом ISeries . Но поскольку интерфейс для этих методов остается прежним, при изменении не разрушается код, написанный ранее. В этом и заключается одно из достоинств использования интерфейсов. Как упоминалось выше, интерфейс может реализовать любое количество классов. Рассмотрим, например, класс Primes , который генерирует ряд простых чисел. Обратите внимание на то, что его способ реализации интерфейса ISeries в корне отличается от используемого классом ByTwos // Использование интерфейса ISeries для реализации // ряда простых чисел. class Primes : ISeries { int start; int val; public Primes() { start = 2; val = 2; } public int getNext() { int i, j; bool isprime; val++; for(i = val; i < 1000000; i++) { isprime = true; for(j = 2; j < (i/j + 1); j++) { if((i%j)==0) { isprime = false; break; } } if(isprime) { val = i; break; } } return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = start; } } Здесь важно понимать, что, хотя классы Primes и ByTwos генерируют разные ряды чисел, оба они реализуют один и тот же интерфейс ISeries . И в этом нет ничего удивительного, поскольку каждый класс волен решить эту задачу так, как “считает” нужным. Глава 12. Интерфейсы, структуры и перечисления 325 Использование интерфейсных ссылок Возможно, вы будете несколько удивлены, узнав, что можно объявить ссылочную переменную интерфейсного типа. Другими словами, можно создать переменную-ссылку на интерфейс. Такая переменная может ссылаться на любой объект, который реализует ее интерфейс. При вызове метода для объекта посредством интерфейсной ссылки будет выполнена та версия указанного метода, которая реализована этим объектом. Этот процесс аналогичен использованию ссылки на базовый класс для доступа к объекту производного класса (см. главу 11). Использование интерфейсной ссылки демонстрируется в следующем примере. Здесь используется одна и та же интерфейсная переменная-ссылка, чтобы вызывать методы для объектов как класса ByTwos , так и класса Primes // Демонстрация использования интерфейсных ссылок. using System; // Определение интерфейса. public interface ISeries { int getNext(); // Возвращает следующее число ряда. void reset(); // Выполняет перезапуск. void setStart(int x); // Устанавливает начальное // значение. } // Используем интерфейс ISeries для генерирования // последовательности четных чисел. class ByTwos : ISeries { int start; int val; public ByTwos() { start = 0; val = 0; } public int getNext() { val +=2; return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = start; } } // Используем интерфейс ISeries для построения // ряда простых чисел. class Primes : ISeries { int start; int val; 326 Часть I. Язык C# public Primes() { start = 2; val = 2; } public int getNext() { int i, j; bool isprime; val++; for(i = val; i < 1000000; i++) { isprime = true; for(j = 2; j < (i/j + 1); j++) { if((i%j)==0) { isprime = false; break; } } if(isprime) { val = i; break; } } return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = start; } } class SeriesDemo2 { public static void Main() { ByTwos twoOb = new ByTwos(); Primes primeOb = new Primes(); ISeries ob; for(int i=0; i < 5; i++) { ob = twoOb; Console.WriteLine( "Следующее четное число равно " + ob.getNext()); ob = primeOb; Console.WriteLine( "Следующее простое число равно " + ob.getNext()); } } } Вот результаты выполнения этой программы: Следующее четное число равно 2 Следующее простое число равно 3 Следующее четное число равно 4 Следующее простое число равно 5 Глава 12. Интерфейсы, структуры и перечисления 327 Следующее четное число равно 6 Следующее простое число равно 7 Следующее четное число равно 8 Следующее простое число равно 11 Следующее четное число равно 10 Следующее простое число равно 13 В методе Main() объявляется переменная ob как ссылка на интерфейс ISeries Это означает, что ее можно использовать для хранения ссылок на любой объект, который реализует интерфейс ISeries . В данном случае она служит для ссылки на объекты twoOb и primeOb , которые являются экземплярами классов ByTwos и Primes , соответственно, причем оба класса реализуют один и тот же интерфейс, ISeries Важно понимать, что интерфейсная ссылочная переменная “осведомлена”" только о методах, объявленных “под сенью” ключевого слова interface . Следовательно, интерфейсную ссылочную переменную нельзя использовать для доступа к другим переменным или методам, которые может определить объект, реализующий этот интерфейс. Интерфейсные свойства Как и методы, свойства определяются в интерфейсе без тела. Ниже приведен формат спецификации свойства. // Интерфейсное свойство тип имя { get; set; } Свойства, предназначенные только для чтения или только для записи, содержат только get - или set -элемент, соответственно. Рассмотрим еще одну версию интерфейса ISeries и класса ByTwos , в котором для получения следующего элемента ряда и его установки используется свойство. // Использование свойства в интерфейсе. using System; public interface ISeries { // Интерфейсное свойство. int next { get; // Возвращает следующее число ряда. set; // Устанавливает следующее число ряда. } } // Реализация интерфейса ISeries. class ByTwos : ISeries { int val; public ByTwos() { val = 0; } // Получаем или устанавливаем значение ряда. public int next { get { 328 Часть I. Язык C# val +=2; return val; } set { val = value; } } } // Демонстрируем использование интерфейсного свойства. class SeriesDemo3 { public static void Main() { ByTwos ob = new ByTwos(); // Получаем доступ к ряду через свойство. for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.next); Console.WriteLine("\nНачинаем с числа 21"); ob.next = 21; for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.next); } } Результаты выполнения этой программы таковы: Следующее значение равно 2 Следующее значение равно 4 Следующее значение равно 6 Следующее значение равно 8 Следующее значение равно 10 Начинаем с числа 21 Следующее значение равно 23 Следующее значение равно 25 Следующее значение равно 27 Следующее значение равно 29 Следующее значение равно 31 Интерфейсные индексаторы В интерфейсе можно определить и индексатор. Объявление индексатора в интерфейсе имеет следующий формат записи: // Интерфейсный индексатор тип_элемента this[int индекс ]{ get; set; } Индексаторы, предназначенные только для чтения или только для записи, содержат только get - или set -метод, соответственно. Предлагаем еще одну версию интерфейса ISeries , в который добавлен индексатор, предназначенный только для чтения элемента ряда. Глава 12. Интерфейсы, структуры и перечисления 329 // Добавление в интерфейс индексатора. using System; public interface ISeries { // Интерфейсное свойство. int next { get; // Возвращает следующее число ряда. set; // Устанавливает следующее число ряда. } // Интерфейсный индексатор. int this[int index] { get; // Возвращает заданный член ряда. } } // Реализация интерфейса ISeries. class ByTwos : ISeries { int val; public ByTwos() { val = 0; } // Получаем или устанавливаем значение с помощью // свойства. public int next { get { val += 2; return val; } set { val = value; } } // Получаем значение с помощью индексатора. public int this[int index] { get { val = 0; for(int i=0; i 2; return val; } } } // Демонстрируем использование интерфейсного индексатора. class SeriesDemo4 { public static void Main() { ByTwos ob = new ByTwos(); // Получаем доступ к ряду посредством свойства. for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение равно " + ob.next); |