Текст книги "C# 4.0: полное руководство"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 17 (всего у книги 83 страниц)
В C# допускается совместное использование одного и того же имени двумя или более методами одного и того же класса, при условии, что их параметры объявляются по-разному. В этом случае говорят, что методы перегружаются, а сам процесс называется перегрузкой методов. Перегрузка методов относится к одному из способов реализации полиморфизма в С#.
В общем, для перегрузки метода достаточно объявить разные его варианты, а об остальном позаботится компилятор. Но при этом необходимо соблюсти следующее важное условие: тип или число параметров у каждого метода должны быть разными. Совершенно недостаточно, чтобы два метода отличались только типами возвращаемых значений. Они должны также отличаться типами или числом своих параметров. (Во всяком случае, типы возвращаемых значений дают недостаточно сведений компилятору С#, чтобы решить, какой именно метод следует использовать.) Разумеется, перегружаемые методы могут отличаться и типами возвращаемых значений. Когда вызывается перегружаемый метод, то выполняется тот его вариант, параметры которого соответствуют (по типу и числу) передаваемым аргументам.
Ниже приведен простой пример, демонстрирующий перегрузку методов.
// Продемонстрировать перегрузку методов.
using System;
class Overload {
public void OvlDemo() {
Console.WriteLine(«Без параметров»);
}
// Перегрузка метода OvlDemo с одним целочисленным параметром,
public void OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
}
// Перегрузка метода OvlDemo с двумя целочисленными параметрами,
public int OvlDemo(int a, int b) {
Console.WriteLine("Два параметра: " + a + " " + b); return a + b;
}
// Перегрузка метода OvlDemo с двумя параметрами типа double,
public double OvlDemo(double a, double b) {
Console.WriteLine("Два параметра типа double: " + a + " "+ b) ;
return a + b;
}
}
class OverloadDemo {
static void Main() {
Overload ob = new Overload();
int resI;
double resD;
// Вызвать все варианты метода OvlDemo().
ob.OvlDemo();
Console.WriteLine();
ob.OvlDemo(2);
Console.WriteLine();
resI = ob.OvlDemo(4, 6);
Console.WriteLine("Результат вызова метода ob.OvlDemo(4, 6): "
+
resI);
Console.WriteLine();
resD = ob.OvlDemo(1.1, 2.32);
Console.WriteLine(«Результат вызова метода ob.OvlDemo(1.1, 2.32):»
+ resD);
}
}
Вот к какому результату приводит выполнение приведенного выше кода.
Без параметров
Один параметр: 2
Два параметра: 4 6
Результат вызова метода ob.OvlDemo(4, 6): 10
Два параметра типа double: 1.1 2.32
Результат вызова метода ob.OvlDemo(1.1, 2.32): 3.42
Как видите, метод OvlDemo()
перегружается четыре раза. Первый его вариант не получает параметров, второй получает один целочисленный параметр, третий – два целочисленных параметра, а четвертый – два параметра типа double
. Обратите также внимание на то, что два первых варианта метода OvlDemo()
возвращают значение типа void
, а по существу, не возвращают никакого значения, а два других – возвращают конкретное значение. И это совершенно допустимо, но, как пояснялось выше, тип возвращаемого значения не играет никакой роли для перегрузки метода. Следовательно, попытка использовать два разных (по типу возвращаемого значения) варианта метода OvlDemo()
в приведенном ниже фрагменте кода приведет к ошибке.
// Одно объявление метода OvlDemo(int) вполне допустимо,
public void OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
}
/* Ошибка! Два объявления метода OvlDemo(int) не допускаются, хотя они и возвращают разнотипные значения. */
public int OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
return a * a;
}
Как следует из комментариев к приведенному выше коду, отличий в типах значений, возвращаемых обоими вариантами метода OvlDemo()
, оказывается недостаточно для перегрузки данного метода.
И как пояснялось в главе 3, в C# предусмотрен ряд неявных (т.е. автоматических) преобразований типов. Эти преобразования распространяются также на параметры перегружаемых методов. В качестве примера рассмотрим следующую программу.
// Неявные преобразования типов могут повлиять на
// решение перегружать метод.
using System;
class Overload2 {
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x);
}
public void MyMeth(double x) {
Console.WriteLine("В методе MyMeth(double): " + x);
}
}
class TypeConv {
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.MyMeth(i); // вызвать метод ob.MyMeth(int)
ob.MyMeth(d); // вызвать метод ob.MyMeth(double)
ob.MyMeth(b); // вызвать метод ob.MyMeth(int) – с преобразованием типа
ob.MyMeth(s); // вызвать метод ob.MyMeth(int) – с преобразованием типа
ob.MyMeth(f); // вызвать метод ob.MyMeth(double) – с преобразованием типа
}
}
При выполнении этой программы получается следующий результат.
В методе MyMeth(int): 10
В методе MyMeth(double): 10.1
В методе MyMeth(int): 99
В методе MyMeth(int): 10
В методе MyMeth(double): 11.5
В данном примере определены только два варианта метода MyMeth()
: с параметром типа int
и с параметром типа double
. Тем не менее методу MyMeth()
можно передать значение типа byte, short или float.
Так, если этому методу передается значение типа byte
или short,
то компилятор C# автоматически преобразует это значение в тип int
и в итоге вызывается вариант MyMeth (int)
данного метода. А если ему передается значение типа float
, то оно преобразуется в тип double
и в результате вызывается вариант MyMeth (double)
данного метода.
Следует, однако, иметь в виду, что неявные преобразования типов выполняются лишь в том случае, если отсутствует точное соответствие типов параметра и аргумента. В качестве примера ниже приведена чуть измененная версия предыдущей программы, в которую добавлен вариант метода MyMeth(),
где указывается параметр типа byte.
// Добавить метод MyMeth(byte).
using System;
class Overload2 {
public void MyMeth(byte x) {
Console.WriteLine("В методе MyMeth(byte): " + x);
}
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x) ;
}
public void MyMeth(double x) {
Console.WriteLine("В методе MyMeth(double): " + x);
}
}
class TypeConv {
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.MyMeth(i); // вызвать метод ob.MyMeth(int)
ob.MyMeth(d); // вызвать метод ob.MyMeth(double)
ob.MyMeth(b); // вызвать метод ob.MyMeth(byte) —
// на этот раз без преобразования типа
ob.MyMeth(s); // вызвать метод ob.MyMeth(int) – с преобразованием типа
ob.MyMeth(f); // вызвать метод ob.MyMeth(double) – с преобразованием типа
}
}
Выполнение этой программы приводит к следующему результату.
В методе MyMeth(int): 10
В методе MyMeth(double): 10.1
В методе MyMeth(byte): 99
В методе MyMeth(int): 10
В методе MyMeth(double): 11.5
В этой программе присутствует вариант метода MyMeth(),
принимающий аргумент типа byte, поэтому при вызове данного метода с аргументом типа byte
выбирается его вариант MyMeth (byte)
без автоматического преобразования в тип int
.
Оба модификатора параметров, ref
и out,
также учитываются, когда принимается решение о перегрузке метода. В качестве примера ниже приведен фрагмент кода, в котором определяются два совершенно разных метода.
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x);
}
public void MyMeth(ref int x) {
Console.WriteLine("В методе MyMeth(ref int): " + x);
}
Следовательно, при обращении
ob.MyMeth(i)
вызывается метод MyMeth (int x),
но при обращении
ob.MyMeth(ref i)
вызывается метод MyMeth(ref int x).
Несмотря на то что модификаторы параметров ref
и out
учитываются, когда принимается решение о перегрузке метода, отличие между ними не столь существенно. Например, два следующих варианта метода MyMeth()
оказываются недействительными.
// Неверно!
public void MyMeth(out int x) {//...
public void MyMeth(ref int x) { // ...
В данном случае компилятор не в состоянии различить два варианта одного и того же метода MyMeth()
только на основании того, что в одном из них используется параметр out
, а в другом – параметр ref
.
Перегрузка методов поддерживает свойство полиморфизма, поскольку именно таким способом в C# реализуется главный принцип полиморфизма: один интерфейс – множество методов. Для того чтобы стало понятнее, как это делается, обратимся к конкретному примеру. В языках программирования, не поддерживающих перегрузку методов, каждому методу должно быть присвоено уникальное имя. Но в программировании зачастую возникает потребность реализовать по сути один и тот же метод для обработки разных типов данных. Допустим, что требуется функция, определяющая абсолютное значение. В языках, не поддерживающих перегрузку методов, обычно приходится создавать три или более вариантов такой функции с несколько отличающимися, но все же разными именами. Например, в С функция abs()
возвращает абсолютное значение целого числа, функция labs() – абсолютное значение длинного целого числа, а функция fabs() – абсолютное значение числа с плавающей точкой обычной (одинарной) точности.
В С перегрузка не поддерживается, и поэтому у каждой функции должно быть свое, особое имя, несмотря на то, что все упомянутые выше функции, по существу, делают одно и то же – определяют абсолютное значение. Но это принципиально усложняет положение, поскольку приходится помнить имена всех трех функций, хотя они реализованы по одному и тому же основному принципу. Подобные затруднения в C# не возникают, поскольку каждому методу, определяющему абсолютное значение, может быть присвоено одно и то же имя. И действительно, в состав библиотеки классов для среды .NET Framework входит метод Abs(),
который перегружается в классе System.Math
для обработки данных разных числовых типов. Компилятор C# сам определяет, какой именно вариант метода Abs()
следует вызывать, исходя из типа передаваемого аргумента.
Главная ценность перегрузки заключается в том, что она обеспечивает доступ к связанным вместе методам по общему имени. Следовательно, имя Abs
обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с намного более сложными ситуациями в программировании.
Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не существует какого-то одного правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имя Sqr
можно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции. Такое применение перегрузки методов противоречит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции.
В C# определено понятие сигнатуры, обозначающее имя метода и список его параметров; Применительно к перегрузке это понятие означает, что в одном классе не должно существовать двух методов с одной и той же сигнатурой. Следует подчеркнуть, что в сигнатуру не входит тип возвращаемого значения, поскольку он не учитывается, когда компилятор C# принимает решение о перегрузке метода. В сигнатуру не входит также модификатор 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;
}
}
class OverloadConsDemo {
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: " + t1.x);
Console.WriteLine("t2.х: " + t2.x);
Console.WriteLine("t3.x: " + t3.x);
Console.WriteLine("t4.x: " + t4.x);
}
}
При выполнении этой программы получается следующий результат.
В конструкторе MyClass().
В конструкторе MyClass (int) .
В конструкторе MyClass(double).
В конструкторе MyClass (int, int).
t1.x: 0
t2.x: 88
t3.x: 17
t4.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++)
stck[i] = ob.stck[i];
// Установить переменную tos для нового стека,
tos = ob.tos;
}
// Поместить символы в стек,
public void Push(char ch) {
if(tos==stck.Length) {
Console.WriteLine(« – Стек заполнен.»);
return;
}
stck[tos] = ch;
tos++;
}
// Извлечь символ из стека,
public char Pop() {
if(tos==0) {
Console.WriteLine (« – Стек пуст.»);
return (char) 0;
}
tos–;
return stck[tos];
}
// Возвратить значение true, если стек заполнен,
public bool IsFull() {
return tos==stck.Length;
}
// Возвратить значение true, если стек пуст,
public bool IsEmpty() {
return tos==0;
}
// Возвратить общую емкость стека,
public int Capacity() {
return stck.Length;
}
// Возвратить количество объектов, находящихся в настоящий момент в стеке,
public int GetNum() {
return tos;
}
}
// Продемонстрировать применение класса Stack.
class StackDemo {
static void Main() {
Stack stk1 = new Stack(10);
char ch;
int i;
// Поместить ряд символов в стек stk1.
Console.WriteLine(«Поместить символы А-J в стек stk1.»);
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
// Создать копию стека stck1.
Stack stk2 = new Stack(stk1);
// Вывести содержимое стека stk1.
Console.Write("Содержимое стека stk1: ");
while ( !stk1.IsEmpty() ) {
ch = stk1.Pop();
Console.Write(ch);
}
Console.WriteLine();
Console.Write("Содержимое стека stk2: ");
while ( !stk2.IsEmpty() ) {
ch = stk2.Pop();
Console.Write(ch);
}
Console.WriteLine («n»);
}
}
Результат выполнения этой программы приведен ниже.
Поместить символы А-J в стек stk1.
Содержимое стека stk1: JIHGFEDCBA
Содержимое стека stk2: JIHGFEDCBA
В классе StackDemo
сначала конструируется первый стек (stk1
), заполняемый символами. Затем этот стек используется, для конструирования второго стека (stk2
). Это приводит к выполнению следующего конструктора класса Stack.
// Сконструировать объект класса Stack из существующего стека,
public Stack(Stack ob) {
// Распределить память для стека,
stck = new char[ob.stck.Length];
// Скопировать элементы в новый стек,
for (int i=0; i < ob.tos; i++)
stck[i] = ob.stck[i];
// Установить переменную tos для нового стека,
tos = ob.tos;
}
В этом конструкторе сначала распределяется достаточный объем памяти для массива, чтобы хранить в нем элементы стека, передаваемого в качестве аргумента ob
. Затем содержимое массива, образующего стек ob, копируется в новый массив, после чего соответственно устанавливается переменная tos
, содержащая индекс вершины стека. По завершении работы конструктора новый и исходный стеки существуют как отдельные, хотя и одинаковые объекты.
Вызов перегружаемого конструктора с помощью ключевого слова this
Когда приходится работать с перегружаемыми конструкторами, то иногда очень полезно предоставить возможность одному конструктору вызывать другой. В C# это дается с помощью ключевого слова this
. Ниже приведена общая форма такого вызова.
имя_конструктора{список_параметров1) : this (список_параметров2) {
// ... Тело конструктора, которое может быть пустым.
}
В исходном конструкторе сначала выполняется перегружаемый конструктор, список параметров которого соответствует критерию список_параметров2, а затем все остальные операторы, если таковые имеются в исходном конструкторе. Ниже приведен соответствующий пример.
// Продемонстрировать вызов конструктора с помощью ключевого слова this.
using System;
class XYCoord {
public int x, y;
public XYCoord() : this(0, 0) {
Console.WriteLine(«В конструкторе XYCoord()»);
}
public XYCoord(XYCoord obj) : this(obj.x, obj.y) {
Console.WriteLine(«В конструкторе XYCoord(obj)»);
}
public XYCoord(int i, int j) {
Console.WriteLine(«В конструкторе XYCoord(int, int)»); x = i;
y = j;
}
}
class OverloadConsDemo {
static void Main() {
XYCoord tl = new XYCoord();
XYCoord t2 = new XYCoord(8, 9);
XYCoord t3 = new XYCoord(t2);
Console.WriteLine("tl.x,
tl.y:
" +
tl.x
+
", " +
tl.y);
Console.WriteLine("t2.x,
t2.y:
" +
t2.x
+
", " +
t2.y);
Console.WriteLine("t3.x,
t3.y:
" +
t3.x
+
", " +
t3.y);
}
}
Выполнение этого кода приводит к следующему результату.
В конструкторе XYCoord(int, int)
В конструкторе XYCoord()
В конструкторе XYCoord(int, int)
В конструкторе XYCoord(int, int)
В конструкторе XYCoord(obj)
tl.x, tl.y: 0, 0
t2.x, t2.у: 8, 9
t3.x, t3.у: 8, 9
Код в приведенном выше примере работает следующим образом. Единственным конструктором, фактически инициализирующим поля х и у в классе XYCoord
, является конструктор XYCoord(int, int).
А два других конструктора просто вызывают этот конструктор с помощью ключевого слова this
. Например, когда создается объект t1, то вызывается его конструктор XYCoord(),
что приводит к вызову this(0, 0),
который в данном случае преобразуется в вызов конструктора XYCoord(0, 0)
. То же самое происходит и при создании объекта t2.
Вызывать перегружаемый конструктор с помощью ключевого слова this
полезно, в частности, потому, что он позволяет исключить ненужное дублирование кода. В приведенном выше примере нет никакой необходимости дублировать во всех трех конструкторах одну и ту же последовательность инициализации, и благодаря применению ключевого слова this
такое дублирование исключается. Другое преимущество организации подобного вызова перезагружаемого конструктора заключается в возможности создавать конструкторы с задаваемыми «по умолчанию» аргументами, когда эти аргументы не указаны явно. Ниже приведен пример создания еще одного конструктора XYCoord
.
public XYCoord(int х) : this(х, х) { }
По умолчанию в этом конструкторе для координаты у автоматически устанавливается то же значение, что и для координаты у. Конечно, пользоваться такими конструкциями с задаваемыми «по умолчанию» аргументами следует благоразумно и осторожно, чтобы не ввести в заблуждение пользователей классов.