1629295407-c61bfe4caba98380ea3e7cdae6295416 (846200), страница 39
Текст из файла (страница 39)
Подробнее о методах и классах205В этом примере определены только две версии метода f(): одна с int-, а другая сdouble-параметром. Тем не менее методу f() можно передать помимо значений типа intи double также значения типа byte, short или float. В случае передачи byte-илиshort-параметров C# автоматически преобразует их в значения типа int (т.е. будетвызвана версия f(int)). В случае передачи float-параметра его значение будетпреобразовано в значение типа double и будет вызвана версия f(double).Здесь важно понимать, что автоматическое преобразование применяется только в томслучае, когда не существует прямого соответствия параметра и аргумента.
Дополнимпредыдущую программу версией метода f(), в которой определен параметр типа byte.// Добавление к предыдущей программе версии f(byte).using System;class Overload2 {public void f(byte x) {Console.WriteLine("Внутри метода f(byte): " + x);}public void f(int x) {Console.WriteLine("Внутри метода f(int): " + x);}}public void f(double x) {Console.WriteLine("Внутри метода f(double): " + x);}class TypeConv {public static void Main() {Overload2 ob = new Overload2();int i = 10;double d = 10.1;byte b = 99;short s = 10;float f = 11.5F;ob.f(i); // Вызов метода ob.f(int).ob.f(d); // Вызов метода ob.f(double).ob.f(b); // Вызов метода ob.f(byte) - теперь без// преобразования типов.ob.f(s); // Вызов метода ob.f(int) — выполняется// преобразование типов.}}ob.f(f); // Вызов метода ob.f(double) — выполняется// преобразование типов.Этот вариант программы генерирует такие результаты:Внутри метода f(int): 10Внутри метода f(double): 10,1206Часть I.
Язык C#Внутри метода f(byte): 99Внутри метода f(int): 10Внутри метода f(double): 11,5В этом варианте, поскольку существует версия метода f(), которая предназначенадля приема аргумента типа byte, при вызове метода f() с byte-аргументом будетвызвана версия f(byte), и автоматического преобразования byte-аргумента в значениетипа int не произойдет.Наличие как ref-, так и out-модификатора играет роль в “зачете” перегруженныхфункций. Например, в следующем фрагменте кода определяются два различных метода.public void f(int x) {Console.WriteLine("Внутри метода f(int): " + x);}public void f(ref int x) {Console.WriteLine("Внутри метода f(ref int): " + x);}Таким образом, при выполнении инструкцииob.f(i);вызывается метод f(int x), но при выполнении инструкцииob.f(ref i);вызывается метод f(ref int x).Посредством перегрузки методов в C# поддерживается полиморфизм, поскольку этоединственный способ реализации в C# парадигмы “один интерфейс — множество методов”.Чтобы понять, как это происходит, рассмотрим следующее.
В языке, который неподдерживает перегрузку методов, каждый метод должен иметь уникальное имя. Однакочасто нужно реализовать один и тот же метод для различных типов данных. Возьмем,например, функцию, возвращающую абсолютное значение. В языках, которые неподдерживают перегрузку методов, обычно существует три или даже больше версий этойфункции, причем их имена незначительно отличаются.
Например, в языке C функцияabs() возвращает абсолютное значение (модуль) целого числа, функция labs()возвращает модуль длинного целочисленного значения, а fabs() — модуль значения сплавающей точкой. Поскольку язык C не поддерживает перегрузку методов, каждаяфункция должна иметь собственное имя, несмотря на то, что все три функции выполняютпо сути одно и то же действие.
Это делает ситуацию сложнее, чем она есть на самом деле.Другими словами, при одних и тех же действиях программисту необходимо помнить именавсех трех (в данном случае) функций. Язык C# избавлен от ненужного “размножения” имен,поскольку все методы получения абсолютного значения могут использовать одно и то жеимя. И в самом деле, библиотека стандартных классов C# включает метод полученияабсолютного значения с именем Abs(). Этот метод перегружается C#-классомSystem.Math, что позволяет обрабатывать значения всех числовых типов, используя одноимя метода.
Определение того, какая именно версия метода должна быть вызвана, основанона типе передаваемого аргумента.Принципиальная значимость перегрузки состоит в том, что она позволяет обращатьсяк связанным методам посредством одного, общего для всех имени. Следовательно, имяAbs() представляет общее действие, которое выполняется во всех случаях. Компиляторуостается правильно выбрать конкретную версию при конкретных обстоятельствах. Апрограммисту нужно помнить лишь общую операцию, которая связана с именем того Илииного метода.
Благодаря полиморфизму применение нескольких имен сводится к одному.Несмотря на простоту приведенного примера, он все же позволяет понять, что перегрузкаспособна упростить процесс программирования.Глава 8. Подробнее о методах и классах207Необходимо подчеркнуть, что каждая версия перегруженного метода можетвыполнять определенные действия. Не существует правила, которое бы обязывалопрограммиста связывать перегруженные методы общими действиями. Однако с точкизрения стилистики перегрузка методов все-таки подразумевает определенное “родство” еговерсий.
Таким образом, несмотря на то, что вы можете использовать одно и то же имя дляперегрузки не связанных общими действиями методов, этого делать не стоит. Например, впринципе можно использовать имя sqr для создания метода, который возвращает квадратцелого числа, и метода, который возвращает значение квадратного корня из вещественногочисла. Но поскольку эти операции фундаментально различны, применение механизмаперегрузки методов в этом случае сводит на нет его первоначальную цель. Хороший стильпрограммирования состоит в организации перегрузки тесно связанных операций.В C# используется термин сигнатура (signature), который представляет собой имяметода со списком его параметров.
Таким образом, в целях обеспечения перегрузкиникакие два метода внутри одного и того же класса не должны иметь одинаковуюсигнатуру. Обратите внимание на то, что сигнатура не включает тип значения,возвращаемого методом, поскольку этот фактор не используется в C# для принятиярешения о выполнении требуемого перегруженного метода. Сигнатура также не включаетparams-параметр, если таковой существует. Другими словами, модификатор params неявляется определяющим фактором отличия одного перегруженного метода от другого.Перегрузка конструкторовПодобно другим методам, конструкторы также можно перегружать. Это позволяетсоздавать объекты различными способами.
Рассмотрим следующую программу:// Демонстрация перегруженных конструкторов.using System;class MyClass {public int x;public MyClass() {Console.WriteLine("Внутри конструктора MyClass().");x = 0;}public MyClass(int i) {Console.WriteLine("Внутри конструктора MyClass(int).");x = i;}public MyClass(double d) {Console.WriteLine("Внутри конструктора MyClass(double).");x = (int) d;}public MyClass(int i, int j) {Console.WriteLine("Внутри конструктора MyClass(int, int).");x = i * j;208Часть I.
Язык C#}}class OverloadConsDemo {public static void Main() {MyClass t1 = new MyClass();MyClass t2 = new MyClass(88);MyClass t3 = new MyClass(17.23);MyClass t4 = new MyClass(2, 4);}}Console.WriteLine("t1.x:Console.WriteLine("t2.x:Console.WriteLine("t3.x:Console.WriteLine("t4.x:""""++++t1.x);t2.x);t3.x);t4.x);При выполнении этой программы получаем следующие результаты:Внутри конструктора MyClass().Внутри конструктора MyClass(int).Внутри конструктора MyClass(double).Внутри конструктора MyClass(int, int).t1.x: 0t2.x: 88t3.x: 17t4.x: 8Конструктор MyClass() перегружен четырежды, и все конструкторы создаютобъекты по-разному. В зависимости от того, какие параметры заданы при выполненииоператора new, вызывается соответствующий конструктор.
Перегружая конструкторкласса, вы тем самым предоставляете пользователю этого класса определенную гибкость ввыборе способа создания объектов.Одна из самых распространенных причин перегрузки конструкторов — возможностьинициализации одного объекта с помощью другого. Например, вот как выглядитусовершенствованная версия представленного выше класса Stack, которая позволяетсоздать один стек на основе другого:// Класс стека для хранения символов.using System;class Stack {// Эти члены закрыты.char[] stck; // Этот массив содержит стек.int tos; // Индекс вершины стека.// Создаем пустой объект класса Stack заданного размера.public Stack(int size) {stck = new char[size]; // Выделяем память для стека.tos = 0;}// Создаем Stack-объект на основе существующего стека.public Stack(Stack ob) {// Выделяем память для стека.stck = new char[ob.stck.Length];// Копируем элементы в новый стек.for(int i=0; i < ob.tos; i++)Глава 8.