Лекция 10 5.2. Перегрузка членов Перегрузка означает определение нескольких членов класса с одним и тем же именем, но с разной сигнатурой (описанием параметров). Перегрузку чаще всего применяют с методами. С# позволяет также перегружать операторы, обеспечивая таким образом нестандартное поведение пользовательских типов данных по отношению к операторам. При вызове перегруженного метода исполняющая среда проверяет типы переданных аргументов, сравнивает их список с сигнатурами доступных перегруженных методов, и вызывает тот, чья сигнатура совпала с имеющимся списком. Если ни один из перегруженных методов не принимает указанный набор аргументов, генерируется ошибка. Для создания перегруженных методов не существует специальных правил, кроме следующего: у перегруженного метода должно быть то же имя, что и у существующего метода, но другая сигнатура. Версии перегруженного метода могут также отличаться уровнями доступа и типом возвращаемого значения (но это не обязательно). Например: public void DisplayMessage (int I) { MessageBox.Show(I.ToString( )); } public void DisplayMessage (string S) { MessageBox.Show(S); } Перегрузка операторов бывает полезной при разработке пользовательских типов данных, когда требуется определить арифметические, логические операторы или операторы сравнения, способные работать с этими типами. Рассмотрим следующую структуру: 2 public struct HoursWorked { float RegularHours, OvertimeHours; } Подобную структуру можно использовать в приложении для учета рабочего времени и сверхурочных. но при работе с несколькими экземплярами такой структуры возможны сложности. Представим, например, что требуется сложить значения двух экземпляров такой структуры. Чтобы решить эту задачу, придется писать новый метод, способный складывать экземпляры такой структуры. Для сложения трех и более экземпляров потребуется вызвать этот метод несколько раз. С# позволяет определять поведение оператора при обработке пользовательских типов данных. Например, чтобы получить возможность суммировать структуру из примера, можно создать перегруженную версию оператора «+», добавив к ней необходимую функциональность. Перегруженные операторы создаются подобно статическим методам с помощью ключевого слова operator, имеющего следующий синтаксис: public static type operator op (Argument1[, Argument2]) { // Здесь должна быть реализация } Здесь type – пользовательский тип, с которым работает оператор, который одновременно является и типом значения, возвращаемого оператором. Argument1 и Argument2 – аргументы оператора. Любой унарный оператор обрабатывает только один аргумент с типом type. Бинарные операторы требуют два аргумента, хотя бы один из которых должен относиться к типу type. Op – это собственно оператор, например +, –, >, != и так далее. Перегруженные операторы необходимо объявлять с модификаторами public и static, внутри описания пользовательского типа данных (структуры или класса), для работы с которым он предназначен. Например: 3 public struct HoursWorked { float RegularHours, OvertimeHours; public static HoursWorked operator + (HoursWorked a, HoursWorked b) { HoursWorked Result = new HoursWorked (); Result.RegularHours = a.RegularHours + b.RegularHours; Result.OvertimeHours = a.OvertimeHours + b.OvertimeHours; return Result; } } Перегруженный оператор можно использовать в программе как любой другой оператор. Например, можно «складывать» более одной переменной: HoursWorked total = new HoursWorked (); total = Sunday + Tuesday + Monday; Как и в случае с перегруженными методами, разрешается создание различных реализаций перегруженных операторов при условии, что они отличаются сигнатурами. Заметим, что в отличие от неуправляемого C++, перегрузка оператора присваивания в C# не допускается. Возможно, это связано с особой ролью оператора присваивания по отношению к целому виду членов – свойствам. Резюме Благодаря перегрузке можно создать несколько методов с одинаковым именем и разными реализациями. Перегруженные методы должны отличаться сигнатурами, но могут иметь одинаковые уровни доступа и типы возвращаемых значений. Перегруженные методы объявляются так же, как и обычные. В программах на С# можно определять нестандартные версии операторов, предназначенные для работы с пользовательскими типами данных. Перегруженные операторы объявляются с модификаторами public static и ключевым словом operator. 4 5.3. Реализация полиморфизма посредством интерфейсов Интерфейсы определяют внешнее поведение класса (или структуры). Интерфейс как аналог класса может содержать лишь 3 вида членов: методы, свойства и события. Кроме того, для методов в определении интерфейса указывается лишь описание типов параметров и возвращаемых значений, без реализации. Реализация членов, объявленных в интерфейсе, целиком ложится на класс, в котором будет реализован этот интерфейс. Таким образом, в разных объектах одни и те же члены интерфейса могут реализоваться по-разному. Рассмотрим интерфейс IShape, определяющий единственный метод – CalculateArea. При этом класс Circle, в котором реализован этот интерфейс, вычисляет площадь фигуры не так, как это делает класс Square, в котором также реализован этот интерфейс. Тем не менее, объект, которому необходимо использовать IShape, может единообразно вызывать метод CalculateArea из класса Circle или Square и получать корректный результат. Интерфейсы определяются с помощью ключевого слова interface. Методы-члены интерфейса описываются с обычной сигнатурой метода, но без модификаторов доступа (public, private и других). Модификатор доступа, заданный для самого интерфейса, определяет уровень доступа для всех его членов. Например: public interface IDrivable { void GoForward (int Speed); void Halt (); int DistanceTraveled (); int FuelLevel { get; set; } event System.EventHandler OutOfFuel; } 5 Смысл методов get и set для свойств интерфейса состоит лишь в том, чтобы определить, может ли это свойство быть прочитанным или записанным. Так, если не указать set, то свойство будет «только для чтения». Объект, в котором реализован некоторый интерфейс, предоставляет этот интерфейс для использования другими объектами. Например: public void GoSomewhere (IDrivable v) { // Реализация опущена } Этому методу в качестве параметра можно передать любой объект, в котором реализован интерфейс IDrivable. Во время вызова передаваемый объект неявно преобразуется в тип соответствующего интерфейса. Кроме того, разрешается явно преобразовывать объекты, реализующие некоторый интерфейс, к типу этого интерфейса. Следующий пример демонстрирует преобразование объекта Truck к типу интерфейса IDrivable (чтобы это работало, в объекте Truck необходимо реализовать интерфейс IDrivable): Truck myTruck = new Truck (); IDrivable myVehicle; myVehicle = (IDrivable) myTruck; Чтобы в классе или структуре реализовать некоторый интерфейс, необходимо указать это в заголовке: public class Truck: IDrivable { // Реализация опущена } В типе могут реализовываться несколько интерфейсов. Чтобы объявить такой тип, достаточно указать все необходимые интерфейсы через запятую: public class Truck: IDrivable, IFuelBurning, ICargoCarrying { // Реализация опущена } 6 Если в классе или структуре объявлена реализация некоторого интерфейса, то она должна быть полной. Для каждого члена интерфейса необходимо в реализующем классе (или структуре) определить член с тем же именем, причем этот член класса объявляется с уровнем доступа, с которым объявлен сам интерфейс. Например: public interface IDrivable { void GoForward (int Speed); } public class Truck: IDrivable { public void GoForward (int Speed) { // Реализация опущена } } Реализованные таким способом члены интерфейса доступны как интерфейсу, так и самому реализующему классу: Truck track = new Truck (); track.GoForward (120); // Это работает (track as IDrivable).GoForward (120); // И это работает Допускается явная реализация интерфейса в классе. Реализованные явно члены доступны только после преобразования объекта в тип интерфейса. Чтобы реализовать член интерфейса явно, следует объявить в классе одноименный член, указав при нем полное имя интерфейса. Например: public class Truck: IDrivable { void IDrivable.GoForward (int Speed) { // Реализация опущена } } …………………………………………………………… 7 track.GoForward (120); // Это не работает (track as IDrivable).GoForward (120); // Это работает Поскольку в этом примере член интерфейса реализован явно, его уровень доступа в реализации не указывается. Он определяется модификатором, заданным для самого интерфейса. Явная реализация интерфейса оказывается полезной, например, в случае, когда класс реализует два интерфейса, содержащие одноименные методы. Резюме Интерфейс определяет поведение объекта и содержит описание поддерживаемых членов. Ими могут быть методы, свойства и события. Объект, реализующий некоторый интерфейс, может взаимодействовать с любым другим объектом, которому необходим этот интерфейс. За реализацию членов интерфейса отвечает класс или структура, в которой этот интерфейс реализован. В классах, как и в структурах, допускается реализация нескольких интерфейсов. Класс или структура, реализующая некоторый интерфейс, должна обеспечить реализацию всех членов, объявленных в этом интерфейсе. В С# члены интерфейса реализуются двумя способами: реализация члена с именем, сигнатурой и уровнем доступа, идентичными соответствующему члену интерфейса. Такие члены доступны как через реализующий класс, так и через интерфейс; явная реализация члена интерфейса с указанием его полного имени. Такие члены доступны только через интерфейс.