В данной главе возобновляется рассмотрение классов и методов. Оно начинается с пояснения механизма управления доступом к членам класса. А затем об суждаются такие вопросы, как передача и возврат объек тов, перегрузка методов, различные формы метода Main(), рекурсия и применение ключевого слова static.
Поддержка свойства инкапсуляции в классе дает два главных преимущества. Во-первых, класс связывает данные с кодом. Это преимущество использовалось в предыдущих примерах программ, начиная с главы 6. И во-вторых, класс предоставляет средства для управления доступом к его чле нам. Именно эта, вторая преимущественная особенность и будет рассмотрена ниже.
В языке С#, по существу, имеются два типа членов клас са: открытые и закрытые, хотя в действительности дело об стоит немного сложнее. Доступ к открытому члену свобод но осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рас сматривавшихся до сих пор примерах программ. А закры тый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организует ся управление доступом.
Ограничение доступа к членам класса является осно вополагающим этапом объектно-ориентированного про граммирования, поскольку позволяет исключить невер ное использование объекта. Разрешая доступ к закрытым данным только с помощью строго определенного ряда методов, можно предупредить присваивание неверных значений этим данным, выполняя, например, проверку диа пазона представления чисел. Для закрытого члена класса нельзя задать значение непо средственно в коде за пределами класса. Но в то же время можно полностью управлять тем, как и когда данные используются в объекте. Следовательно, правильно реализо ванный класс образует некий "черный ящик", которым можно пользоваться, но вну тренний механизм его действия закрыт для вмешательства извне.
Управление доступом в языке C# организуется с помощью четырех модификаторов доступа: public, private, protected и internal. В этой главе основное внимание уделяется модификаторам доступа public и private. Модификатор protected при меняется только в тех случаях, которые связаны с наследованием, и поэтому речь о нем пойдет в главе 11. А модификатор internal служит в основном для сборки, которая в широком смысле означает в C# разворачиваемую программу или библиотеку, и поэ тому данный модификатор подробнее рассматривается в главе 16.
Когда член класса обозначается спецификатором public, он становится доступ ным из любого другого кода в программе, включая и методы, определенные в других классах. Когда же член класса обозначается спецификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену (private) данного класса. Как пояснялось в главе 6, если ни один из спецификаторов доступа не указан, член класса считается закры тым для своего класса по умолчанию. Поэтому при создании закрытых членов класса спецификатор private указывать для них необязательно.
Спецификатор доступа указывается перед остальной частью описания типа отдель ного члена. Это означает, что именно с него должен начинаться оператор объявления члена класса. Ниже приведены соответствующие примеры.
public string errMsg;
private double bal;
private bool isError(byte status) { // ...
Для того чтобы стали более понятными отличия между модификаторами public и private, рассмотрим следующий пример программы.
// Отличия между видами доступа public и private к членам класса.
using System;
class MyClass {
private int alpha; // закрытый доступ, указываемый явно
int beta; // закрытый доступ по умолчанию
public int gamma; // открытый доступ
// Методы, которым доступны члены alpha и beta данного класса.
// Член класса может иметь доступ к закрытому члену этого же класса.
public void SetAlpha(int а) {
alpha = а;
}
public int GetAlpha() {
return alpha;
}
public void SetBeta(int a) {
beta = a;
}
public int GetBeta() {
return beta;
}
}
class AccessDemo {
static void Main() {
MyClass ob = new MyClass();
// Доступ к членам alpha и beta данного класса
// разрешен только посредством его методов.
ob.SetAlpha(-99);
ob.SetBeta(19);
Console.WriteLine("ob.alpha равно " + ob.GetAlpha());
Console.WriteLine("ob.beta равно " + ob.GetBeta ());
// Следующие виды доступа к членам alpha и beta
// данного класса не разрешаются.
// ob.alpha = 10; // Ошибка! alpha - закрытый член!
// ob.beta =9; // Ошибка! beta - закрытый член!
// Член gamma данного класса доступен непосредственно,
// поскольку он является открытым.
ob.gamma = 99;
}
}
Как видите, в классе MyClass член alpha указан явно как private, член beta ста новится private по умолчанию, а член gamma указан как public. Таким образом, члены alpha и beta недоступны непосредственно из кода за пределами данного клас са, поскольку они являются закрытыми. В частности, ими нельзя пользоваться непо средственно в классе AccessDemo. Они доступны только с помощью таких открытых (public) методов, как SetAlpha() и GetAlpha(). Так, если удалить символы коммен тария в начале следующей строки кода:
// ob.alpha = 10; // Ошибка! alpha - закрытый член!
то приведенная выше программа не будет скомпилирована из-за нарушения правил доступа. Но несмотря на то, что член alpha недоступен непосредственно за преде лами класса MyClass, свободный доступ к нему организуется с помощью методов, определенных в классе MyClass, как наглядно показывают методы SetAlpha() и GetAlpha(). Это же относится и к члену beta.
Из всего сказанного выше можно сделать следующий важный вывод: закрытый член может свободно использоваться другими членами этого же класса, но недоступен для кода за пределами своего класса.
Правильная организация закрытого и открытого доступа — залог успеха в объектно- ориентированном программировании. И хотя для этого не существует твердо уста новленных правил, ниже перечислен ряд общих принципов, которые могут служить в качестве руководства к действию.
- Члены, используемые только в классе, должны быть закрытыми.
- Данные экземпляра, не выходящие за определенные пределы значений, должны быть закрытыми, а при организации доступа к ним с помощью открытых методов следует выполнять проверку диапазона представления чисел.
- Если изменение члена приводит к последствиям, распространяющимся за пределы области действия самого члена, т.е. оказывает влияние на другие аспекты объекта, то этот член должен быть закрытым, а доступ к нему — контролируемым.
- Члены, способные нанести вред объекту, если они используются неправильно, должны быть закрытыми. Доступ к этим членам следует организовать с помощью открытых методов, исключающих неправильное их использование.
- Методы, получающие и устанавливающие значения закрытых данных, должны быть открытыми.
- Переменные экземпляра допускается делать открытыми лишь в том случае, если нет никаких оснований для того, чтобы они были закрытыми.
Разумеется, существует немало ситуаций, на которые приведенные выше прин ципы не распространяются, а в особых случаях один или несколько этих принципов могут вообще нарушаться. Но в целом, следуя этим правилам, вы сможете создавать объекты, устойчивые к попыткам неправильного их использования.
Для чтобы стали понятнее особенности внутреннего механизма управления до ступом, обратимся к конкретному примеру. Одним из самых характерных примеров объектно-ориентированного программирования служит класс, реализующий стек — структуру данных, воплощающую магазинный список, действующий по принципу "первым пришел — последним обслужен". Свое название он получил по аналогии со стопкой тарелок, стоящих на столе. Первая тарелка в стопке является в то же время последней использовавшейся тарелкой.
Стек служит классическим примером объектно-ориентированного программиро вания потому, что он сочетает в себе средства хранения информации с методами досту па к ней. Для реализации такого сочетания отлично подходит класс, в котором члены, обеспечивающие хранение информации в стеке, должны быть закрытыми, а методы доступа к ним — открытыми. Благодаря инкапсуляции базовых средств хранения ин формации соблюдается определенный порядок доступа к отдельным элементам стека из кода, в котором он используется.
Для стека определены две основные операции: поместить данные в стек и извлечь их оттуда. Первая операция помещает значение на вершину стека, а вторая — извле кает значение из вершины стека. Следовательно, операция извлечения является без возвратной: как только значение извлекается из стека, оно удаляется и уже недоступно в стеке.
В рассматриваемом здесь примере создается класс Stack, реализующий функции стека. В качестве базовых средств для хранения данных в стеке служит закрытый мас сив. А операции размещения и извлечения данных из стека доступны с помощью от крытых методов класса Stack. Таким образом, открытые методы действуют по упо мянутому выше принципу "последним пришел — первым обслужен". Как следует из приведенного ниже кода, в классе Stack сохраняются символы, но тот же самый меха низм может быть использован и для хранения данных любого другого типа.
// Класс для хранения символов в стеке.
using System;
class Stack {
// Эти члены класса являются закрытыми.
char[] stck; // массив, содержащий стек
int tos; // индекс вершины стека
// Построить пустой класс Stack для реализации стека заданного размера.
public Stack(int size) {
stck = new char[size]; // распределить память для стека
tos = 0;
}
// Поместить символы в стек.
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 более подробно. В начале этого класса объявляются две следующие переменные экземпляра.
// Эти члены класса являются закрытыми.
char[] stck; // массив, содержащий стек
int tos; // индекс вершины стека
Массив stck предоставляет базовые средства для хранения данных в стеке (в дан ном случае — символов). Обратите внимание на то, что память для этого массива не распределяется. Это делается в конструкторе класса Stack. А член tos данного класса содержит индекс вершины стека.
Оба члена, tos и stck, являются закрытыми, и благодаря этому соблюдается прин цип "последним пришел — первым обслужен". Если же разрешить открытый доступ к члену stck, то элементы стека окажутся доступными не по порядку. Кроме того, член tos содержит индекс вершины стека, где находится первый обслуживаемый в стеке элемент, и поэтому манипулирование членом tos в коде, находящемся за преде лами класса Stack, следует исключить, чтобы не допустить разрушение самого стека. Но в то же время члены stck и tos доступны пользователю класса Stack косвенным образом с помощью различных отрытых методов, описываемых ниже.
Рассмотрим далее конструктор класса Stack.
// Построить пустой класс Stack для реализации стека заданного размера.
public Stack(int size) {
stck = new char[size]; // распределить память для стека
tos = 0;
}
Этому конструктору передается требуемый размер стека. Он распределяет память для базового массива и устанавливает значение переменной tos в нуль. Следователь но, нулевое значение переменной tos указывает на то, что стек пуст.
Открытый метод Push() помещает конкретный элемент в стек, как показано ниже.
// Поместить символы в стек.
public void Push(char ch) {
if (tos==stck.Length) {
Console.WriteLine(" - Стек заполнен.");
return;
}
stck[tos] = ch;
tos++;
}
Элемент, помещаемый в стек, передается данному методу в качестве параметра ch. Перед тем как поместить элемент в стек, выполняется проверка на наличие свободного места в базовом массиве, а именно: не превышает ли значение переменной tos длину массива stck. Если свободное место в массиве stck есть, то элемент сохраняется в нем по индексу, хранящемуся в переменной tos, после чего значение этой переменной инкрементируется. Таким образом, в переменной tos всегда хранится индекс следую щего свободного элемента массива stck.
Для извлечения элемента из стека вызывается открытый метод Pop(), приведен ный ниже.
// Извлечь символ из стека.
public char Pop() {
if(tos==0) {
Console.WriteLine(" - Стек пуст.");
return (char) 0;
}
tos--;
return stck[tos];
}
В этом методе сначала проверяется значение переменной tos. Если оно равно нулю, значит, стек пуст. В противном случае значение переменной tos декрементиру ется, и затем из стека возвращается элемент по указанному индексу.
Несмотря на то что для реализации стека достаточно методов Push() и Pop(), по лезными могут оказаться и другие методы. Поэтому в классе Stack определены еще четыре метода: IsFull(), IsEmpty(), Capacity() и GetNum(). Эти методы предо ставляют всю необходимую информацию о состоянии стека и приведены ниже.
// Возвратить значение 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;
}
Метод IsFull() возвращает логическое значение true, если стек заполнен, а ина че — логическое значение false. Метод IsEmpty() возвращает логическое значение true, если стек пуст, а иначе — логическое значение false. Для получения общей ем кости стека (т.е. общего числа элементов, которые могут в нем храниться) достаточно вызвать метод Capacity(), а для получения количества элементов, хранящихся в на стоящий момент в стеке, — метод GetNum(). Польза этих методов состоит в том, что для получения информации, которую они предоставляют, требуется доступ к закры той переменной tos. Кроме того, они служат наглядными примерами организации безопасного доступа к закрытым членам класса с помощью открытых методов.
Конкретное применение класса Stack для реализации стека демонстрируется в приведенной ниже программе.
// Продемонстрировать применение класса Stack.
using System;
// Класс для хранения символов в стеке.
class Stack {
// Эти члены класса являются закрытыми.
char[] stck; // массив, содержащий стек
int tos; // индекс вершины стека
// Построить пустой класс Stack для реализации стека заданного размера.
public Stack (int size) {
stck = new char[size]; // распределить память для стека
tos = 0;
}
// Поместить символы в стек.
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;
}
}
class StackDemo {
static void Main() {
Stack stk1 = new Stack(10);
Stack stk2 = new Stack(10);
Stack stk3 = new Stack(10);
char ch;
int i;
// Поместить ряд символов в стек stk1.
Console.WriteLine("Поместить символы А-J в стек stk1.");
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
if(stk1.IsFull()) Console.WriteLine("Стек stk1 заполнен.");
// Вывести содержимое стека stk1.
Console.Write("Содержимое стека stk1: ");
while( !stk1.IsEmpty() ) {
ch = stk1.Pop();
Console.Write(ch);
}
Console.WriteLine();
if(stk1.IsEmpty()) Console.WriteLine("Стек stk1 пуст.\n");
// Поместить дополнительные символы в стек stk1.
Console.WriteLine("Вновь поместить символы A-J в стек stk1.");
for(i=0; !stk1.IsFull(); i++)
stk1.Push((char) ('A' + i));
// А теперь извлечь элементы из стека stk1 и поместить их в стек stk2.
// В итоге элементы сохраняются в стеке stk2 в обратном порядке.
Console.WriteLine("А теперь извлечь символы из стека stk1\n" +
"и поместить их в стек stk2.");
while( !stk1.IsEmpty() ) {
ch = stk1.Pop();
stk2.Push(ch);
}
Console.Write("Содержимое стека stk2: ");
while( !stk2.IsEmpty() ) {
ch = stk2.Pop();
Console.Write(ch);
}
Console.WriteLine("\n");
// Поместить 5 символов в стек.
Console.WriteLine("Поместить 5 символов в стек stk3.");
for(i=0; i < 5; i++)
stk3.Push((char) ('A' + i));
Console.WriteLine("Емкость стека stk3: " + stk3.Capacity());
Console.WriteLine("Количество объектов в стеке stk3: " +
stk3.GetNum());
}
}
При выполнении этой программы получается следующий результат.
Поместить символы А-J в стек stk1. Стек stk1 заполнен. Содержимое стека stk1: JIHGFEDCBA Стек stk1 пуст. Вновь поместить символы А-J в стек stk1. А теперь извлечь символы из стека stk1 и поместить их в стек stk2. Содержимое стека stk2: ABCDEFGHIJ Поместить 5 символов в стек stk3. Емкость стека stk3: 10 Количество объектов в стеке stk3: 5
## Передача объектов методам по ссылке
В приведенных до сих пор примерах программ при указании параметров, пере
даваемых методам, использовались типы значений, например int или double. Но в
методах можно также использовать параметры ссылочного типа, что не только пра
вильно, но и весьма распространено в ООП. Подобным образом объекты могут пере
даваться методам по ссылке. В качестве примера рассмотрим следующую программу.
// Пример передачи объектов методам по ссылке. using System;
class MyClass { int alpha, beta; public MyClass(int i, int j) { alpha = i; beta = j; } // Возвратить значение true, если параметр ob // имеет те же значения, что и вызывающий объект. public bool SameAs(MyClass ob) { if((ob.alpha == alpha) & (ob.beta == beta)) return true; else return false; } // Сделать копию объекта ob. public void Copy(MyClass ob) { alpha = ob.alpha; beta = ob.beta; } public void Show() { Console.WriteLine("alpha: (0), beta: (1}", alpha, beta); } }
class PassOb { static void Main() { MyClass ob1 = new MyClass(4, 5); MyClass ob2 = new MyClass(6, 7); Console.Write("ob1: "); ob1.Show(); Console.Write("ob2: "); ob2.Show(); if(ob1.SameAs(ob2)) Console.WriteLine("ob1 и ob2 имеют одинаковые значения."); else Console.WriteLine("ob1 и ob2 имеют разные значения."); Console.WriteLine(); // А теперь сделать объект ob1 копией объекта ob2. ob1.Copy(ob2); Console.Write("оЫ после копирования: "); ob1.Show(); if(ob1.SameAs(ob2)) Console.WriteLine("ob1 и ob2 имеют одинаковые значения."); else Console.WriteLine("ob1 и ob2 имеют разные значения."); } }
Выполнение этой программы дает следующий результат.
ob1: alpha: 4, beta: 5 ob2: alpha: 6, beta: 7 ob1 и ob2 имеют разные значения. ob1 после копирования: alpha: 6, beta: 7 ob1 и оb2 имеют одинаковые значения.
Каждый из методов SameAs() и Сору() в приведенной выше программе получа
ет ссылку на объект типа MyClass в качестве аргумента. Метод SameAs() сравнивает
значения переменных экземпляра alpha и beta в вызывающем объекте со значени
ями аналогичных переменных в объекте, передаваемом посредством параметра ob.
Данный метод возвращает логическое значение true только в том случае, если оба
объекта имеют одинаковые значения этих переменных экземпляра. А метод Сору()
присваивает значения переменных alpha и beta из объекта, передаваемого по ссылке
посредством параметра ob, переменным alpha и beta из вызывающего объекта. Как
показывает данный пример, с точки зрения синтаксиса объекты передаются методам
по ссылке таким же образом, как и значения обычных типов.
### Способы передачи аргументов методу
Как показывает приведенный выше пример, передача объекта методу по ссылке
делается достаточно просто. Но в этом примере показаны не все нюансы данного про
цесса. В некоторых случаях последствия передачи объекта по ссылке будут отличаться
от тех результатов, к которым приводит передача значения обычного типа. Для выяс
нения причин этих отличий рассмотрим два способа передачи аргументов методу.
Первым способом является вызов по значению. В этом случае значение аргумента
копируется в формальный параметр метода. Следовательно, изменения, вносимые в
параметр метода, не оказывают никакого влияния на аргумент, используемый для вы
зова. А вторым способом передачи аргумента является вызов по ссылке. В данном случае
параметру метода передается ссылка на аргумент, а не значение аргумента. В методе
эта ссылка используется для доступа к конкретному аргументу, указываемому при вы
зове. Это означает, что изменения, вносимые в параметр, будут оказывать влияние на
аргумент, используемый для вызова метода.
По умолчанию в C# используется вызов по значению, а это означает, что копия ар
гумента создается и затем передается принимающему параметру. Следовательно, при
передаче значения обычного типа, например int или double, все, что происходит с
параметром, принимающим аргумент, не оказывает никакого влияния за пределами
метода. В качестве примера рассмотрим следующую программу.
// Передача аргументов обычных типов по значению. using System; class Test { /* Этот метод не оказывает никакого влияния на аргументы, используемые для его вызова. */ public void NoChange(int i, int j) { i = i + j; j = -j; } }
class CallByValue { static void Main() { Test ob = new Test(); int a = 15, b = 20; Console.WriteLine("а и b до вызова: " + a + " " + b); ob.NoChange(a, b); Console.WriteLine("а и b после вызова: " + a + " " + b); } }
Вот какой результат дает выполнение этой программы.
а и b до вызова: 15 20 а и b после вызова: 15 20
Как видите, операции, выполняемые в методе NoChange(), не оказывают никакого
влияния на значения аргументов а и b, используемых для вызова данного метода. Это
опять же объясняется тем, что параметрам i и j переданы копии значений аргументов
а и b, а сами аргументы а и b совершенно не зависят от параметров i и j. В частности,
присваивание параметру i нового значения не будет оказывать никакого влияния на
аргумент а.
Дело несколько усложняется при передаче методу ссылки на объект. В этом случае
сама ссылка по-прежнему передается по значению. Следовательно, создается копия
ссылки, а изменения, вносимые в параметр, не оказывают никакого влияния на аргу
мент. (Так, если организовать ссылку параметра на новый объект, то это изменение не
повлечет за собой никаких последствий для объекта, на который ссылается аргумент.)
Но главное отличие вызова по ссылке заключается в том, что изменения, происходя
щие с объектом, на который ссылается параметр, окажут влияние на тот объект, на
который ссылается аргумент. Попытаемся выяснить причины подобного влияния.
Напомним, что при создании переменной типа класса формируется только ссыл
ка на объект. Поэтому при передаче этой ссылки методу принимающий ее параметр
будет ссылаться на тот же самый объект, на который ссылается аргумент. Это означает,
что и аргумент, и параметр ссылаются на один и тот же объект и что объекты, по суще
ству, передаются методам по ссылке. Таким образом, объект в методе будет оказывать
влияние на объект, используемый в качестве аргумента. Для примера рассмотрим сле
дующую программу.
// Передача объектов по ссылке. using System;
class Test { public int a, b; public Test(int i, int j) { a = i; b = j; } /* Передать объект. Теперь переменные ob.а и ob.b из объекта, используемого в вызове метода, будут изменены. */ public void Change(Test ob) { ob.a = ob.a + ob.b; ob.b = -ob.b; } }
class CallByRef { static void Main() { Test ob = new Test(15, 20); Console.WriteLine("ob.а и ob.b до вызова: " + ob.a + " " + ob.b); ob.Change(ob); Console.WriteLine("ob.а и ob.b после вызова: " + ob.a + " " + ob.b); } }
Выполнение этой программы дает следующий результат.
ob.a и ob.b до вызова: 15 20 ob.a и ob.b после вызова: 35 -20
Как видите, действия в методе Change() оказали в данном случае влияние на
объект, использовавшийся в качестве аргумента.
Итак, подведем краткий итог. Когда объект передается методу по ссылке, сама ссыл
ка передается по значению, а следовательно, создается копия этой ссылки. Но эта ко
пия будет по-прежнему ссылаться на тот же самый объект, что и соответствующий ар
гумент. Это означает, что объекты передаются методам неявным образом по ссылке.
## Использование модификаторов параметров ref и out
Как пояснялось выше, аргументы простых типов, например int или char, переда
ются методу по значению. Это означает, что изменения, вносимые в параметр, прини
мающий значение, не будут оказывать никакого влияния на аргумент, используемый
для вызова. Но такое поведение можно изменить, используя ключевые слова ref и out
для передачи значений обычных типов по ссылке. Это позволяет изменить в самом
методе аргумент, указываемый при его вызове.
Прежде чем переходить к особенностям использования ключевых слов ref и out,
полезно уяснить причины, по которым значение простого типа иногда требуется пе
редавать по ссылке. В общем, для этого существуют две причины: разрешить методу
изменить содержимое его аргументов или же возвратить несколько значений. Рассмо
трим каждую из этих причин более подробно.
Нередко требуется, чтобы метод оперировал теми аргументами, которые ему пере
даются. Характерным тому примером служит метод Swap(), осуществляющий пере
становку значений своих аргументов. Но поскольку аргументы простых типов пере
даются по значению, то, используя выбираемый в C# по умолчанию механизм вызова
по значению для передачи аргумента параметру, невозможно написать метод, меняю
щий местами значения двух его аргументов, например типа int. Это затруднение раз
решает модификатор ref.
Как вам должно быть уже известно, значение возвращается из метода вызывающей
части программы с помощью оператора return. Но метод может одновременно воз
вратить лишь одно значение. А что, если из метода требуется возвратить два или более
фрагментов информации, например, целую и дробную части числового значения с
плавающей точкой? Такой метод можно написать, используя модификатор out.
### Использование модификатора параметра ref
Модификатор параметра ref принудительно организует вызов по ссылке, а не по
значению. Этот модификатор указывается как при объявлении, так и при вызове мето
да. Для начала рассмотрим простой пример. В приведенной ниже программе создает
ся метод Sqr(), возвращающий вместо своего аргумента квадрат его целочисленного
значения. Обратите особое внимание на применение и местоположение модификато
ра ref.
// Использовать модификатор ref для передачи значения обычного типа по ссылке. using System;
class RefTest { // Этот метод изменяет свой аргумент. Обратите // внимание на применение модификатора ref. public void Sqr(ref int i) { i = i * i; } }
class RefDemo { static void Main() { RefTest ob = new RefTest(); int a = 10; Console.WriteLine("а до вызова: " + a); ob.Sqr(ref a); // обратите внимание на применение модификатора ref Console.WriteLine("а после вызова: " + а); } }
Как видите, модификатор ref указывается перед объявлением параметра в самом
методе и перед аргументом при вызове метода. Ниже приведен результат выполнения
данной программы, который подтверждает, что значение аргумента а действительно
было изменено с помощью метода Sqr().
а до вызова: 10 а после вызова: 100
Теперь, используя модификатор ref, можно написать метод, переставляющий ме
стами значения двух своих аргументов простого типа. В качестве примера ниже при
ведена программа, в которой метод Swap() выполняет перестановку значений двух
своих целочисленных аргументов, когда он вызывается.
// Поменять местами два значения. using System;
class ValueSwap { // Этот метод меняет местами свои аргументы. public void Swap(ref int a, ref int b) { int t; t = a; a = b; b = t; } }
class ValueSwapDemo { static void Main() { ValueSwap ob = new ValueSwap(); int x = 10, у = 20; Console.WriteLine("x и у до вызова: " + х + " " + у); ob.Swap(ref х, ref у); Console.WriteLine("х и у после вызова: " + х + " " + у); } }
Вот к какому результату приводит выполнение этой программы.
х и у до вызова: 10 20 х и у после вызова: 20 10
В отношении модификатора ref необходимо иметь в виду следующее. Аргументу,
передаваемому по ссылке с помощью этого модификатора, должно быть присвоено
значение до вызова метода. Дело в том, что в методе, получающем такой аргумент в
качестве параметра, предполагается, что параметр ссылается на действительное зна
чение. Следовательно, при использовании модификатора ref в методе нельзя задать
первоначальное значение аргумента.
### Использование модификатора параметра out
Иногда ссылочный параметр требуется использовать для получения значения из
метода, а не для передачи ему значения. Допустим, что имеется метод, выполняющий
некоторую функцию, например, открытие сетевого сокета и возврат кода успешно
го или неудачного завершения данной операции в качестве ссылочного параметра.
В этом случае методу не передается никакой информации, но в то же время он должен
возвратить определенную информацию. Главная трудность при этом состоит в том,
что параметр типа ref должен быть инициализирован определенным значением до
вызова метода. Следовательно, чтобы воспользоваться параметром типа ref, придется
задать для аргумента фиктивное значение и тем самым преодолеть данное ограниче
ние. Правда, в C# имеется более подходящий вариант выхода из подобного затрудне
ния — воспользоваться модификатором параметра out.
Модификатор параметра out подобен модификатору ref, за одним исключени
ем: он служит только для передачи значения за пределы метода. Поэтому перемен
ной, используемой в качестве параметра out, не нужно (да и бесполезно) присваи
вать какое-то значение. Более того, в методе параметр out считается неинициализи
рованным, т.е. предполагается, что у него отсутствует первоначальное значение. Это
означает, что значение должно быть присвоено данному параметру в методе до его
завершения. Следовательно, после вызова метода параметр out будет содержать не
которое значение.
Ниже приведен пример применения модификатора параметра out. В этом приме
ре программы для разделения числа с плавающей точкой на целую и дробную части
используется метод GetParts() из класса Decompose. Обратите внимание на то, как
возвращается каждая часть исходного числа.
// Использовать модификатор параметра out. using System;
class Decompose { /* Разделить числовое значение с плавающей точкой на целую и дробную части. */ public int GetParts(double n, out double frac) { int whole; whole = (int) n; frac = n - whole; // передать дробную часть числа через параметр frac return whole; // возвратить целую часть числа } }
class UseOut { static void Main() { Decompose ob = new Decompose(); int i; double f; i = ob.GetParts(10.125, out f); Console.WriteLine("Целая часть числа равна " + i); Console.WriteLine("Дробная часть числа равна " + f); } }
Выполнение этой программы дает следующий результат.
Целая часть числа равна 10 Дробная часть числа равна 0.125
Метод GetParts() возвращает два фрагмента информации. Во-первых, целую
часть исходного числового значения переменной n обычным образом с помощью опе
ратора return. И во-вторых, дробную часть этого значения посредством параметра
frас типа out. Как показывает данный пример, используя модификатор параметра
out, можно организовать возврат двух значений из одного и того же метода.
Разумеется, никаких ограничений на применение параметров out в одном методе
не существует. С их помощью из метода можно возвратить сколько угодно фрагментов
информации. Рассмотрим пример применения двух параметров out. В этом примере
программы метод HasComFactor() выполняет две функции. Во-первых, он определя
ет общий множитель (кроме 1) для двух целых чисел, возвращая логическое значение
true, если у них имеется общий множитель, а иначе — логическое значение false.
И во-вторых, он возвращает посредством параметров типа out наименьший и наи
больший общий множитель двух чисел, если таковые обнаруживаются.
// Использовать два параметра типа out. using System;
class Num { /* Определить, имеется ли у числовых значений переменных х и v общий множитель. Если имеется, то возвратить наименьший и наибольший множители посредством параметров типа out. */ public bool HasComFactor (int x, int y, out int least, out int greatest) { int i; int max = x < у ? x : y; bool first = true; least = 1; greatest = 1; // Найти наименьший и наибольший общий множитель. for(i=2; i <= max/2 + 1; i++) { if( ((y%i)==0) & ((x%i)==0) ) { if(first) { least = i; first = false; } greatest = i; } } if(least != 1) return true; else return false; } }
class DemoOut { static void Main() { Num ob = new Num(); int lcf, gcf; if(ob.HasComFactor(231, 105, out lcf, out gcf)) { Console.WriteLine("Наименьший общий множитель " + "чисел 231 и 105 равен " + lcf); Console.WriteLine("Наибольший общий множитель " + "чисел 231 и 105 равен " + gcf); } else Console.WriteLine("Общий множитель у чисел 35 и 49 отсутствует.");
if(ob.HasComFactor(35, 51, out lcf, out gcf)) {
Console.WriteLine("Наименьший общий множитель " +
"чисел 35 и 51 равен " + lcf);
Console.WriteLine("Наибольший общий множитель " +
"чисел 35 и 51 равен " + gcf);
}
else
Console.WriteLine("Общий множитель у чисел 35 и 51 отсутствует.");
}
} Обратите внимание на то, что значения присваиваются переменным lcf и gcf в методе Main() до вызова метода HasComFactor(). Если бы параметры метода HasComFactor() были типа ref, а не out, это привело бы к ошибке. Данный метод возвращает логическое значение true или false, в зависимости от того, имеется ли общий множитель у двух целых чисел. Если он имеется, то посредством параметров типа out возвращаются наименьший и наибольший общий множитель этих чисел. Ниже приведен результат выполнения данной программы.
Наименьший общий множитель чисел 231 и 105 равен 3
Наибольший общий множитель чисел 231 и 105 равен 21
Общий множитель у чисел 35 и 51 отсутствует.
Применение модификаторов ref и out не ограничивается только передачей значе ний обычных типов. С их помощью можно также передавать ссылки на объекты. Если модификатор ref или out указывает на ссылку, то сама ссылка передается по ссылке. Это позволяет изменить в методе объект, на который указывает ссылка. Рассмотрим в качестве примера следующую программу, в которой ссылочные параметры типа ref служат для смены объектов, на которые указывают ссылки.
// Поменять местами две ссылки.
using System;
class RefSwap {
int a, b;
public RefSwap(int i, int j) {
a = i;
b = j;
}
public void Show() {
Console.WriteLine("a: {0}, b: {l}", a, b);
}
// Этот метод изменяет свои аргументы.
public void Swap(ref RefSwap ob1, ref RefSwap ob2) {
RefSwap t;
t = ob1;
ob1 = ob2;
ob2 = t;
}
}
class RefSwapDemo {
static void Main() {
RefSwap x = new RefSwap(1, 2);
RefSwap у = new RefSwap(3, 4);
Console.Write("x до вызова: ");
x.Show();
Console.Write("у до вызова: ");
у.Show();
Console.WriteLine();
// Смена объектов, на которые ссылаются аргументы х и у.
х.Swap(ref х, ref у);
Console.Write("х после вызова: ");
х.Show();
Console.Write("у после вызова: ");
у.Show();
}
}
При выполнении этой программы получается следующий результат.
х до вызова: а: 1, b: 2
у до вызова: а: 3, b: 4
х после вызова: а: 3, b: 4
у после вызова: а: 1, b: 2
В данном примере в методе Swap() выполняется смена объектов, на которые ссы лаются два его аргумента. До вызова метода Swap() аргумент х ссылается на объект, содержащий значения 1 и 2, тогда как аргумент у ссылается на объект, содержащий значения 3 и 4. А после вызова метода Swap() аргумент х ссылается на объект, содер жащий значения 3 и 4, тогда как аргумент у ссылается на объект, содержащий значе ния 1 и 2. Если бы не параметры типа ref, то перестановка в методе Swap() не имела бы никаких последствий за пределами этого метода. Для того чтобы убедиться в этом, исключите параметры типа ref из метода Swap().
При создании метода обычно заранее известно число аргументов, которые будут переданы ему, но так бывает не всегда. Иногда возникает потребность создать метод, которому можно было бы передать произвольное число аргументов. Допустим, что требуется метод, обнаруживающий наименьшее среди ряда значений. Такому методу можно было бы передать не менее двух, трех, четырех или еще больше значений. Но в любом случае метод должен возвратить наименьшее из этих значений. Такой метод нельзя создать, используя обычные параметры. Вместо этого придется воспользовать ся специальным типом параметра, обозначающим произвольное число параметров. И это делается с помощью создаваемого параметра типа params.
Для объявления массива параметров, способного принимать от нуля до нескольких аргументов, служит модификатор params. Число элементов массива параметров бу дет равно числу аргументов, передаваемых методу. А для получения аргументов в про грамме организуется доступ к данному массиву.
Ниже приведен пример программы, в которой модификатор params использует ся для создания метода MinVal(), возвращающего наименьшее среди ряда заданных значений.
// Продемонстрировать применение модификатора params.
using System;
class Min {
public int MinVal(params int[] nums) {
int m;
if(nums.Length == 0) {
Console.WriteLine("Ошибка: нет аргументов.");
return 0;
}
m = nums[0];
for(int i=1; i < nums.Length; i++)
if(nums[i] < m) m = nums[i];
return m;
}
}
class ParamsDemo {
static void Main() {
Min ob = new Min();
int min;
int a = 10, b = 20;
// Вызвать метод с двумя значениями.
min = ob.MinVal(a, b);
Console.WriteLine("Наименьшее значение равно " + min);
// Вызвать метод с тремя значениями.
min = ob.MinVal(a, b, -1);
Console.WriteLine("Наименьшее значение равно " + min);
// Вызвать метод с пятью значениями.
min = ob.MinVal(18, 23, 3, 14, 25);
Console.WriteLine("Наименьшее значение равно " + min);
// Вызвать метод с массивом целых значений.
int[] args = { 45, 67, 34, 9, 112, 8 };
min = ob.MinVal(args);
Console.WriteLine("Наименьшее значение равно " + min);
}
}
При выполнении этой программы получается следующий результат.
Наименьшее значение равно 10
Наименьшее значение равно -1
Наименьшее значение равно 3
Наименьшее значение равно 8
Всякий раз, когда вызывается метод MinVal(), ему передаются аргументы в мас сиве nums. Длина этого массива равна числу передаваемых аргументов. Поэтому с по мощью метода MinVal() можно обнаружить наименьшее среди любого числа зна чений.
Обратите внимание на последний вызов метода MinVal(). Вместо отдельных значе ний в данном случае передается массив, содержащий ряд значений. И такая передача аргументов вполне допустима. Когда создается параметр типа params, он восприни мает список аргументов переменной длины или же массив, содержащий аргументы. Несмотря на то что параметру типа params может быть передано любое число аргументов, все они должны иметь тип массива, указываемый этим параметром.
Например, вызов метода MinVal()
min = ob.MinVal(l, 2.2); // Неверно!
считается недопустимым, поскольку нельзя автоматически преобразовать тип double (значение 2.2) в тип int, указанный для массива nums в методе MinVal(). Пользоваться модификатором params следует осторожно, соблюдая граничные условия, так как параметр типа params может принимать любое число аргументов — даже нулевое! Например, вызов метода MinVal() в приведенном ниже фрагменте кода считается правильным с точки зрения синтаксиса С#.
min = ob.MinVal(); // нет аргументов
min = ob.MinVal(3); // 1 аргумент
Именно поэтому в методе MinVal() организована проверка на наличие в масси ве nums хотя бы одного элемента перед тем, как пытаться получить доступ к этому элементу. Если бы такой проверки не было, то при вызове метода MinVal() без аргу ментов возникла бы исключительная ситуация во время выполнения. (Подробнее об исключительных ситуациях речь пойдет в главе 13.) Больше того, код метода MinVal() написан таким образом, чтобы его можно было вызывать с одним аргументом. В этом случае возвращается этот единственный аргумент.
У метода могут быть как обычные параметры, так и параметр переменной дли ны. В качестве примера ниже приведена программа, в которой метод ShowArgs() принимает один параметр типа string, а также целочисленный массив в качестве параметра типа params.
// Использовать обычный параметр вместе с параметром
// переменной длины типа params.
using System;
class MyClass {
public void ShowArgs(string msg, params int[] nums) {
Console.Write(msg + ");
foreach(int i in nums)
Console.Write(i + " ");
Console.WriteLine();
}
}
class ParamsDemo2 {
static void Main() {
MyClass ob = new MyClass();
ob.ShowArgs("Это ряд целых чисел",
1, 2, 3, 4, 5);
ob.ShowArgs("А это еще два целых числа ",
17, 20);
}
}
Вот какой результат дает выполнение этой программы.
Это ряд целых чисел: 1, 2, 3, 4, 5
А это еще два целых числа: 17, 20
В тех случаях, когда у метода имеются обычные параметры, а также параметр пере менной длины типа params, он должен быть указан последним в списке параметров данного метода. Но в любом случае параметр типа params должен быть единственным.
Метод может возвратить данные любого типа, в том числе и тип класса. Ниже в ка честве примера приведен вариант класса Rect, содержащий метод Enlarge(), в ко тором строится прямоугольник с теми же сторонами, что и у вызывающего объекта прямоугольника, но пропорционально увеличенными на указанный коэффициент.
// Возвратить объект из метода.
using System;
class Rect {
int width;
int height;
public Rect(int w, int h) {
width = w;
height = h;
}
public int Area() {
return width * height;
}
public void Show() {
Console.WriteLine(width + " " + height);
}
/* Метод возвращает прямоугольник со сторонами, пропорционально
увеличенными на указанный коэффициент по сравнению с вызывающим
объектом прямоугольника. */
public Rect Enlarge(int factor) {
return new Rect(width * factor, height * factor);
}
}
class RetObj {
static void Main() {
Rect r1 = new Rect(4, 5);
Console.Write("Размеры прямоугольника r1: ");
r1.Show();
Console.WriteLine("Площадь прямоугольника r1: " + rl.Area(1);
Console.WriteLine();
// Создать прямоугольник в два раза больший прямоугольника rl.
Rect r2 = r1.Enlarge(2);
Console.Write("Размеры прямоугольника r2: ");
r2.Show();
Console.WriteLine("Площадь прямоугольника r2: " + r2.Агеа());
}
}
Выполнение этой программы дает следующий результат.
Размеры прямоугольника r1: 4 5
Площадь прямоугольника r1: 20
Размеры прямоугольника r2: 8 10
Площадь прямоугольника r2: 80
Когда метод возвращает объект, последний продолжает существовать до тех пор, пока не останется ссылок на него. После этого он подлежит сборке как "мусор". Сле довательно, объект не уничтожается только потому, что завершается создавший его метод.
Одним из практических примеров применения возвращаемых данных типа объ ектов служит фабрика класса, которая представляет собой метод, предназначенный для построения объектов его же класса. В ряде случаев предоставлять пользователям класса доступ к его конструктору нежелательно из соображений безопасности или же потому, что построение объекта зависит от некоторых внешних факторов. В подобных случаях для построения объектов используется фабрика класса. Обратимся к просто му примеру.
// Использовать фабрику класса.
using System;
class MyClass {
int a, b; // закрытые члены класса
// Создать фабрику для класса MyClass.
public MyClass Factory(int i, int j) {
MyClass t = new MyClass();
t.a = i;
t.b = j;
return t; // возвратить объект
}
public void Show() {
Console.WriteLine("а и b: " + a + " " + b);
}
}
class MakeObjects {
static void Main() {
MyClass ob = new MyClass();
int i, j;
// Сформировать объекты, используя фабрику класса.
for(i=0, j=10; i < 10; i++, j--){
MyClass anotherOb = ob.Factory(i, j); // создать объект
anotherOb.Show();
}
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этого кода.
а и b: 0 10
а и b: 1 9
а и b: 2 8
а и b: 3 7
а и b: 4 6
а и b: 5 5
а и b: 6 4
а и b: 73
а и b: 8 2
а и b: 91
Рассмотрим данный пример более подробно. В этом примере конструктор для класса MyClass не определяется, и поэтому доступен только конструктор, вызывае мый по умолчанию. Это означает, что значения переменных а и b нельзя задать с по мощью конструктора. Но в фабрике класса Factory() можно создать объекты, в ко торых задаются значения переменных а и b. Более того, переменные а и b являются закрытыми, и поэтому их значения могут быть заданы только с помощью фабрики класса Factory().
В методе Main() получается экземпляр объекта класса MyClass, а его фабричный метод используется в цикле for для создания десяти других объектов. Ниже приведе на строка кода, в которой создаются эти объекты.
MyClass anotherOb = ob.Factory(i, j); // создать объект
На каждом шаге итерации цикла создается переменная ссылки на объект anotherOb, которой присваивается ссылка на объект, формируемый фабрикой клас са. По завершении каждого шага итерации цикла переменная anotherOb выходит за пределы области своего действия, а объект, на который она ссылается, утилизируется.
В C# массивы реализованы в виде объектов, а это означает, что метод может также возвратить массив. (В этом отношении C# отличается от C++, где не допускается воз врат массивов из методов.) В качестве примера ниже приведена программа, в которой метод FindFactors() возвращает массив, содержащий множители переданного ему аргумента.
// Возвратить массив из метода.
using System;
class Factor {
/* Метод возвращает массив facts, содержащий множители аргумента num.
При возврате из метода параметр numfactors типа out будет содержать
количество обнаруженных множителей. */
public int[] FindFactors(int num, out int numfactors) {
int[] facts = new int[80]; // размер массива 80 выбран произвольно
int i, j;
// Найти множители и поместить их в массив facts.
for(i=2, j=0; i < num/2 + 1; i++)
if( (num%i)==0 ) {
facts[j] = i;
j++;
}
numfactors = j;
return facts;
}
}
class FindFactors {
static void Main() {
Factor f = new Factor();
int numfastors;
int[] factors;
factors = f.FindFactors(1000, out numfactors);
Console.WriteLine("Множители числа 1000: ");
for(int i=0; i < numfactors; i++)
Console.Write(factors[i] + " ");
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
Множители числа 1000:
2 4 5 8 10 20 25 40 50 100 125 200 250 500
В классе Factor метод FindFactors() объявляется следующим образом.
public int[] FindFactors(int num, out int numfactors) {
Обратите внимание на то, как указывается возвращаемый массив типа int. Этот синтаксис можно обобщить. Всякий раз, когда метод возвращает массив, он указыва ется аналогичным образом, но с учетом его типа и размерности. Например, в следую щей строке кода объявляется метод someMeth(), возвращающий двумерный массив типа double.
public double[,] someMeth() { // ...
В 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() — абсолютное значение числа с плавающей точкой обычной (одинарной) точности.
В С перегрузка не поддерживается, и поэтому у каждой функции должно быть свое, особое имя, несмотря на то, что все упомянутые выше функции, по существу, делают одно и то же — определяют абсолютное значение. Но это принципиально усложняет положение, поскольку приходится помнить имена всех трех функций, хотя они реали зованы по одному и тому же основному принципу. Подобные затруднения в С# не воз никают, поскольку каждому методу, определяющему абсолютное значение, может быть присвоено одно и то же имя. И действительно, в состав библиотеки классов для среды .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 Когда приходится работать с перегружаемыми конструкторами, то иногда очень по лезно предоставить возможность одному конструктору вызывать другой. В С# это дает ся с помощью ключевого слова 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;
у = j;
}
}
class OverloadConsDemo {
static void Main() {
XYCoord t1 = new XYCoord();
XYCoord t2 = new XYCoord(8, 9);
XYCoord t3 = new XYCoord(t2);
Console.WriteLine("t1.x, t1.y: " + t1.x + ", " + t1.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)
t1.х, t1.у: 0, 0
t2.х, t2.у: 8, 9
t3.х, t3.у: 8, 9
Код в приведенном выше примере работает следующим образом. Единственным конструктором, фактически инициализирующим поля х и у в классе XYCoord, явля ется конструктор XYCoord(int, int). А два других конструктора просто вызывают этот конструктор с помощью ключевого слова this. Например, когда создается объект t1, то вызывается его конструктор XYCoord(), что приводит к вызову this(0, 0), который в данном случае преобразуется в вызов конструктора XYCoord(0, 0). То же самое происходит и при создании объекта t2.
Вызывать перегружаемый конструктор с помощью ключевого слова this полез но, в частности, потому, что он позволяет исключить ненужное дублирование кода. В приведенном выше примере нет никакой необходимости дублировать во всех трех конструкторах одну и ту же последовательность инициализации, и благодаря при менению ключевого слова this такое дублирование исключается. Другое преимуще ство организации подобного вызова перезагружаемого конструктора заключается в возможности создавать конструкторы с задаваемыми "по умолчанию" аргументами, когда эти аргументы не указаны явно. Ниже приведен пример создания еще одного конструктора XYCoord.
public XYCoord(int х) : this(х, х) { }
По умолчанию в этом конструкторе для координаты у автоматически устанавли вается то же значение, что и для координаты у. Конечно, пользоваться такими кон струкциями с задаваемыми "по умолчанию" аргументами следует благоразумно и осторожно, чтобы не ввести в заблуждение пользователей классов.
Инициализаторы объектов предоставляют еще один способ создания объекта и ини циализации его полей и свойств. (Подробнее о свойствах речь пойдет в главе 10.) Если используются инициализаторы объектов, то вместо обычного вызова конструктора класса указываются имена полей или свойств, инициализируемых первоначально зада ваемым значением. Следовательно, синтаксис инициализатора объекта предоставляет альтернативу явному вызову конструктора класса. Синтаксис инициализатора объекта используется главным образом при создании анонимных типов в LINQ-вьгражениях. (Подробнее об анонимных типах и LINQ-вьгражениях — в главе 19.) Но поскольку ини циализаторы объектов можно, а иногда и должно использовать в именованном классе, то ниже представлены основные положения об инициализации объектов.
Обратимся сначала к простому примеру.
// Простой пример, демонстрирующий применение инициализаторов объектов.
using System;
class MyClass {
public int Count;
public string Str;
}
class ObjInitDemo {
static void Main() {
// Сконструировать объект типа MyClass, используя инициализаторы
объектов.
MyClass obj = new MyClass { Count = 100, Str = "Тестирование" };
Console.WriteLine(obj.Count + " " + obj.Str);
}
}
Выполнение этого кода дает следующий результат.
100 Тестирование
Как показывает результат выполнения приведенного выше кода, переменная экзем
пляра obj.Count инициализирована значением 100, а переменная экземпляра obj.
Str — символьной строкой "Тестирование". Но обратите внимание на то, что в клас
се MyClass отсутствуют явно определяемые конструкторы и не используется обычный
синтаксис конструкторов. Вместо этого объект obj класса MyClass создается с помо
щью следующей строки кода.
MyClass obj = new MyClass { Count = 100, Str = "Тестирование" };
В этой строке кода имена полей указываются явно вместе с их первоначальными
значениями. Это приводит к тому, что сначала конструируется экземпляр объекта
типа MyClass (с помощью неявно вызываемого по умолчанию конструктора), а затем
задаются первоначальные значения переменных Count и Str данного экземпляра.
Следует особо подчеркнуть, что порядок указания инициализаторов особого зна
чения не имеет. Например, объект obj можно было бы инициализировать и так, как
показано ниже.
MyClass obj = new MyClass { Str = "Тестирование", Count = 100 };
В этой строке кода инициализация переменной экземпляра Str предшествует
инициализации переменной экземпляра Count, а в приведенном выше коде все про
исходило наоборот. Но в любом случае результат получается одинаковым.
Ниже приведена общая форма синтаксиса инициализации объектов:
new имя_класса {имя = выражение, имя = выражение, ...}
где имя обозначает имя поля или свойства, т.е. доступного члена класса, на который
указывает имя_класса. А выражение обозначает инициализирующее выражение, тип
которого, конечно, должен соответствовать типу поля или свойства.
Инициализаторы объектов обычно не используются в именованных классах, как, на
пример, в представленном выше классе MyClass, хотя это вполне допустимо. Вообще,
при обращении с именованными классами используется синтаксис вызова обычного
конструктора. И, как упоминалось выше, инициализаторы объектов применяются в
основном в анонимных типах, формируемых в LINQ-выражениях.
## Необязательные аргументы
В версии C# 4.0 внедрено новое средство, повышающее удобство указания аргу
ментов при вызове метода. Это средство называется необязательными аргументами и
позволяет определить используемое по умолчанию значение для параметра метода.
Данное значение будет использоваться по умолчанию в том случае, если для параме
тра не указан соответствующий аргумент при вызове метода. Следовательно, указывать
аргумент для такого параметра не обязательно. Необязательные аргументы позволяют
упростить вызов методов, где к некоторым параметрам применяются аргументы, вы
бираемые по умолчанию. Их можно также использовать в качестве "сокращенной"
формы перегрузки методов.
Применение необязательного аргумента разрешается при создании необязательного
параметра. Для этого достаточно указать используемое по умолчанию значение па
раметра с помощью синтаксиса, аналогичного инициализации переменной. Исполь
зуемое по умолчанию значение должно быть константным выражением. В качестве
примера рассмотрим следующее определение метода.
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
В этой строке кода объявляются два необязательных параметра: beta и gamma, при
чем параметру beta по умолчанию присваивается значение 10, а параметру gamma —
значение 20. Эти значения используются по умолчанию, если для данных параметров
не указываются аргументы при вызове метода. Следует также иметь в виду, что пара
метр alpha не является необязательным. Напротив, это обычный параметр, для кото
рого всегда нужно указывать аргумент.
Принимая во внимание приведенное выше объявление метода OptArgMeth(),
последний можно вызвать следующими способами.
// Передать все аргументы явным образом. OptArgMeth(1, 2, 3); // Сделать аргумент gamma необязательным. OptArgMeth(1, 2); // Сделать оба аргумента beta и gamma необязательными. OptArgMeth(1);
При первом вызове параметру alpha передается значение 1, параметру beta —
значение 2, а параметру gamma — значение 3. Таким образом, все три аргумента за
даются явным образом, а значения, устанавливаемые по умолчанию, не используются.
При втором вызове параметру alpha передается значение 1, а параметру beta — зна
чение 2, но параметру gamma присваивается устанавливаемое по умолчанию значение
20. И наконец, при третьем вызове упомянутого выше метода параметру alpha пере
дается значение 1, а параметрам beta и gamma присваиваются устанавливаемые по
умолчанию значения. Следует, однако, иметь в виду, что параметр beta не получит
устанавливаемое по умолчанию значение, если то же самое не произойдет с параме
тром gamma. Если первый аргумент устанавливается по умолчанию, то и все остальные
аргументы должны быть установлены по умолчанию.
Весь описанный выше процесс демонстрируется в приведенном ниже примере
программы.
// Продемонстрировать необязательные аргументы. using System; class OptionArgDemo { static void OptArgMeth(int alpha, int beta=10, int gamma = 20) { Console.WriteLine("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma); } static void Main() { // Передать все аргументы явным образом. OptArgMeth(1,2,3); // Сделать аргумент gamma необязательным. OptArgMeth(1, 2); // Сделать оба аргумента beta и gamma необязательными. OptArgMeth(1); } }
Результат выполнения данной программы лишь подтверждает применение ис
пользуемых по умолчанию аргументов.
Это аргументы alpha, beta и gamma: 1 2 3 Это аргументы alpha, beta и gamma: 1 2 20 Это аргументы alpha, beta и gamma: 1 10 20
Как следует из приведенного выше результата, если аргумент не указан, то исполь
зуется его значение, устанавливаемое по умолчанию.
Следует иметь в виду, что все необязательные аргументы должны непременно ука
зываться справа от обязательных. Например, следующее объявление оказывается не
действительным.
int Sample(string name = "пользователь", int userid) { // Ошибка!
Для исправления ошибки в этом объявлении необходимо указать аргумент userId
до аргумента name. Раз уж вы начали объявлять необязательные аргументы, то ука
зывать после них обязательные аргументы нельзя. Например, следующее объявление
также оказывается неверным.
int Sample(int accountId, string name = "пользователь", int userId) { // Ошибка!
Аргумент name объявляется как необязательный, и поэтому аргумент userId сле
дует указать до аргумента name (или же сделать его также необязательным).
Помимо методов, необязательные аргументы можно применять в конструкторах, ин
дексаторах и делегатах. (Об индексаторах и делегатах речь пойдет далее в этой книге.)
Преимущество необязательных аргументов заключается, в частности, в том, что
они упрощают программирующему обращение со сложными вызовами методов и
конструкторов. Ведь нередко в методе приходится задавать больше параметров, чем
обычно требуется. И в подобных случаях некоторые из этих параметров могут быть
сделаны необязательными благодаря аккуратному применению необязательных ар
гументов. Это означает, что передавать нужно лишь те аргументы, которые важны в
данном конкретном случае, а не все аргументы, которые в противном случае должны
быть обязательными. Такой подход позволяет рационализировать метод и упростить
программирующему обращение с ним.
### Необязательные аргументы и перегрузка методов
В некоторых случаях необязательные аргументы могут стать альтернативой пере
грузке методов. Для того чтобы стало понятнее, почему это возможно, обратимся еще
раз к примеру метода OptArgMeth(). До появления в C# необязательных аргументов
нам пришлось бы создать три разных варианта метода OptArgMeth(), чтобы добиться
таких же функциональных возможностей, как и у рассмотренного выше варианта это
го метода. Все эти варианты пришлось бы объявить следующим образом.
static void OptArgMeth(int alpha) static void OptArgMeth(int alpha, int beta) static void OptArgMeth(int alpha, int beta, int gamma)
Эти перегружаемые варианты метода OptArgMeth() позволяют вызывать его с од
ним, двумя или тремя аргументами. (Если значения параметров beta и gamma не пере
даются, то они предоставляются в теле перегружаемых вариантов данного метода.) Без
условно, в такой реализации функциональных возможностей метода OptArgMeth()
с помощью перегрузки нет ничего дурного. Но в данном случае целесообразнее все же
воспользоваться необязательными аргументами, хотя такой подход не всегда оказыва
ется более совершенным, чем перегрузка метода.
### Необязательные аргументы и неоднозначность
При использовании необязательных аргументов может возникнуть такое затрудне
ние, как неоднозначность. Нечто подобное может произойти при перегрузке метода
с необязательными параметрами. В некоторых случаях компилятор может оказать
ся не в состоянии определить, какой именно вариант метода следует вызывать, когда
необязательные аргументы не заданы. В качестве примера рассмотрим два следующих
варианта метода OptArgMeth().
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) { Console.WriteLine("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma); } static void OptArgMeth(int alpha, double beta=10.0, double gamma = 20.0) { Console.WriteLine("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma); }
Обратите внимание на то, что единственное отличие в обоих вариантах рассматри
ваемого здесь метода состоит в типах параметров beta и gamma, которые оказываются
необязательными. В первом варианте оба параметра относятся к типу int, а во вто
ром — к типу double. С учетом этих вариантов перегрузки метода OptArgMeth()
следующий его вызов приводит к неоднозначности.
OptArgMeth(1); // Ошибка из-за неоднозначности!
Этот вызов приводит к неоднозначности потому, что компилятору неизвестно, ка
кой именно вариант данного метода использовать: тот, где параметры beta и gamma
имеют тип int, или же тот, где они имеют тип double. Но самое главное, что конкрет
ный вызов метода OptArgMeth() может привести к неоднозначности, даже если она
и не присуща его перегрузке.
В связи с тем что перегрузка методов, допускающих применение необязательных
аргументов, может привести к неоднозначности, очень важно принимать во внимание
последствия такой перегрузки. В некоторых случаях, возможно, придется отказаться
от применения необязательных аргументов, чтобы исключить неоднозначность и тем
самым предотвратить использование метода непреднамеренным образом.
### Практический пример использования необязательных аргументов
Для того чтобы показать на практике, насколько необязательные аргументы упро
щают вызовы некоторых типов методов, рассмотрим следующий пример программы.
В этой программе объявляется метод Display(), выводящий на экран символьную
строку полностью или частично.
// Использовать необязательный аргумент, чтобы упростить вызов метода. using System;
class UseOptArgs { // Вывести на экран символьную строку полностью или частично. static void Display(string str, int start = 0, int stop = -1) { if (stop < 0) stop = str.Length; // Проверить условие выхода за заданные пределы. if(stop > str.Length | start > stop | start < 0) return; for(int i=start; i < stop; i++) Console.Write(str[i]); Console.WriteLine(); } static void Main() { Display("это простой тест"); Display("это простой тест", 12); Display("это простой тест", 4, 14); } }
Выполнение этой программы дает следующий результат.
это простой тест тест простой те
Внимательно проанализируем метод Display(). Выводимая на экран символьная
строка передается в первом аргументе данного метода. Это обязательный аргумент, а
два других аргумента — необязательные. Они задают начальный и конечный индексы
для вывода части символьной строки. Если параметру stop не передается значение, то
по умолчанию он принимает значение -1, указывающее на то, что конечной точкой вы
вода служит конец символьной строки. Если же параметру start не передается значе
ние, то по умолчанию он принимает значение 0. Следовательно, в отсутствие одного из
необязательных аргументов символьная строка выводится на экран полностью. В про
тивном случае она выводится на экран частично. Эго означает, что если вызвать метод
Display() с одним аргументом (т.е. с выводимой строкой), то символьная строка будет
выведена на экран полностью. Если же вызвать метод Display() с двумя аргументами,
то на экран будут выведены символы, начиная с позиции, определяемой аргументом
start, и до самого конца строки. А если вызвать метод Display() с тремя аргумента
ми, то на экран будут выведены символы из строки, начиная с позиции, определяемой
аргументом start, и заканчивая позицией, определяемой аргументом stop.
Несмотря на всю простоту данного примера, он, тем не менее, демонстрирует зна
чительное преимущество, которое дают необязательные аргументы. Это преимуще
ство заключается в том, что при вызове метода можно указывать только те аргументы,
которые требуются. А передавать явным образом устанавливаемые по умолчанию зна
чения не нужно.
Прежде чем переходить к следующей теме, остановимся на следующем важном мо
менте. Необязательные аргументы оказываются весьма эффективным средством лишь
в том случае, если они используются правильно. Они предназначены для того, чтобы
метод выполнял свои функции эффективно, а пользоваться им можно было бы просто
и удобно. В этом отношении устанавливаемые по умолчанию значения всех аргумен
тов должны упрощать обычное применение метода. В противном случае необязатель
ные аргументы способны нарушить структуру кода и ввести в заблуждение тех, кто
им пользуется. И наконец, устанавливаемое по умолчанию значение необязательного
параметра не должно наносить никакого вреда. Иными словами, неумышленное ис
пользование необязательного аргумента не должно приводить к необратимым, отри
цательным последствиям. Так, если забыть указать аргумент при вызове метода, то это
не должно привести к удалению важного файла данных!
## Именованные аргументы
Еще одним средством, связанным с передачей аргументов методу, является име
нованный аргумент. Именованные аргументы были внедрены в версии C# 4.0. Как вам
должно быть уже известно, при передаче аргументов методу порядок их следования,
как правило, должен совпадать с тем порядком, в котором параметры определены в
самом методе. Иными словами, значение аргумента присваивается параметру по его
позиции в списке аргументов. Данное ограничение призваны преодолеть именованные
аргументы. Именованный аргумент позволяет указать имя того параметра, которому
присваивается его значение. И в этом случае порядок следования аргументов уже не
имеет никакого значения. Таким образом, именованные аргументы в какой-то степени
похожи на упоминавшиеся ранее инициализаторы объектов, хотя и отличаются от
них своим синтаксисом.
Для указания аргумента по имени служит следующая форма синтаксиса.
имя_параметра : значение
Здесь имя_параметра обозначает имя того параметра, которому передается зна
чение. Разумеется, имя_параметра должно обозначать имя действительного параме
тра для вызываемого метода.
Ниже приведен простой пример, демонстрирующий применение именованных
аргументов. В этом примере создается метод IsFactor(), возвращающий логическое
значение true, если первый его параметр нацело делится на второй параметр.
// Применить именованные аргументы. using System;
class NamedArgsDemo { // Выяснить, делится ли одно значение нацело на другое. static bool IsFactor(int val, int divisor) { if((val % divisor) == 0) return true; return false; } static void Main() { // Ниже демонстрируются разные способы вызова метода IsFactor(). // Вызов с использованием позиционных аргументов. if(IsFactor(10, 2)) Console.WriteLine("2 - множитель 10."); // Вызов с использованием именованных аргументов. if(IsFactor(val; 10, divisor: 2)) Console.WriteLine("2 - множитель 10."); // Для именованного аргумента порядок указания не имеет значения. if(IsFactor(divisor: 2, val: 10)) Console.WriteLine("2 - множитель 10."); // Применить как позиционный, так и именованный аргумент. if(IsFactor(10, divisor: 2)) Console.WriteLine("2 - множитель 10."); } }
Выполнение этого кода дает следующий результат.
2 - множитель 10. 2 - множитель 10. 2 - множитель 10. 2 - множитель 10.
Как видите, при каждом вызове метода IsFactor() получается один и тот же
результат.
Помимо демонстрации именованного аргумента в действии, приведенный выше
пример кода иллюстрирует две важные особенности именованных аргументов.
Во-первых, порядок следования аргументов не имеет никакого значения. Например,
два следующих вызова метода IsFactor() совершенно равнозначны.
IsFactor(val :10, divisor: 2) IsFactor(divisor: 2, val: 10)
Независимость от порядка следования является главным преимуществом имено
ванных аргументов. Это означает, что запоминать (или даже знать) порядок следо
вания параметров в вызываемом методе совсем не обязательно. Для работы с СОМ-
интерфейсами это может быть очень удобно. И во-вторых, позиционные аргументы
можно указывать вместе с именованными в одном и том же вызове, как показано в
следующем примере.
IsFactor(10, divisor: 2)
Следует, однако, иметь в виду, что при совместном использовании именованных
и позиционных аргументов все позиционные аргументы должны быть указаны перед
любыми именованными аргументами.
Именованные аргументы можно также применять вместе с необязательными аргу
ментами. Покажем это на примере вызова метода Display(), рассматривавшегося в
предыдущем разделе.
// Указать все аргументы по имени. Display(stop: 10, str: "это простой тест", start: 0); // Сделать аргумент start устанавливаемым по умолчанию. Display(stop: 10, str: "это простой тест"); // Указать строку по позиции, аргумент stop — по имени by name, // тогда как аргумент start — устанавливаемым по умолчанию Display("это простой тест", stop: 10);
Вообще говоря, комбинация именованных и необязательных аргументов позволяет
упростить вызовы сложных методов со многими параметрами.
Синтаксис именованных аргументов более многословен, чем у обычных позицион
ных аргументов, и поэтому для вызова методов чаще всего применяются позиционные
аргументы. Но в тех случаях, когда это уместно, именованные аргументы могут быть
использованы довольно эффективно.
**ПРИМЕЧАНИЕ**
Помимо методов, именованные и необязательные аргументы могут применяться
в конструкторах, индексаторах и делегатах. (Об индексаторах и делегатах речь пойдет далее
в этой книге.)
## Метод Main()
В представленных до сих пор примерах программ использовалась одна форма ме
тода Main(). Но у него имеется также целый ряд перегружаемых форм. Одни из них
могут служить для возврата значений, другие — для получения аргументов. В этом
разделе рассматриваются и те и другие формы.
#### Возврат значений из метода Main()
По завершении программы имеется возможность возвратить конкретное значение
из метода Main() вызывающему процессу (зачастую операционной системе). Для этой
цели служит следующая форма метода Main().
static int Main()
Обратите внимание на то, что в этой форме метода Main() объявляется возвращае
мый тип int вместо типа void.
Как правило, значение, возвращаемое методом Main(), указывает на нормальное
завершение программы или на аварийное ее завершение из-за сложившихся ненор
мальных условий выполнения. Условно нулевое возвращаемое значение обычно ука
зывает на нормальное завершение программы, а все остальные значения обозначают
тип возникшей ошибки.
### Передача аргументов методу Main()
Многие программы принимают так называемые аргументы командной строки,
т.е. информацию, которая указывается в командной строке непосредственно после
имени программы при ее запуске на выполнение. В программах на C# такие аргумен
ты передаются затем методу Main(). Для получения аргументов служит одна из при
веденных ниже форм метода Main().
static void Main(string[] args) static int Main(string[] args)
В первой форме метод Main() возвращает значение типа void, а во второй — це
лое значение, как пояснялось выше. Но в обеих формах аргументы командной строки
сохраняются в виде символьных строк в массиве типа string, который передается ме
тоду Main(). Длина этого массива (args) должна быть равна числу аргументов ко
мандной строки, которое может быть и нулевым.
В качестве примера ниже приведена программа, выводящая все аргументы команд
ной строки, вместе с которыми она вызывается.
// Вывести все аргументы командной строки. using System; class CLDemo { static void Main(string[] args) { Console.WriteLine("Командная строка содержит " + args.Length + " аргумента."); Console.WriteLine("Вот они: "); for(int i=0; i < args.Length; i++) Console.WriteLine(args[i]); } }
Если программа CLDemo запускается из командной строки следующим образом:
CLDemo один два три то ее выполнение дает такой результат. Командная строка содержит 3 аргумента. Вот они: один два три
Для того чтобы стало понятнее, каким образом используются аргументы командной
строки, рассмотрим еще один пример программы, в которой применяется простой
подстановочный шифр для шифровки или расшифровки сообщений. Шифруемое
или расшифровываемое сообщение указывается в командной строке. Применяемый
шифр действует довольно просто. Для шифровки слова значение каждой его буквы
инкрементируется на 1. Следовательно, Буква "А" становится буквой "Б" и т.д. А для
расшифровки слова значение каждой его буквы декрементируется на 1. Разумеется,
такой шифр не имеет никакой практической ценности, поскольку его нетрудно раз
гадать. Тем не менее он может стать приятным развлечением для детей.
// Зашифровать и расшифровать сообщение, используя // простой подстановочный шифр. using System;
class Cipher { static int Main(string[] args) { // Проверить наличие аргументов. if(args.Length < 2) { Console.WriteLine("ПРИМЕНЕНИЕ: " + "слово1: <зашифровать>/<расшифровать> " + "[слово2... словоN]"); return 1; // возвратить код неудачного завершения программы } // Если аргументы присутствуют, то первым аргументом должно быть // слово <зашифровать> или же слово <расшифровать>. if(args[0] != "зашифровать" & args[0] != "расшифровать") { Console.WriteLine("Первым аргументом должно быть слово " + "<зашифровать> или <расшифровать>."); return 1; // возвратить код неудачного завершения программы } // Зашифровать или расшифровать сообщение. for(int n=1; n < args.Length; n++) { for(int i=0; i < args[n].Length; i++) { if(args[0] == "зашифровать") Console.Write((char) (args[n][i] + 1) ); else Console.Write((char) (args[n][i] - 1) ); } Console.Write(" "); } Console.WriteLine(); return 0; } }
Для того чтобы воспользоваться этой программой, укажите в командной строке
имя программы, затем командное слово "зашифровать" или "расшифровать" и далее
сообщение, которое требуется зашифровать или расшифровать. Ниже приведены два
примера выполнения данной программы, при условии, что она называется Cipher.
С:\Cipher зашифровать один два пейо егб С:\Cipher расшифровать пейо егб один два
Данная программа отличается двумя интересными свойствами. Во-первых, обра
тите внимание на то, как в ней проверяется наличие аргументов командной строки
перед тем, как продолжить выполнение. Это очень важное свойство, которое можно
обобщить. Если в программе принимается во внимание наличие одного или более ар
гументов командной строки, то в ней должна быть непременно организована проверка
факта передачи ей предполагаемых аргументов, иначе программа будет работать не
правильно. Кроме того, в программе должна быть организована проверка самих ар
гументов перед тем, как продолжить выполнение. Так, в рассматриваемой здесь про
грамме проверяется наличие командного слова "зашифровать" или "расшифровать"
в качестве первого аргумента командной строки.
И во-вторых, обратите внимание на то, как программа возвращает код своего завер
шения. Если предполагаемые аргументы командной строки отсутствуют или указаны
неправильно, программа возвращает код 1, указывающий на ее аварийное завершение.
В противном случае возвращается код 0, когда программа завершается нормально.
## Рекурсия
В C# допускается, чтобы метод вызывал самого себя. Этот процесс называется рекур
сией, а метод, вызывающий самого себя, — рекурсивным. Вообще, рекурсия представля
ет собой процесс, в ходе которого нечто определяет самое себя. В этом отношении она
чем-то напоминает циклическое определение. Рекурсивный метод отличается главным
образом тем, что он содержит оператор, в котором этот метод вызывает самого себя.
Рекурсия является эффективным механизмом управления программой.
Классическим примером рекурсии служит вычисление факториала числа. Факто
риал числа N представляет собой произведение всех целых чисел от 1 до N. Напри
мер, факториал числа 3 равен 1×2×3, или 6. В приведенном ниже примере программы
демонстрируется рекурсивный способ вычисления факториала числа. Для сравнения в
эту программу включен также нерекурсивный вариант вычисления факториала числа.
// Простой пример рекурсии. using System;
class Factorial { // Это рекурсивный метод. public int FactR(int n) { int result; if(n==1) return 1; result = FactR(n-1) * n; return result; } // Это итерационный метод. public int FactI(int n) { int t, result; result = 1; for(t=1; t <= n; t++) result *= t; return result; } }
class Recursion { static void Main() { Factorial f = new Factorial(); Console.WriteLine("Факториалы, рассчитанные рекурсивным методом."); Console.WriteLine("Факториал числа 3 равен " + f.FactR(3)); Console.WriteLine("Факториал числа 4 равен " + f.FactR(4)); Console.WriteLine("Факториал числа 5 равен " + f.FactR(5)); Console.WriteLine();
Console.WriteLine("Факториалы, рассчитанные итерационным методом.");
Console.WriteLine("Факториал числа 3 равен " + f.FactR(3));
Console.WriteLine("Факториал числа 4 равен " + f.FactR(4));
Console.WriteLine("Факториал числа 5 равен " + f.FactR(5));
}
}
При выполнении этой программы получается следующий результат.
Факториалы, рассчитанные рекурсивным методом. Факториал числа 3 равен 6 Факториал числа 4 равен 24 Факториал числа 5 равен 120 Факториалы, рассчитанные итерационным методом. Факториал числа 3 равен 6 Факториал числа 4 равен 24 Факториал числа 5 равен 120
Принцип действия нерекурсивного метода FactI() вполне очевиден. В нем ис
пользуется цикл, в котором числа, начиная с 1, последовательно умножаются друг на
друга, постепенно образуя произведение, дающее факториал.
А рекурсивный метод FactR() действует по более сложному принципу. Если ме
тод FactR() вызывается с аргументом 1, то он возвращает значение 1. В противном
случае он возвращает произведение FactR(n-1)*n. Для вычисления этого произве
дения метод FactR() вызывается с аргументом n-1. Этот процесс повторяется до тех
пор, пока значение аргумента n не станет равным 1, после чего из предыдущих вызовов
данного метода начнут возвращаться полученные значения. Например, когда вычисля
ется факториал числа 2, то при первом вызове метода FactR() происходит второй его
вызов с аргументом 1. Из этого вызова возвращается значение 1, которое затем умно
жается на 2 (первоначальное значение аргумента n). В итоге возвращается результат 2,
равный факториалу числа 2(1×2). Было бы любопытно ввести в метод FactR() опе
раторы, содержащие вызовы метода WriteLine(), чтобы наглядно показать уровень
рекурсии при каждом вызове метода FactR(), а также вывести промежуточные ре
зультаты вычисления факториала заданного числа.
Когда метод вызывает самого себя, в системном стеке распределяется память для
новых локальных переменных и параметров, и код метода выполняется с этими новы
ми переменными и параметрами с самого начала. При рекурсивном вызове метода не
создается его новая копия, а лишь используются его новые аргументы. А при возврате
из каждого рекурсивного вызова старые локальные переменные и параметры извле
каются из стека, и выполнение возобновляется с точки вызова в методе. Рекурсивные
методы можно сравнить по принципу действия с постепенно сжимающейся и затем
распрямляющейся пружиной.
Ниже приведен еще один пример рекурсии для вывода символьной строки в об
ратном порядке. Эта строка задается в качестве аргумента рекурсивного метода
DisplayRev().
// Вывести символьную строку в обратном порядке, используя рекурсию. using System;
class RevStr { // Вывести символьную строку в обратном порядке. public void DisplayRev(string str) { if(str.Length > 0) DisplayRev(str.Substring(1, str.Length-1)); else return; Console.Write(str[0]); } }
class RevStrDemo { static void Main() { string s = "Это тест"; RevStr rsOb = new RevStr(); Console.WriteLine("Исходная строка: " + s); Console.Write("Перевернутая строка: "); rsOb.DisplayRev(s); Console.WriteLine(); } }
Вот к какому результату приводит выполнение этого кода.
Исходная строка: Это тест Перевернутая строка: тсет отЭ
Всякий раз, когда вызывается метод DisplayRev(), в нем происходит проверка
длины символьной строки, представленной аргументом str. Если длина строки не
равна нулю, то метод DisplayRev() вызывается рекурсивно с новой строкой, кото
рая меньше исходной строки на один символ. Этот процесс повторяется до тех пор,
пока данному методу не будет передана строка нулевой длины. После этого начнет
ся раскручиваться в обратном порядке механизм всех рекурсивных вызовов метода
DisplayRev(). При возврате из каждого такого вызова выводится первый символ
строки, представленной аргументом str, а в итоге вся строка выводится в обратном
порядке.
Рекурсивные варианты многих процедур могут выполняться немного медленнее,
чем их итерационные эквиваленты из-за дополнительных затрат системных ресурсов
на неоднократные вызовы метода. Если же таких вызовов окажется слишком много, то
в конечном итоге может быть переполнен системный стек. А поскольку параметры и
локальные переменные рекурсивного метода хранятся в системном стеке и при каж
дом новом вызове этого метода создается их новая копия, то в какой-то момент стек
может оказаться исчерпанным. В этом случае возникает исключительная ситуация,
и общеязыковая исполняющая среда (CLR) генерирует соответствующее исключение.
Но беспокоиться об этом придется лишь в том случае, если рекурсивная процедура
выполняется неправильно.
Главное преимущество рекурсии заключается в том, что она позволяет реализовать
некоторые алгоритмы яснее и проще, чем итерационным способом. Например, ал
горитм быстрой сортировки довольно трудно реализовать итерационным способом.
А некоторые задачи, например искусственного интеллекта, очевидно, требуют именно
рекурсивного решения.
При написании рекурсивных методов следует непременно указать в соответству
ющем месте условный оператор, например if, чтобы организовать возврат из мето
да без рекурсии. В противном случае возврата из вызванного однажды рекурсивного
метода может вообще не произойти. Подобного рода ошибка весьма характерна для
реализации рекурсии в практике программирования. В этом случае рекомендуется
пользоваться операторами, содержащими вызовы метода WriteLine(), чтобы сле
дить за происходящим в рекурсивном методе и прервать его выполнение, если в нем
обнаружится ошибка.
## Применение ключевого слова static
Иногда требуется определить такой член класса, который будет использоваться не
зависимо от всех остальных объектов этого класса. Как правило, доступ к члену клас
са организуется посредством объекта этого класса, но в то же время можно создать
член класса для самостоятельного применения без ссылки на конкретный экземпляр
объекта. Для того чтобы создать такой член класса, достаточно указать в самом начале
его объявления ключевое слово static. Если член класса объявляется как static, то
он становится доступным до создания любых объектов своего класса и без ссылки на
какой-нибудь объект. С помощью ключевого слова static можно объявлять как пере
менные, так и методы. Наиболее характерным примером члена типа static служит
метод Main(), который объявляется таковым потому, что он должен вызываться опе
рационной системой в самом начале выполняемой программы.
Для того чтобы воспользоваться членом типа static за пределами класса, доста
точно указать имя этого класса с оператором-точкой. Но создавать объект для этого не
нужно. В действительности член типа static оказывается доступным не по ссылке на
объект, а по имени своего класса. Так, если требуется присвоить значение 10 перемен
ной count типа static, являющейся членом класса Timer, то для этой цели можно
воспользоваться следующей строкой кода.
Timer.count = 10;
Эта форма записи подобна той, что используется для доступа к обычным перемен
ным экземпляра посредством объекта, но в ней указывается имя класса, а не объекта.
Аналогичным образом можно вызвать метод типа static, используя имя класса и
оператор-точку.
Переменные, объявляемые как static, по существу, являются глобальными. Ког
да же объекты объявляются в своем классе, то копия переменной типа static не
создается. Вместо этого все экземпляры класса совместно пользуются одной и той же
переменной типа static. Такая переменная инициализируется перед ее примене
нием в классе. Когда же ее инициализатор не указан явно, то она инициализируется
нулевым значением, если относится к числовому типу данных, пустым значением,
если относится к ссылочному типу, или же логическим значением false, если отно
сится к типу bool. Таким образом, переменные типа static всегда имеют какое-то
значение.
Метод типа static отличается от обычного метода тем, что его можно вызывать по
имени его класса, не создавая экземпляр объекта этого класса. Пример такого вызова
уже приводился ранее. Это был метод Sqrt() типа static, относящийся к классу
System.Math из стандартной библиотеки классов С#.
Ниже приведен пример программы, в которой объявляются переменная и метод
типа static.
// Использовать модификатор static. using System;
class StaticDemo { // Переменная типа static. public static int Val = 100; // Метод типа static. public static int ValDiv2() { return Val/2; } }
class SDemo { static void Main() { Console.WriteLine("Исходное значение переменной " + "StaticDemo.Val равно " + StaticDemo.Val); StaticDemo.Val = 8; Console.WriteLine("Текущее значение переменной" + "StaticDemo.Val равно " + StaticDemo.Val); Console.WriteLine("StaticDemo.ValDiv2(): " + StaticDemo.ValDiv2()); } }
Выполнение этой программы приводит к следующему результату.
Исходное значение переменной StaticDemo.Val равно 100 Текущее значение переменной StaticDemo.Val равно 8 StaticDemo.ValDiv2(): 4
Как следует из приведенного выше результата, переменная типа static инициа
лизируется до создания любого объекта ее класса.
На применение методов типа static накладывается ряд следующих ограничений.
* В методе типа static должна отсутствовать ссылка this, поскольку такой метод не выполняется относительно какого-либо объекта.
* В методе типа static допускается непосредственный вызов только других методов типа static, но не метода экземпляра из того самого же класса. Дело в том, что методы экземпляра оперируют конкретными объектами, а метод типа static не вызывается для объекта. Следовательно, у такого метода отсутствуют объекты, которыми он мог бы оперировать.
* Аналогичные ограничения накладываются на данные типа static. Для метода типа static непосредственно доступными оказываются только другие данные типа static, определенные в его классе. Он, в частности, не может оперировать переменной экземпляра своего класса, поскольку у него отсутствуют объекты, которыми он мог бы оперировать.
Ниже приведен пример класса, в котором недопустим метод ValDivDenom() типа
static.
class StaticError { public int Denom = 3; // обычная переменная экземпляра public static int Val = 1024; // статическая переменная /* Ошибка! Непосредственный доступ к нестатической переменной из статического метода недопустим. */ static int ValDivDenom() { return Val/Denom; // не подлежит компиляции! } }
В данном примере кода Denom является обычной переменной, которая недоступна
из метода типа static. Но в то же время в этом методе можно воспользоваться пере
менной Val, поскольку она объявлена как static.
Аналогичная ошибка возникает при попытке вызвать нестатический метод из ста
тического метода того же самого класса, как в приведенном ниже примере.
using System;
class AnotherStaticError { // Нестатический метод. void NonStaticMeth() { Console.WriteLine("В методе NonStaticMeth()."); } /* Ошибка! Непосредственный вызов нестатического метода из статического метода недопустим. */ static void staticMeth() { NonStaticMeth(); // не подлежит компиляции! } }
В данном случае попытка вызвать нестатический метод (т.е. метод экземпляра) из
статического метода приводит к ошибке во время компиляции.
Следует особо подчеркнуть, что из метода типа static нельзя вызывать мето
ды экземпляра и получать доступ к переменным экземпляра его класса, как это
обычно делается посредством объектов данного класса. И объясняется это тем, что
без указания конкретного объекта переменная или метод экземпляра оказываются
недоступными. Например, приведенный ниже фрагмент кода считается совершенно
верным.
class MyClass { // Нестатический метод. void NonStaticMeth() { Console.WriteLine("В методе NonStaticMeth()."); } /* Нестатический метод может быть вызван из статического метода по ссылке на объект. */ public static void staticMeth(MyClass ob) { ob.NonStaticMeth(); // все верно! } }
В данном примере метод NonStaticMeth() вызывается из метода staticMeth()
по ссылке на объект ob типа MyClass.
Поля типа static не зависят от конкретного объекта, и поэтому они удобны для
хранения информации, применимой ко всему классу. Ниже приведен пример про
граммы, демонстрирующей подобную ситуацию. В этой программе поле типа static
служит для хранения количества существующих объектов.
// Использовать поле типа static для подсчета // экземпляров существующих объектов. using System;
class CountInst { static int count = 0; // Инкрементировать подсчет, когда создается объект. public CountInst() { count++; } // Декрементировать подсчет, когда уничтожается объект. ~Countlnst() { count--; } public static int GetCount() { return count; } }
class CountDemo { static void Main() { CountInst ob; for(int i=0; i < 10; i++) { ob = new CountInst(); Console.WriteLine("Текущий подсчет: " + CountInst.GetCount()); } } }
Выполнение этой программы приводит к следующему результату.
Текущий подсчет: 1 Текущий подсчет: 2 Текущий подсчет: 3 Текущий подсчет: 4 Текущий подсчет: 5 Текущий подсчет: 6 Текущий подсчет: 7 Текущий подсчет: 8 Текущий подсчет: 9 Текущий подсчет: 10
Всякий раз, когда создается объект типа CountInst, инкрементируется поле count
типа static. Но всякий раз, когда такой объект утилизируется, поле count декре
ментируется. Следовательно, поле count всегда содержит количество существующих
в настоящий момент объектов. И это становится возможным только благодаря исполь
зованию поля типа static. Аналогичный подсчет нельзя организовать с помощью
переменной экземпляра, поскольку он имеет отношение ко всему классу, а не только к
конкретному экземпляру объекта этого класса.
Ниже приведен еще один пример применения статических членов класса. Ранее в
этой главе было показано, как объекты создаются с помощью фабрики класса. В том
примере фабрика была нестатическим методом, а это означало, что фабричный метод
можно было вызывать только по ссылке на объект, который нужно было предвари
тельно создать. Но фабрику класса лучше реализовать как метод типа static, что даст
возможность вызывать этот фабричный метод, не создавая ненужный объект. Именно
это улучшение и отражено в приведенном ниже измененном примере программы,
реализующей фабрику класса.
// Использовать статическую фабрику класса. using System;
class MyClass { int a, b; // Создать фабрику для класса MyClass. static public MyClass Factory(int i, int j) { MyClass t = new MyClass(); t.a = i; t.b = j; return t; // возвратить объект } public void Show() { Console.WriteLine("а и b: " + a + " " + b); } }
class MakeObjects { static void Main() { int i, j; // Сформировать объекты, используя фабрику. for(i=0, j=10; i < 10; i++, j--) { MyClass ob = MyClass.Factory(i, j); // создать объект ob.Show(); } Console.WriteLine(); } }
В этом варианте программы фабричный метод Factory() вызывается по имени
его класса в следующей строке кода.
MyClass ob = MyClass.Factory(i, j); // создать объект
Теперь нет необходимости создавать объект класса MyClass, перед тем как пользо
ваться фабрикой этого класса.
### Статические конструкторы
Конструктор можно также объявить как static. Статический конструктор,
как правило, используется для инициализации компонентов, применяемых ко
всему классу, а не к отдельному экземпляру объекта этого класса. Поэтому члены
класса инициализируются статическим конструктором до создания каких-либо
объектов этого класса. Ниже приведен простой пример применения статического
конструктора.
// Применить статический конструктор. using System;
class Cons { public static int alpha; public int beta; // Статический конструктор. static Cons() { alpha = 99; Console.WriteLine("В статическом конструкторе."); } // Конструктор экземпляра. public Cons() { beta = 100; Console.WriteLine("В конструкторе экземпляра."); } }
class ConsDemo { static void Main() { Cons ob = new Cons(); Console.WriteLine("Cons.alpha: " + Cons.alpha); Console.WriteLine("ob.beta: " + ob.beta); } }
При выполнении этого кода получается следующий результат.
В статическом конструкторе. В конструкторе экземпляра. Cons.alpha: 99 ob.beta: 100
Обратите внимание на то, что конструктор типа static вызывается автоматически,
когда класс загружается впервые, причем до конструктора экземпляра. Из этого мож
но сделать более общий вывод: статический конструктор должен выполняться до лю
бого конструктора экземпляра. Более того, у статических конструкторов отсутствуют
модификаторы доступа — они пользуются доступом по умолчанию, а следовательно,
их нельзя вызывать из программы.
## Статические классы
Класс можно объявлять как static. Статический класс обладает двумя основны
ми свойствами. Во-первых, объекты статического класса создавать нельзя. И во-вторых,
статический класс должен содержать только статические члены. Статический класс
создается по приведенной ниже форме объявления класса, видоизмененной с помо
щью ключевого слова static.
static class имя_класса { // ...
В таком классе все члены должны быть объявлены как static. Ведь если класс ста
новится статическим, то это совсем не означает, что статическими становятся и все его
члены.
Статические классы применяются главным образом в двух случаях. Во-первых,
статический класс требуется при создании метода расширения. Методы расширения
связаны в основном с языком LINQ и поэтому подробнее рассматриваются в главе 19.
И во-вторых, статический класс служит для хранения совокупности связанных друг с
другом статических методов. Именно это его применение и рассматривается ниже.
В приведенном ниже примере программы класс NumericFn типа static слу
жит для хранения ряда статических методов, оперирующих числовым значением.
А поскольку все члены класса NumericFn объявлены как static, то этот класс также
объявлен как static, чтобы исключить получение экземпляров его объектов. Таким
образом, класс NumericFn выполняет организационную роль, предоставляя удобные
средства для группирования логически связанных методов.
// Продемонстрировать применение статического класса. using System;
static class NumericFn { // Возвратить обратное числовое значение. static public double Reciprocal(double num) { return 1/num; } // Возвратить дробную часть числового значения. static public double FracPart(double num) { return num - (int) num; } // Возвратить логическое значение true, если числовое // значение переменной num окажется четным. static public bool IsEven(double num) { return (num % 2) == 0 ? true : false; } // Возвратить логическое значение true, если числовое // значение переменной num окажется нечетным. static public bool IsOdd(double num) { return !IsEven(num); } }
class StaticClassDemo { static void Main() { Console.WriteLine("Обратная величина числа 5 равна " + NumericFn.Reciprocal(5.0)); Console.WriteLine("Дробная часть числа 4.234 равна " + NumericFn.FracPart(4.234)); if(NumericFn.IsEven(10)) Console.WriteLine("10 — четное число."); if (NumericFn.IsOdd(5)) Console.WriteLine("5 — нечетное число."); // Далее следует попытка создать экземпляр объекта класса NumericFn, // что может стать причиной появления ошибки. // NumericFn ob = new NumericFn(); // Ошибка! } }
Вот к какому результату приводит выполнение этой программы.
Обратная величина числа 5 равна 0.2 Дробная часть числа 4.234 равна 0.234 10 — четное число. 5 — нечетное число.
Обратите внимание на то, что последняя строка приведенной выше про
граммы закомментирована. Класс NumericFn является статическим, и поэтому
любая попытка создать объект этого класса может привести к ошибке во время
компиляции. Ошибкой будет также считаться попытка сделать нестатическим член
класса NumericFn.
И последнее замечание: несмотря на то, что для статического класса не допускается
наличие конструктора экземпляра, у него может быть статический конструктор.