Презентацию к данной лекции Вы можете скачать здесь.
Часто бывает удобно, чтобы методы, реализующие один и тот же алгоритм для различных типов данных, имели одно и то же имя. Использование нескольких методов с одним и тем же именем, но различными типами параметров называется перегрузкой методов.
Компилятор определяет, какой именно метод требуется вызвать, по типу фактических параметров. Этот процесс называется разрешением (resolution) перегрузки. Тип возвращаемого методом значения в разрешении не участвует. Механизм разрешения основан на достаточно сложном наборе правил, смысл которых сводится к тому, чтобы использовать метод с наиболее подходящими аргументами и выдать сообщение, если такой не найдется. Допустим, имеется четыре варианта метода, определяющего наибольшее значение:
// Возвращает наибольшее из двух целых: int max( int a, int b ) // Возвращает наибольшее из трех целых: int max( int a, int b, int c ) // Возвращает наибольшее из первого параметра и длины второго: int max ( int a, string b ) // Возвращает наибольшее из второго параметра и длины первого: int max ( string b, int a ) ... Console.WriteLine( max( 1, 2 ) ); Console.WriteLine( max( 1, 2, 3 ) ); Console.WriteLine( max( 1, "2" ) ); Console.WriteLine( max( "1", 2 ) );
При вызове метода max компилятор выбирает вариант метода, соответствующий типу передаваемых в метод аргументов (в приведенном примере будут последовательно вызваны все четыре варианта метода).
Если точного соответствия не найдено, выполняются
Перегруженные методы имеют одно имя, но должны различаться параметрами, точнее их типами и способами передачи ( out или ref ). Например, методы, заголовки которых приведены ниже, имеют различные сигнатуры и считаются перегруженными:
int max( int a, int b ) int max( int a, ref int b )
Перегрузка широко используется в классах библиотеки .NET. Например, в стандартном классе Console метод WriteLine перегружен 19 раз для вывода величин разных типов. Пример класса с перегруженными методами приведен в конце этой лекции (листинг 7.3).
Рекурсивным называется метод, который вызывает сам себя. Такая рекурсия называется прямой. Существует еще косвенная рекурсия, когда два или более метода вызывают друг друга. Если метод вызывает себя, в стеке создается копия значений его параметров, как и при вызове обычного метода, после чего управление передается первому исполняемому оператору метода. При повторном вызове этот процесс повторяется.
Ясно, что для завершения вычислений каждый рекурсивный метод должен содержать хотя бы одну нерекурсивную ветвь алгоритма, заканчивающуюся оператором возврата. При завершении метода соответствующая часть стека освобождается, и управление передается вызывающему методу, выполнение которого продолжается с точки, следующей за рекурсивным вызовом.
Классическим примером рекурсивной функции является функция вычисления факториала (это не означает, что факториал следует вычислять именно так). Для того чтобы получить значение факториала числа n, требуется умножить на n факториал числа (n – 1 ). Известно также, что 0!= 1 и 1 != 1.
long fact( long n ) {
if ( n == 0 || n == 1 ) return 1; // нерекурсивная ветвь
return ( n * fact( n – 1 ) ); // рекурсивная ветвь
}
То же самое можно записать короче:
long fact( long n ) {
return ( n > 1 ) ? n * fact( n – 1 ) : 1;
}
К достоинствам рекурсии можно отнести компактность записи, к недостаткам — расход времени и памяти на повторные вызовы метода и передачу ему копий параметров, а главное, опасность переполнения стека.
Иногда бывает удобно создать метод, в который можно передавать разное количество аргументов. Язык C# предоставляет такую возможность с помощью ключевого слова params. Параметр, помеченный этим ключевым словом, размещается в списке параметров последним и обозначает массив заданного типа неопределенной длины, например:
public int Calculate( int a, out int c, params int[] d ) …
В этот метод можно передать три и более параметров. Внутри метода к параметрам, начиная с третьего, обращаются как к обычным элементам массива. Количество элементов массива получают с помощью его свойства Length. В качестве примера рассмотрим метод вычисления среднего значения элементов массива (листинг 7.1).
using System;
namespace ConsoleApplication1
{ class Class1
{
public static double Average( params int[] a )
{
if ( a.Length == 0 )
throw new Exception( "Недостаточно аргументов в методе" );
double av = 0;
foreach ( int elem in a ) av += elem;
return av / a.Length;
}
static void Main()
{ try
{
int[] a = { 10, 20, 30 };
Console.WriteLine( Average( a ) ); // 1
int[] b = { -11, -4, 12, 14, 32, -1, 28 };
Console.WriteLine( Average( b ) ); // 2
short z = 1, e = 12;
byte v = 100;
Console.WriteLine( Average( z, e, v ) ); // 3
Console.WriteLine( Average() ); // 4
}
catch( Exception e )
{
Console.WriteLine( e.Message );
return;
}
}
}
}
Результат работы программы:
10 20 40 Недостаточно аргументов в методе
Параметр-массив может быть только один и должен располагаться последним в списке. Соответствующие ему аргументы должны иметь типы, для которых возможно неявное преобразование к типу массива.
Метод, которому передается управление после запуска программы, должен иметь имя Мain и быть статическим. Он может принимать параметры из внешнего окружения и возвращать значение в вызвавшую среду. Предусматривается два варианта метода — с параметрами и без параметров:
// без параметров:
static тип Main() { ... }
static void Main() { ... }
// с параметрами:
static тип Main( string[] args ) { /* ... */ }
static void Main( string[] args ) { /* ... */ }
Параметры, разделяемые пробелами, задаются при запуске программы из командной строки после имени исполняемого файла программы. Они передаются в массив args.
Если метод возвращает значение, оно должно быть целого типа, если не возвращает, он должен описываться как void. Ненулевое значение обычно означает аварийное завершение. Возвращаемое значение анализируется в командном файле, из которого запускается программа. Обычно это делается для того, чтобы можно было принять решение, выполнять ли командный файл дальше. В листинге 7.2 приведен пример метода Main, который выводит свои аргументы и ожидает нажатия любой клавиши.
using System;
namespace ConsoleApplication1
{ class Class1
{ static void Main( string[] args )
{
foreach( string arg in args ) Console.WriteLine( arg );
Console.Read();
}
}
}
Индексатор представляет собой разновидность свойства. Если у класса есть скрытое поле, представляющее собой массив, то с помощью индексатора можно обратиться к элементу этого массива, используя имя объекта и номер элемента массива в квадратных скобках. Иными словами, индексатор — это такой "умный" индекс для объектов.
Синтаксис индексатора аналогичен синтаксису свойства:
атрибуты спецификаторы тип this [ список_параметров ]
{
get код_доступа
set код_доступа
}
Индексаторы чаще всего объявляются со спецификатором public, поскольку они входят в интерфейс объекта. Атрибуты и спецификаторы могут отсутствовать.
Код доступа представляет собой блоки операторов, которые выполняются при получении ( get ) или установке значения ( set ) элемента массива. Может отсутствовать либо часть get, либо set, но не обе одновременно. Если отсутствует часть set, индексатор доступен только для чтения (read-only), если отсутствует часть get, индексатор доступен только для записи (write-only).
Список параметров содержит одно или несколько описаний индексов, по которым выполняется доступ к элементу. Чаще всего используется один индекс целого типа.
Индексаторы в основном применяются для создания специализированных массивов, на работу с которыми накладываются какие-либо ограничения. Примеры работы с индексаторами приведены в учебнике.
Язык C# допускает использование многомерных индексаторов. Они описываются аналогично обычным и применяются в основном для контроля за занесением данных в многомерные массивы и выборке данных из многомерных массивов, оформленных в виде классов.
Класс — это полноценный тип данных, поэтому программист может определить для него собственные операции. Вы уже сталкивались с перегрузкой (т.е. + " для арифметических типов означает сложение, а для строк — склеивание. Операции перегружают в основном для классов, с помощью которых задают какие-либо математические понятия, т.е. тогда, когда знаки операции имеют общепринятую семантику. Можно задать операцию сложения для класса Monster, описанного в листинге 5.8, но будет ли понятен ее смысл пользователям этого класса? Примеры перегрузки приведены именно для этого класса только для того, чтобы не тратить время на изучение нового класса.
Можно определять в классе следующие унарные операции:
+ - ! ~ ++ -- true false
Синтаксис объявителя унарной операции:
тип operator унарная_операция ( параметр )
Примеры заголовков унарных операций:
public static int operator ++( MyObject m ) public static MyObject operator --( MyObject m ) public static bool operator true( MyObject m )
Параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция должна возвращать:
~ величину любого типа;++ и -- величину типа класса, для которого она определяется;true и false величину типа bool.Операции не должны изменять значение передаваемого им операнда. Операция, возвращающая величину типа класса, для которого она определяется, должна создать новый объект этого класса, выполнить с ним необходимые действия и передать его в качестве результата.
Примечание
Префиксный и постфиксный инкремент не различаются (для них может существовать только одна реализация, которая вызывается в обоих случаях).
Пример
class Monster {
public static Monster operator ++( Monster m )
{
Monster temp = new Monster();
temp.health = m.health + 1;
return temp;
}
…
}
…
Monster vasia = new Monster();
++vasia; vasia++;
Можно определять в классе следующие бинарные операции:
+ - * / % | ^ << >> == != > < >= <=
Синтаксис объявителя бинарной операции:
тип operator бинарная_операция (параметр1, параметр2)
Примеры заголовков бинарных операций:
public static MyObject operator + ( MyObject m1, MyObject m2 ) public static bool operator == ( MyObject m1, MyObject m2 )
Хотя бы один параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция может возвращать величину любого типа.
Операции == и !=, > и <, >= и <= определяются только парами и обычно возвращают логическое значение. Чаще всего в классе определяют операции сравнения на равенство и неравенство для того, чтобы обеспечить сравнение объектов, а не их ссылок, как определено по умолчанию для ссылочных типов.
Сложные операции присваивания (например, += ) определять не требуется, да это и невозможно. При выполнении такой операции автоматически вызываются сначала операция сложения, а потом присваивания.
Пример описания бинарных операций для класса Monster (сложение с константой):
class Monster {
public static Monster operator +( Monster m, int k )
{ Monster temp = new Monster();
temp.ammo = m.ammo + k;
return temp;
}
public static Monster operator +( int k, Monster m )
{ Monster temp = new Monster();
temp.ammo = m.ammo + k;
return temp;
}
…
}
…
Monster vasia = new Monster();
Monster masha = vasia + 10;
Monster petya = 5 + masha;
…
Операции преобразования типа обеспечивают возможность явного и неявного преобразования между пользовательскими типами данных. Синтаксис объявителя операции преобразования типа:
implicit operator тип ( параметр ) // неявное преобразование explicit operator тип ( параметр ) // явное преобразование
Эти операции выполняют преобразование из типа параметра в тип, указанный в заголовке операции. Одним из этих типов должен быть класс, для которого определяется операция. Таким образом, операции выполняют преобразование либо типа класса к другому типу, либо наоборот. Преобразуемые типы не должны быть связаны отношениями наследования. Примеры операций преобразования типа для класса Monster, описанного ранее:
public static implicit operator int( Monster m )
{
return m.health;
}
public static explicit operator Monster( int h )
{
return new Monster( h, 100, "FromInt" );
}
Ниже приведены примеры использования этих преобразований в программе. Не надо искать в них смысл, они просто иллюстрируют синтаксис:
Monster Masha = new Monster( 200, 200, "Masha" ); int i = Masha; // неявное преобразование Masha = (Monster) 500; // явное преобразование
Неявное преобразование выполняется автоматически:
Явное преобразование выполняется при использовании операции приведения типа.
Все операции класса должны иметь разные сигнатуры. В отличие от других видов методов, для операций преобразования тип возвращаемого значения включается в сигнатуру, иначе нельзя было бы определять варианты преобразования данного типа в несколько других. Ключевые слова implicit и explicit в сигнатуру не включаются, следовательно, для одного и того же преобразования нельзя определить одновременно явную и неявную версию.
Неявное преобразование следует определять так, чтобы при его выполнении не возникала потеря точности и не генерировались исключения. Если эти ситуации возможны, преобразование следует описать как явное.
Рассмотрим решение следующей задачи.
В текстовом файле хранится база отдела кадров предприятия. На предприятии 100 сотрудников. Каждая строка файла содержит запись об одном сотруднике. Формат записи: фамилия и инициалы (30 поз., фамилия должна начинаться с первой позиции), год рождения (5 поз.), оклад (10 поз.). Написать программу, которая по заданной фамилии выводит на экран сведения о сотруднике, подсчитывая средний оклад всех запрошенных сотрудников.
Для решения этой задачи логично создать класс "сотрудник" и организовать из экземпляров этого класса массив. При описании класса полезно задаться следующим вопросом: какие обязанности должны быть возложены на этот класс? Очевидно, что первая обязанность — хранение сведений о сотруднике. Чтобы воспользоваться этими сведениями, клиент (то есть код, использующий класс) должен иметь возможность получить эти сведения, изменить их и вывести на экран. Кроме этого, для поиска сотрудника желательно иметь возможность сравнивать его имя с заданным.
Поля класса сделаем в соответствии с принципом инкапсуляции закрытыми, а доступ к ним организуем с помощью свойств. Это позволит контролировать процесс занесения данных в поля. Для идентификации попытки ввода неверных данных воспользуемся механизмом исключений. Чтобы сделать класс более универсальным, введем в него возможность увеличивать и уменьшать оклад сотрудника с помощью перегруженных операций класса.
На этом примере мы формализуем общий порядок создания программы, которого полезно придерживаться при решении даже простейших задач
I. Исходные данные, результаты и промежуточные величины.
Исходные данные. База сотрудников находится в текстовом файле. Прежде всего надо решить, хранить ли в оперативной памяти одновременно всю информацию из файла или можно обойтись буфером на одну строку. Если бы сведения о сотруднике запрашивались однократно, можно было бы остановиться на втором варианте, но поскольку поиск по базе будет выполняться более одного раза, всю информацию желательно хранить в оперативной памяти, поскольку многократное чтение из файла крайне нерационально.
Максимальное количество строк файла по условию задачи ограничено, поэтому можно выделить для их хранения массив из 100 элементов. Каждый элемент массива будет содержать сведения об одном сотруднике, организованные в виде класса.
Примечание
Строго говоря, для решения этой конкретной задачи запись о сотруднике может быть просто строкой, из которой при необходимости выделяется подстрока с окладом, преобразуемая затем в число, но мы для общности и удобства дальнейшей модификации программы и демонстрации синтаксиса будем использовать класс.
В программу по условию требуется также вводить фамилии искомых сотрудников. Очередную вводимую фамилию будем также хранить в стандартной строке типа string.
Результаты. В результате работы программы требуется вывести на экран требуемые элементы исходного массива. Поскольку эти результаты представляют собой выборку из исходных данных, дополнительная память для них не отводится. Кроме того, необходимо подсчитать средний оклад для найденных сотрудников. Для этого необходима переменная вещественного типа.
Промежуточные величины. Для поиска среднего оклада необходимо подсчитать количество сотрудников, для которых выводились сведения. Заведем для этого переменную целого типа.
II. Алгоритм решения задачи очевиден:
Необходимо решить, каким образом будет производиться выход из
using System;
using System.IO;
namespace ConsoleApplication1
{
class Man { // 1
const int l_name = 30;
string name;
int birth_year;
double pay;
public Man() // конструктор по умолчанию
{
name = "Anonymous";
birth_year = 0;
pay = 0;
}
public Man(string s) // 2
{
name = s.Substring(0, l_name);
birth_year = Convert.ToInt32(s.Substring(l_name, 4));
pay = Convert.ToDouble(s.Substring(l_name + 4));
if (birth_year < 0) throw new FormatException();
if (pay < 0) throw new FormatException();
}
public override string ToString() // 3
{
return string.Format( "Name: {0,30} birth: {1} pay: {2:F2}",
name, birth_year, pay);
}
public int Compare(string name) // сравнение фамилии
{
return (string.Compare(this.name, 0,
name + " ", 0, name.Length + 1,
StringComparison.OrdinalIgnoreCase));
}
// -------------------- свойства класса --------------------------
public string Name
{
get { return name; }
set { name = value; }
}
public int Birth_year
{
get { return birth_year; }
set { if (value > 0) birth_year = value;
else throw new FormatException(); }
}
public double Pay
{
get { return pay; }
set { if (value > 0) pay = value;
else throw new FormatException(); }
}
// ------------------ операции класса ------------------------------
public static double operator +(Man man1, double a)
{
man1.pay += a;
return man1.pay;
}
public static double operator +(double a, Man man1)
{
man1.pay += a;
return man1.pay;
}
public static double operator -(Man man1, double a)
{
man1.pay -= a;
if ( man1.pay < 0 ) throw new FormatException();
return man1.pay;
}
};
class Class1
{
static void Main()
{
Man[] dbase = new Man[100];
int n = 0;
try
{
StreamReader f = new StreamReader("dbase.txt"); // 4
string s;
int i = 0;
while ((s = f.ReadLine()) != null) // 5
{
dbase[i] = new Man(s);
Console.WriteLine(dbase[i]);
++i;
}
n = i - 1;
f.Close();
}
catch (FileNotFoundException e)
{
Console.WriteLine(e.Message);
Console.WriteLine(" Проверьте правильность имени файла!");
return;
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Слишком большой файл!");
return;
}
catch (FormatException)
{
Console.WriteLine("Недопустимая дата рождения или оклад");
return;
}
catch (Exception e)
{
Console.WriteLine("Ошибка: " + e.Message);
return;
}
int n_man = 0;
double mean_pay = 0;
Console.WriteLine("Введите фамилию сотрудника");
string name;
while ((name = Console.ReadLine()) != "") // 6
{
bool not_found = true;
for ( int k = 0; k <= n; ++k )
{
Man man = dbase[k];
if (man.Compare(name) == 0)
{
Console.WriteLine(man);
++n_man; mean_pay += man.Pay;
not_found = false;
}
}
if (not_found) Console.WriteLine("Такого сотрудника нет");
Console.WriteLine(
"Введите фамилию сотрудника или Enter для окончания");
}
if (n_man > 0)
Console.WriteLine("Средний оклад: {0:F2}", mean_pay / n_man);
}
}
}
В операторе 1 описан класс Man для хранения информации об одном сотруднике. Кроме полей данных, в ней задан конструктор, выполняющий заполнение полей (оператор 2), переопределен метод преобразования в строку (оператор 3), что позволяет выводить экземпляр объекта на экран с помощью метода WriteLine класса Console, а также описан метод Compare, в котором фамилия сотрудника сравнивается с заданной через параметр.
Сравнение фамилии выполняется с помощью одного из вариантов метода Compare класса string, который позволяет сравнивать подстроки без учета регистра. После заданной фамилии добавлен пробел, поскольку если пробела нет, то искомая фамилия является частью другой, и эта строка нам не подходит.
Свойства и операции класса позволяют выполнять ввод и корректировку оклада и года рождения.
Входной файл следует создать до первого запуска программы в соответствии с форматом, заданным в условии задачи. Можно использовать любой текстовый редактор, поддерживающий кодировку Unicode (например, Блокнот). Файл для целей тестирования должен состоять из нескольких строк, причем необходимо предусмотреть случай, когда одна фамилия является частью другой (например, Иванов и Ивановский). Не забудьте проверить, выдается ли диагностическое сообщение, если файл не найден.
В операторе 4 описан экземпляр стандартного класса символьного потока. Цикл 5 выполняет построчное считывание из файла в строку s и заполнение очередного элемента массива dbase. В операторе 6 организуется цикл просмотра массива. Просматриваются только заполненные при вводе элементы.
В C# существует специальный вид метода, называемый деструктором. Он вызывается сборщиком мусора непосредственно перед удалением объекта из памяти. В деструкторе описываются действия, гарантирующие корректность последующего удаления объекта, например, проверяется, все ли ресурсы, используемые объектом, освобождены (файлы закрыты,
Синтаксис деструктора:
[ атрибуты ] [ extern ] ~имя_класса() тело
Как видно из определения, деструктор не имеет параметров, не возвращает значения и не требует указания спецификаторов доступа. Его имя совпадает с именем класса и предваряется тильдой ( $$\tilde{}$$ ), символизирующей обратные по отношению к конструктору действия. Тело деструктора представляет собой блок или просто точку с запятой, если деструктор определен как внешний ( extern ).
Сборщик мусора удаляет объекты, на которые нет ссылок. Он работает в соответствии со своей внутренней стратегией в неизвестные для программиста моменты времени. Поскольку деструктор вызывается сборщиком мусора, невозможно гарантировать, что деструктор будет обязательно вызван в процессе работы программы. Следовательно, его лучше использовать только для гарантии освобождения ресурсов, а "штатное" освобождение выполнять в другом месте программы.
В классе можно определять типы данных, внутренние по отношению к классу. Так определяются вспомогательные типы, которые используются только содержащим их классом. Механизм вложенных типов позволяет скрыть ненужные детали и более полно реализовать принцип инкапсуляции. Непосредственный доступ извне к такому классу невозможен (имеется в виду доступ по имени без уточнения). Для вложенных типов можно использовать те же спецификаторы, что и для полей класса.
Например, введем в наш класс Monster вспомогательный класс Gun. Объекты этого класса без "хозяина" бесполезны, поэтому его можно определить как внутренний:
using System;
namespace ConsoleApplication1
{ class Monster
{
class Gun
{
...
}
...
}
}
Помимо классов, вложенными могут быть и другие типы данных: интерфейсы, структуры и перечисления. Мы рассмотрим их позже.
Теоретический материал: глава 7.
Каждый разрабатываемый класс должен, как правило, содержать следующие элементы: скрытые поля, конструкторы с параметрами и без параметров, методы; свойства, индексаторы; перегруженные операции. Функциональные элементы класса должны обеспечивать непротиворечивый, полный, минимальный и удобный интерфейс класса. При возникновении ошибок должны выбрасываться исключения.
Описать класс для работы с одномерным массивом целых чисел (вектором). Обеспечить следующие возможности:
Написать программу, демонстрирующую все разработанные элементы класса.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.