198
Часть I. Язык C#
//
Вызываем метод с двумя значениями. min = ob.minVal(a, b);
Console.WriteLine("Минимум равен " + min);
// call with 3 values min = ob.minVal(a, b, -1);
Console.WriteLine("Минимум равен " + min);
//
Вызываем метод с пятью значениями. min = ob.minVal(18, 23, 3, 14, 25);
Console.WriteLine("Минимум равен " + min);
//
Этот метод можно также вызвать с int-массивом. int[] args = { 45, 67, 34, 9, 112, 8 }; min = ob.minVal(args);
Console.WriteLine("Минимум равен " + min);
}
}
Вот результаты выполнения этой программы:
Минимум равен 10
Минимум равен -1
Минимум равен 3
Минимум равен 8
При каждом вызове метода minVal()
аргументы передаются ему через массив nums. Длина этого массива равна количеству элементов. Поэтому метод minVal()
можно использовать для определения минимального из любого числа элементов.
Несмотря на то что params
-параметру можно передать любое количество аргументов, все они должны иметь тип, совместимый с типом массива, заданным этим параметром. Например, такой вызов метода minVal() min = ob.minVal(1, 2.2); неверен, поскольку автоматического преобразования значения типа double(2.2)
в значение типа int
(тип int имеет массив nums в методе minVal()
) не существует.
При использовании модификатора params необходимо внимательно отнестись к граничным ситуациям задания аргументов, поскольку params
-параметр может принять любое количество аргументов, даже нулевое! Например, синтаксически вполне допустимо вызвать метод minVal()
следующим образом: min = ob.minVal(); // аргументы отсутствуют min = ob.minVal(3); // один аргумент
Вот поэтому в методе minVal()
до попытки доступа к элементу массива nums предусмотрена проверка существования хотя бы одного элемента в массиве. Если бы такая проверка отсутствовала, то при вызове метода minVal()
без аргументов имела бы место исключительная ситуация. (Ниже в этой книге при рассмотрении исключительных ситуаций будет показан более удачный способ обработки таких типов ошибок.) Более того, код метода minVal()
был написан так специально, чтобы разрешить его вызов с одним аргументом. В этом случае метод возвращает этот (единственный) аргумент.
Наряду с обычными параметрами методы могут иметь и параметр переменной длины.
Например, в следующей программе метод showArgs()
принимает один параметр типа string и params
-массив целочисленного типа.
// Использование обычного параметра вместе
// с params-параметром.
Глава 8. Подробнее о методах и классах
199 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 { public 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;
}
200
Часть I. Язык C# public void show() {
Console.WriteLine(width + " " + height);
}
/*
Метод возвращает прямоугольник, который увеличен по сравнению с вызывающим объектом прямоугольника с использованием заданного коэффициента увеличения. */ public Rect enlarge(int factor) { return new Rect(width * factor, height * factor);
}
} class RetObj { public static void Main() {
Rect r1 = new Rect(4, 5);
Console.Write("Размеры прямоугольника r1: "); r1.show();
Console.WriteLine("Площадь прямоугольника r1: " + r1.area());
Console.WriteLine();
//
Создаем прямоугольник, который вдвое больше
// прямоугольника r1 .
Rect r2 = r1.enlarge(2);
Console.Write("Размеры прямоугольника r2: "); r2.show();
Console.WriteLine("Площадь прямоугольника r2: " + r2.area());
}
}
Вот результаты выполнения этой программы:
Размеры прямоугольника r1: 4 5
Площадь прямоугольника r1: 20
Размеры прямоугольника r2: 8 10
Площадь прямоугольника r2: 80
В тех случаях, когда метод возвращает объект, существование этого объекта продолжается до тех пор, пока на него есть хотя бы одна ссылка. И только когда на объект больше нет ни одной ссылки, он подвергается утилизации, т.е. попадает в поле действия процесса сбора мусора. Таким образом, объект не будет разрушен только по причине завершения метода, который его создал.
Одним из применений классовых типов значений, возвращаемых методами, является генератор объектов класса, или фабрика класса (class factory). “Фабрика” класса — это метод, который используется для построения объектов заданного класса. В определенных случаях пользователям некоторого класса нежелательно предоставлять доступ к. конструктору этого класса из соображений безопасности или по причине того, что создание объектов зависит от неких внешних факторов. В таких случаях для построения объектов и используется “фабрика” класса. Рассмотрим простой пример:
// Использование "фабрики" класса. using System;
Глава 8. Подробнее о методах и классах
201 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 { public 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: 7 3 а и b: 8 2 а и b: 9 1
Рассмотрим этот пример более внимательно. В классе
MyClass конструктор не определен, поэтому доступен только конструктор, создаваемый средствами C# по умолчанию. Следовательно, установить значения членов класса а
и b
с помощью конструктора невозможно. Однако создавать объекты с заданными значениями членов а
и b
способна “фабрика” класса, реализованная в виде метода factory()
. Более того, поскольку члены а
и b
закрыты, использование метода factory()
— единственный способ установки этих значений.
В функции
Main()
создается объект ob класса
MyClass
, а затем в цикле for создается еще десять объектов. Приведем здесь строку кода, которая представляет собой основной “конвейер” объектов.
MyClass anotherOb = ob.factory(i, j); // создаем объект
202 Часть I. Язык C#
На каждой итерации цикла создается ссылочная переменная anotherOb
, которой присваивается ссылка на объект, сгенерированный “фабрикой” объектов. В конце каждой итерации цикла ссылочная переменная anotherOb
выходит из области видимости, и объект, на который она ссылалась, утилизируется.
Возвращение методами массивов Поскольку в C# массивы реализованы как объекты, метод может возвратить массив.
(В этом еще одно отличие C# от языка C++, в котором не допускается, чтобы метод, или функция, возвращал массив.) Например, в следующей программе метод findfactors()
возвращает массив, который содержит множители аргумента, переданного этому методу.
// Демонстрация возврата методом массива. using System; class Factor {
/*
Метод возвращает массив, содержащий множители параметра num. После выполнения метода out-параметр numfactors будет содержать количество найденных множителей. */ 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 { public static void Main() {
Factor f = new Factor(); int numfactors; 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
Глава 8. Подробнее о методах и классах
203
В классе
Factor метод findfactors()
объявляется следующим образом: public int[] findfactors(int num, out int numfactors) {
Обратите внимание на то, как задан тип возвращаемого массива int
. Этот синтаксис можно обобщить. Если вам нужно, чтобы метод возвращал массив, объявите его (метод) подобным образом, изменив при необходимости тип массива и размерность. Например, эта инструкция объявляет метод с именем someMeth()
, который возвращает двумерный массив double
-значений. public double[,] someMeth() { // ...
Перегрузка методов
В этом разделе мы узнаем об одной из самых удивительных возможностей языка C#
— перегрузке методов. В C# два или больше методов внутри одного класса могут иметь одинаковое имя, но при условии, что их параметры будут различными. Такую ситуацию называют перегрузкой методов (method overloading), а методы, которые в ней задействованы, — перегруженными (overloaded). Перегрузка методов — один из способов реализации полиморфизма в C#.
В общем случае для создания перегрузки некоторого метода достаточно объявить еще одну его версию. Об остальном позаботится компилятор. Но здесь необходимо отметить одно важное условие: все перегруженные методы должны иметь списки параметров, которые отличаются по типу и/или количеству. Методам для перегрузки
недостаточно отличаться лишь типами возвращаемых значений. Они должны отличаться типами или числом параметров. (Другими словами, тип возвращаемого значения не обеспечивает достаточную информацию для 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("Два int-параметра: " + a + " " + b); return a + b;
}
//
Перегружаем метод ovlDemo() для двух
204
Часть I. Язык C#
// double-параметров. public double ovlDemo(double a, double b) {
Console.WriteLine("Два double-параметра: " + a + " "+ b); return a + b;
}
} class OverloadDemo { public 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.2): " + resD);
}
}
Программа генерирует следующие результаты:
Без параметров
Один параметр: 2
Два int-параметра: 4 6
Результат вызова ob.ovlDemo(4, 6): 10
Два double-параметра: 1,1 2,32
Результат вызова ob.ovlDemo(1.1, 2.2): 3,42
Как видите, метод ovlDemo()
перегружается четыре раза. Первая версия вообще не принимает параметров, вторая принимает один целочисленный параметр, третья — два целочисленных параметра, а четвертая — два double
-параметра. Обратите внимание на то, что первые две версии метода ovlDemo()
возвращают тип void
, т.е. не возвращают никакого значения, а вторые две возвращают значения соответствующих типов. Это вполне допустимо, но, как уже разъяснялось, перегрузка методов не достигается различием только в типе возвращаемого значения. Поэтому попытка использовать следующие две версии метода ovlDemo()
приведет к ошибке:
// Одно объявление метода ovlDemo(int) вполне допустимо. public void ovlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
}
Глава 8. Подробнее о методах и классах
205
// Ошибка! Два объявления метода ovlDemo(int) неприемлемы,
// несмотря на то, что типы возвращаемых ими значений
// разные. public int ovlDemo(int a) {
Console.WriteLine("Один параметр: " + a); return a * a;
}
Как отмечено в комментариях, различие в типах значений, возвращаемых методами, является недостаточным фактором для обеспечения их перегрузки.
Как указывалось в главе 3, в определенных пределах C# обеспечивает автоматическое преобразование типов. Эта возможность преобразования типов применяется и к параметрам перегруженных методов. Рассмотрим, например, следующую программу.
/* Возможность автоматического преобразования типов может повлиять на решение о перегрузке методов. */ using System; class Overload2 { public void f(int x) {
Console.WriteLine("Внутри метода f(int): " + x);
} public void f(double x) {
Console.WriteLine("Внутри метода f(double): " + x);
}
} class TypeConv { public static void Main() {
Overload2 ob = new Overload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i);
// Вызов метода ob.f(int). ob.f(d);
// Вызов метода ob.f(double). ob.f(b);
// Вызов метода ob.f(int) — выполняется
// преобразование типов. ob.f(s);
// Вызов метода ob.f(int) — выполняется
// преобразование типов. ob.f(f);
// Вызов метода ob.f(double) — выполняется
// преобразование типов.
}
}
Вот результаты выполнения этой программы:
Внутри метода f(int): 10
Внутри метода f(double): 10,1
Внутри метода f(int): 99
Внутри метода f(int): 10
Внутри метода f(double): 11,5
206
Часть I. Язык C#
В этом примере определены только две версии метода f()
: одна с int
-, а другая с double
-параметром. Тем не менее методу f()
можно передать помимо значений типа int и double также значения типа byte
, short или float
. В случае передачи byte
-или short
-параметров C# автоматически преобразует их в значения типа int
(т.е. будет вызвана версия f(int)
). В случае передачи float
-параметра его значение будет преобразовано в значение типа double и будет вызвана версия f(double)
Здесь важно понимать, что автоматическое преобразование применяется только в том случае, когда не существует прямого соответствия параметра и аргумента. Дополним предыдущую программу версией метода f()
, в которой определен параметр типа byte
// Добавление к предыдущей программе версии f(byte). using System; class Overload2 { public void f(byte x) {
Console.WriteLine("Внутри метода f(byte): " + x);
} public void f(int x) {
Console.WriteLine("Внутри метода f(int): " + x);
} public void f(double x) {
Console.WriteLine("Внутри метода f(double): " + x);
}
} class TypeConv { public static void Main() {
Overload2 ob = new Overload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i);
// Вызов метода ob.f(int). ob.f(d);
// Вызов метода ob.f(double). ob.f(b);
// Вызов метода ob.f(byte) - теперь без
// преобразования типов. ob.f(s);
// Вызов метода ob.f(int) — выполняется
// преобразование типов. ob.f(f);
// Вызов метода ob.f(double) — выполняется
// преобразование типов.
}
}
Этот вариант программы генерирует такие результаты:
Внутри метода f(int): 10
Внутри метода f(double): 10,1
Глава 8. Подробнее о методах и классах
207 Внутри метода f(byte): 99
Внутри метода f(int): 10
Внутри метода f(double): 11,5
В этом варианте, поскольку существует версия метода f()
, которая предназначена для приема аргумента типа byte
, при вызове метода f() с byte
-аргументом будет вызвана версия f(byte)
, и автоматического преобразования byte
-аргумента в значение типа int не произойдет.
Наличие как ref
-, так и out
-модификатора играет роль в “зачете” перегруженных функций. Например, в следующем фрагменте кода определяются два различных метода. public void f(int x) {
Console.WriteLine("Внутри метода f(int): " + x);
} public void f(ref int x) {
Console.WriteLine("Внутри метода f(ref int): " + x);
}
Таким образом, при выполнении инструкции ob.f(i); вызывается метод f(int x)
, но при выполнении инструкции ob.f(ref i); вызывается метод f(ref int x)
Посредством перегрузки методов в C#
поддерживается полиморфизм, поскольку это единственный способ реализации в C# парадигмы “один интерфейс — множество методов”.
Чтобы понять, как это происходит, рассмотрим следующее. В языке, который не поддерживает перегрузку методов, каждый метод должен иметь уникальное имя. Однако часто нужно реализовать один и тот же метод для различных типов данных. Возьмем, например, функцию, возвращающую абсолютное значение. В языках, которые не поддерживают перегрузку методов, обычно существует три или даже больше версий этой функции, причем их имена незначительно отличаются. Например, в языке C функция abs()
возвращает абсолютное значение (модуль) целого числа, функция labs()
возвращает модуль длинного целочисленного значения, а fabs()
— модуль значения с плавающей точкой. Поскольку язык C не поддерживает перегрузку методов, каждая функция должна иметь собственное имя, несмотря на то, что все три функции выполняют по сути одно и то же действие. Это делает ситуацию сложнее, чем она есть на самом деле.
Другими словами, при одних и тех же действиях программисту необходимо помнить имена всех трех (в данном случае) функций. Язык C# избавлен от ненужного “размножения” имен, поскольку все методы получения абсолютного значения могут использовать одно и то же имя. И в самом деле, библиотека стандартных классов C# включает метод получения абсолютного значения с именем
Abs()
. Этот метод перегружается C#-классом
System.Math
, что позволяет обрабатывать значения всех числовых типов, используя одно имя метода. Определение того, какая именно версия метода должна быть вызвана, основано на типе передаваемого аргумента.
Принципиальная значимость перегрузки состоит в том, что она позволяет обращаться к связанным методам посредством одного, общего для всех имени. Следовательно, имя
Abs()
представляет
общее действие, которое выполняется во всех случаях. Компилятору остается правильно выбрать
конкретную версию при конкретных обстоятельствах. А программисту нужно помнить лишь общую операцию, которая связана с именем того Или иного метода. Благодаря полиморфизму применение нескольких имен сводится к одному.
Несмотря на простоту приведенного примера, он все же позволяет понять, что перегрузка способна упростить процесс программирования.
208
Часть I. Язык C#
Необходимо подчеркнуть, что каждая версия перегруженного метода может выполнять определенные действия. Не существует правила, которое бы обязывало программиста связывать перегруженные методы общими действиями. Однако с точки зрения стилистики перегрузка методов все-таки подразумевает определенное “родство” его версий. Таким образом, несмотря на то, что вы можете использовать одно и то же имя для перегрузки не связанных общими действиями методов, этого делать не стоит. Например, в принципе можно использовать имя sqr для создания метода, который возвращает квадрат целого числа, и метода, который возвращает значение квадратного корня из вещественного числа. Но поскольку эти операции фундаментально различны, применение механизма перегрузки методов в этом случае сводит на нет его первоначальную цель. Хороший стиль программирования состоит в организации перегрузки тесно связанных операций.
В C# используется термин сигнатура (signature), который представляет собой имя метода со списком его параметров. Таким образом, в целях обеспечения перегрузки никакие два метода внутри одного и того же класса не должны иметь одинаковую сигнатуру. Обратите внимание на то, что сигнатура не включает тип значения, возвращаемого методом, поскольку этот фактор не используется в C# для принятия решения о выполнении требуемого перегруженного метода. Сигнатура также не включает params-параметр, если таковой существует. Другими словами, модификатор params не является определяющим фактором отличия одного перегруженного метода от другого.
Перегрузка конструкторов
Подобно другим методам, конструкторы также можно перегружать. Это позволяет создавать объекты различными способами. Рассмотрим следующую программу:
// Демонстрация перегруженных конструкторов. using System; class MyClass { public int x; public MyClass() {
Console.WriteLine("Внутри конструктора MyClass()."); x = 0;
} public MyClass(int i) {
Console.WriteLine("Внутри конструктора MyClass(int)."); x = i;
} public MyClass(double d) {
Console.WriteLine(
"Внутри конструктора MyClass(double)."); x = (int) d;
} public MyClass(int i, int j) {
Console.WriteLine(
"Внутри конструктора MyClass(int, int)."); x = i * j;
Глава 8. Подробнее о методах и классах
209
}
} class OverloadConsDemo { public static void Main() {
MyClass t1 = new MyClass();
MyClass t2 = new MyClass(88);
MyClass t3 = new MyClass(17.23);
MyClass t4 = new MyClass(2, 4);
Console.WriteLine("t1.x: " + t1.x);
Console.WriteLine("t2.x: " + 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++)
210
Часть I. Язык C# 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 full() { return tos==stck.Length;
}
//
Метод возвращает значение true, если стек пуст. public bool empty() { return tos==0;
}
//
Возвращает общий объем стека. public int capacity() { return stck.Length;
}
//
Возвращает текущее количество объектов в стеке. public int getNum() { return tos;
}
}
// Демонстрация использования класса Stack. class StackDemo { public static void Main() {
Stack stk1 = new Stack(10); char ch; int i;
//
Помещаем символы в стек stk1.
Console.WriteLine(
Глава 8. Подробнее о методах и классах
211 "Помещаем символы от А до Z в стек stk1."); for(i=0; !stk1.full(); i++) stk1.push((char) ('A' + i));
//
Создаем копию стека stck1.
Stack stk2 = new Stack(stk1);
//
Отображаем содержимое стека stk1.
Console.Write("Содержимое стека stk1: "); while( !stk1.empty() ) { ch
= stk1.pop();
Console.Write(ch);
}
Console.WriteLine();
Console.Write("Содержимое стека stk2: "); while( !stk2.empty() ) { ch
= stk2.pop();
Console.Write(ch);
}
Console.WriteLine("\n");
}
}
Результаты выполнения этой программы:
Помещаем символы от А до Z в стек 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;
}
При выполнении кода этого конструктора для массива stck выделяется область памяти, причем ее размер
позволяет поместить в этот массив все элементы, содержащиеся в стеке, заданном в качестве аргумента ob
. Затем содержимое базового массива, на котором основан стек ob
, копируется в новый массив, и соответствующим образом устанавливается переменная индекса tos
. По завершении работы этого конструктора новый и исходный стеки являются отдельными объектами, но идентичны по своему содержимому.
212
Часть I. Язык C#
Вызов перегруженного конструктора с помощью ссылки this
При работе с перегруженными конструкторами иногда необходимо обеспечить вызов одного конструктора из другого. В C# это реализуется с помощью еще одной формы ключевого слова this
. Общий формат записи такого вызова:
имя_конструктора
(
список_параметров1
) : this(
список_параметров2
) {
//
Тело конструктора,
// которое может быть пустым.
}
При выполнении перегруженного конструктора сначала вызывается та его версия, список параметров которой совпадает с элементом
список_параметров2
. При этом будут выполнены любые инструкции, содержащиеся внутри исходного конструктора.
Например:
// Демонстрация вызова конструктора с помощью ссылки this. using System; class XYCoord { public int x, y; public XYCoord() : this(0, 0) {
Console.WriteLine("Внутри конструктора XYCoord()");
} public XYCoord(XYCoord obj) : this(obj.x, obj.y) {
Console.WriteLine("Внутри конструктора XYCoord(obj)");
} public XYCoord(int i, int j) {
Console.WriteLine("Внутри конструктора XYCoord(int, int)"); x = i; y = j;
}
} class OverloadConsDemo { public 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.x, t1.y: 0, 0 t2.x, t2.y: 8, 9 t3.x, t3.y: 8, 9
Глава 8. Подробнее о методах и классах
213
Вот как работает эта программа. В классе
XYCoord единственным конструктором, который реально инициализирует члены x
и y
, является
XYCoord(int, int)
. Остальные два конструктора просто вызывают конструктор
XYCoord(int, int)
, используя ключевое слово this
. Например, при создании объекта t1
вызывается конструктор
XYCoord()
, выполняющий вызов this(0, 0)
, который преобразуется в вызов конструктора
XYCoord(0, 0)
, Создание объекта t2
происходит аналогично.
Преимущество использования ключевого слова this для вызова перегруженных конструкторов состоит в том, что можно избежать ненужного дублирования кода. В предыдущем примере применение слова this позволило избежать дублирования всеми тремя конструкторами одного и того же кода инициализации членов. Еще одно достоинство этого средства — возможность создавать конструкторы с заданием действующих “по умолчанию” аргументов, которые используются в том случае, когда аргументы конструктора не заданы явным образом. Например, вы могли бы создать еще один конструктор класса
XYCoord следующим образом: public XYCoord(int x) : this(x, x) { }
Этот конструктор автоматически устанавливает координату y
равной значению координаты x. Конечно, использовать такие действующие “по умолчанию” аргументы нужно очень аккуратно, поскольку их неправильное использование может ввести пользователей в заблуждение.
Метод Main()
До сих пор мы использовали только одну форму метода
Main()
. Однако существует несколько перегруженных форм этого метода. Одни возвращают значение, а другие принимают аргументы. Рассмотрением этих форм мы и займемся в следующих разделах.
Возвращение значений из метода Main()
По завершении программы можно возвратить значение вызывающему процессу
(часто в его роли выступает операционная система). Для этого используется следующая форма метода
Main(): public static int Main()
Обратите внимание на то, что вместо типа void
, эта версия метода
Main()
имеет в качестве типа возвращаемого значения int
Обычно значение, возвращаемое методом
Main()
, служит индикатором того, как была завершена программа (нормально или аварийно). По соглашению нулевое значение, как правило, подразумевает нормальное завершение. Все же другие значения соответствуют определенным типам ошибок.
Передача аргументов методу Main()
Многие программы принимают аргументы командной строки. Аргумент командной строки — это информация, которая указывается при запуске программы сразу после ее имени в командной строке. Эти аргументы затем передаются методу
Main()
. Для работы с аргументами командной строки необходимо использовать одну из следующих форм метода
Main()
: public static void Main(string[]
args
) public static int Main(string[]
args
)
214 Часть I. Язык C#
Первая форма возвращает значение типа void
, а вторую можно использовать для возврата целочисленного значения, как описано в предыдущем разделе. В обоих случаях аргументы командной строки хранятся как строки в string
-массиве, передаваемом методу
Main()
Следующая программа
отображает все аргументы командной строки, с которыми она была вызвана.
// Отображение всей информации из командной строки. using System; class CLDemo { public static void Main(string[] args) {
Console.WriteLine("Командная строка содержит " + args.Length
+
" аргументов.");
Console.WriteLine("Вот они: "); for(int i=0; i
Console.WriteLine(args[i]);
}
}
Предположим, мы запустили на выполнение программу CLDemo следующим образом:
CLDemo один два три четыре пять
В этом случае мы увидим такие результаты:
Командная строка содержит 5 аргументов. Вот они: один два три четыре пять
Чтобы “попробовать на вкус” возможности использования аргументов командной строки, рассмотрим следующую программу. Она кодирует и декодирует сообщения.
Сообщение, предназначенное для кодирования или декодирования, указывается в командной строке. Метод шифрования очень прост: чтобы закодировать слово, код каждой его буквы инкрементируется на 1. В результате буква “А” превращается в букву “Б” и т.д.
Чтобы декодировать слово, достаточно код каждой его буквы декрементировать на 1.
// Кодирование и декодирование сообщений. using System; class Cipher { public static int Main(string[] args) {
//
Проверка наличия аргументов. if(args.Length < 2) {
Console.WriteLine(
"ИСПОЛЬЗОВАНИЕ: " +
"слово1: <<закодировать>>/<<раскодировать>> " +
"[слово2... словоN]"); return
1;
//
Возврат признака неверного выполнения.
}
//
Если аргументы присутствуют, то первым аргументом
Глава 8. Подробнее о методах и классах
215
// должно быть слово "закодировать" или "раскодировать". 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
, приводим два примера ее выполнения.
D:\Cipher закодировать один два пейо егб
D:\Cipher раскодировать пейо егб один два
В этой программе есть два интересных момента. Во-первых, обратите внимание на то, как проверяется наличие аргументов командной строки. Это очень важный момент, который можно обобщить. Если работа программы опирается на один или несколько аргументов командной строки, всегда необходимо удостовериться в том, что эти аргументы действительно переданы программе. Отсутствие такой проверки может привести к сбою программы. Кроме того, поскольку первым аргументом командной строки должно быть слово “закодировать” или “раскодировать”, то, прежде чем выполнять кодирование или раскодирование текста, необходимо убедиться в наличии этого ключевого слова.
Во-вторых, обратите внимание на то, как программа возвращает код своего завершения. Если командная строка не записана должным образом, возвращается значение
1, которое свидетельствует о нештатной ситуации и аварийном завершении программы.
Возвращаемое значение, равное 0, — признак нормальной работы программы и благополучного ее завершения.
Рекурсия
В C# метод может вызвать сам себя. Этот процесс называется рекурсией, а метод, который вызывает себя, называют рекурсивным. В общем случае рекурсия — это процесс определения чего-либо с использованием самого себя. Ключевым компонентом
216
Часть I. Язык C# рекурсивного метода является обязательное включение им инструкции обращения к самому себе. Рекурсия — это мощный механизм управления.
Классическим примером рекурсии является вычисление факториала числа. Факториал числа
N
представляет собой произведение целых чисел от 1 до
N
. Например, факториал числа 3 равен 1x2x3, или 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 { public 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.factI(3));
Console.WriteLine("Факториал числа 4 равен " + f.factI(4));
Console.WriteLine("Факториал числа 5 равен " + f.factI(5));
}
}
Глава 8. Подробнее о методах и классах
217 Вот результаты выполнения этой программы:
Факториалы, вычисленные с использованием рекурсивного метода.
Факториал числа 3 равен 6
Факториал числа 4 равен 24
Факториал числа 5 равен 120
Факториалы, вычисленные с использованием итеративного метода.
Факториал числа 3 равен 6
Факториал числа 4 равен 24
Факториал числа 5 равен 120
Нерекурсивный метод factI()
довольно прост. В нем используется цикл, в котором организовано перемножение последовательных чисел, начиная с 1 (поэтому начальное значение управляющей переменной равно 1) и заканчивая числом, заданным в качестве параметра метода.
Рекурсивный метод factR()
несколько сложнее. Если он вызывается с аргументом, равным 1, то сразу возвращает значение 1. В противном случае он возвращает произведение factR(n-1) * n
. Для вычисления этого выражения вызывается метод factR()
с аргументом n-1
. Этот процесс повторяется до тех пор, пока аргумент не станет равным 1, после чего вызванные ранее методы начнут возвращать значения. Например, при вычислении факториала числа 2 первое обращение к методу factR()
приведет ко
второму обращению к тому же методу, но с аргументом, равным 1. Второй вызов метода factR()
возвратит значение 1, которое будет умножено на 2 (исходное значение параметра n
).
Возможно, вам будет интересно вставить в метод factR()
инструкции с вызовом метода
WriteLine()
, чтобы показать уровень каждого вызова и промежуточные результаты.
Когда метод вызывает сам себя, в системном стеке выделяется память для новых локальных переменных и параметров, и код метода с самого начала выполняется с новыми переменными. Рекурсивный вызов не создает новой копии метода. Новыми являются только аргументы. При возвращении каждого рекурсивного вызова из стека извлекаются старые локальные переменные и параметры, и выполнение метода возобновляется с
“внутренней” точки вызова этого метода.
Рассмотрим еще один пример рекурсии. Метод 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 { public static void Main() { string s = "Этот тест";
RevStr rsOb = new RevStr();
218 Часть I. Язык C#
Console.WriteLine("Исходная строка: " + s);
Console.Write("Перевернутая строка: "); rsOb.displayRev(s);
Console.WriteLine();
}
}
Вот результаты выполнения этой программы:
Исходная строка: Этот тест
Перевернутая строка: тсет тотЭ
Если при каждом вызове метода displayRev()
проверка показывает, что длина строки str больше нуля, то выполняется рекурсивный вызов displayRev()
с новым аргументом-строкой, которая состоит из предыдущей строки str без ее первого символа.
Этот процесс повторяется до тех пор, пока тому же методу не будет передана строка нулевой длины. После этого вызванные ранее методы начнут возвращать значения, и каждый возврат будет сопровождаться довыполнением метода, т.е. отображением первого символа строки str
. В результате исходная строка посимвольно отобразится в обратном порядке.
Рекурсивные версии многих процедур выполняются медленнее, чем их итеративные эквиваленты, из-за дополнительных затрат системных ресурсов, связанных с многократными вызовами методов. Слишком большое количество рекурсивных обращений к методу может вызвать переполнение стека. Поскольку локальные переменные и параметры сохраняются в системном стеке и каждый новый вызов создает новую копию переменных, может настать момент, когда память стека будет исчерпана. В этом случае C#- системой будет сгенерировано соответствующее исключение. Но если рекурсия построена корректно, об этом вряд ли стоит волноваться.
Основное достоинство рекурсии состоит в том, что некоторые типы алгоритмов рекурсивно реализуются проще, чем их итеративные эквиваленты. Например, алгоритм сортировки Quicksort довольно трудно реализовать итеративным способом. Кроме того, некоторые задачи просто созданы для рекурсивных решений.
При написании рекурсивных методов необходимо включить в них инструкцию проверки условия (например, if
-инструкцию), которая бы заставила вернуться из метода без выполнения рекурсивного вызова. Если этого не будет сделано, то, вызвав однажды метод, из него уже нельзя будет вернуться. При работе с рекурсией это самый распространенный тип ошибки. Поэтому при ее разработке не стоит скупиться на инструкции вызова метода
WriteLine()
, чтобы быть в курсе того, что происходит в методе, и прервать его работу в случае обнаружения ошибки.
Использование модификатора типа static Иногда требуется определить член класса, который должен использоваться независимо от объекта этого класса. Обычно к члену класса доступ предоставляется через объект этого класса. Однако можно создать член, который заведомо разрешено использовать сам по себе, т.е. без ссылки на конкретный экземпляр. Чтобы создать такой член, предварите его объявление ключевым словом static
. Если член объявлен как static
, к нему можно получить доступ до создания объектов этого класса и без ссылки на объект. С использованием ключевого слова static
можно объявлять как методы, так и переменные. В качестве первого примера static
-члена приведем метод
Main()
, который должен быть вызван операционной системой в начале работы программы.
Глава 8. Подробнее о методах и классах
219
При использовании static
-члена вне класса необходимо указать имя класса и следующий за ним оператор “точка”. Объект при этом не нужно создавать. К static
- члену получают доступ не через экземпляр класса, а с помощью имени класса. Например, чтобы присвоить число 10 static
-переменной с именем count
, которая является членом класса
Timer
, используйте следующую строку кода:
Timer.count = 10;
Этот формат подобен тому, что используется для доступа к обычной переменной экземпляра через объект, но здесь вместо имени объекта необходимо указать имя класса.
Аналогично можно вызвать и static
-метод, т.е. с помощью оператора “точка” после имени класса.
Переменные, объявленные как static
-члены, являются по сути глобальными переменными. При объявлении объектов класса копии static
-переменной не создаются, причем все экземпляры класса совместно используют одну и ту же static
-переменную.
Инициализация static
-переменной происходит при загрузке класса. Если инициализатор явно не указан, static
-переменная, предназначенная для хранения числовых значений, инициализируется нулем; объектные ссылки — null
-значениями, а переменные типа bool
— значением false
. Таким образом, static
-переменная всегда имеет значение.
Различие между static
- и обычным методом состоит в том, что static
-метод можно вызвать посредством имени класса, без необходимости создания объекта этого класса. Выше вы уже имели возможность рассмотреть пример такого вызова, когда обращались к static
-методу
Sqrt()
, принадлежащему классу
System.Math
,
Теперь рассмотрим пример создания static
-переменной и static
-метода.
// Использование модификатора типа static. using System; class StaticDemo {
//
Объявление статической переменной. public static int val = 100;
//
Объявление статического метода. public static int valDiv2() { return val/2;
}
} class SDemo { public static void Main() {
Console.WriteLine(
"Начальное значение переменной StaticDemo.val равно " +
StaticDemo.val);
StaticDemo.val = 8;
Console.WriteLine(
"Значение переменной StaticDemo.val равно " +
StaticDemo.val);
Console.WriteLine("StaticDemo.valDiv2(): " +
StaticDemo.valDiv2());
}
}
При выполнении эта программа генерирует следующие результаты:
220
Часть I. Язык C#
Начальное значение переменной StaticDemo.val равно 100
Значение переменной StaticDemo.val равно 8
StaticDemo.valDiv2() : 4
Как видно по результатам выполнения программы, static
-переменная инициализируется в начале ее работы, т.е. еще до создания объекта класса, в котором она определяется.
На static
-методы накладывается ряд ограничений.
1. static
-метод не имеет ссылки this
,
2. static
-метод может напрямую вызывать только другие static
-методы. Он не может напрямую вызывать метод экземпляра своего класса. Дело в том, что методы экземпляров работают с конкретными экземплярами класса, чего не скажешь о static
-методах.
3. static
-метод должен получать прямой доступ только к static
-данным. Он не может напрямую использовать переменные экземпляров, поскольку не работает с экземплярами класса.
Например, в следующем классе static
-метод valDivDenom()
недопустим: class StaticError { int denom =3; // обычная переменная экземпляра static int val = 1024; // статическая переменная
/*
Ошибка! Внутри статического метода прямой доступ к нестатической переменной недопустим. */ static int valDivDenom() { return val/denom; // Инструкция не скомпилируется!
}
}
Здесь denom
— обычная переменная экземпляра, к которой невозможно получить доступ внутри статического метода. Однако с использованием переменной val проблем нет, поскольку это static
-переменная.
Аналогичная проблема возникает при попытке вызвать нестатический метод из static- метода того же класса. Вот пример: using System; class AnotherStaticError { // Нестатический метод. void nonStaticMeth() {
Console.WriteLine("Внутри метода nonStaticMeth().");
}
/*
Ошибка! Внутри статического метода нельзя напрямую вызвать нестатический метод. */ static void staticMeth() { nonStaticMeth();
//
Инструкция не скомпилируется!
}
}
В этом случае попытка вызвать нестатический метод (т.е. метод экземпляра) из статического метода приведет к ошибке компиляции.
Важно понимать, что static
-метод может вызывать методы экземпляров и получать доступ к переменным экземпляров своего класса, но должен делать это через объект класса. Другими словами, он не может использовать обычные члены класса без указания конкретного объекта. Например, этот фрагмент программы совершенно корректен:
Глава 8. Подробнее о методах и классах
221 class MyClass {
//
Нестатический метод. void nonStaticMeth() {
Console.WriteLine("Внутри метода nonStaticMeth().");
}
/*
Внутри статического метода можно вызвать нестатический метод, использовав ссылку на объект. */ public static void staticMeth(MyClass ob) { ob.nonStaticMeth();
//
Здесь все в порядке.
}
}
Поскольку static
-поля не зависят от конкретного объекта, они используются при обработке информации, применимой ко всему классу. Рассмотрим пример такой ситуации.
В следующей программе используется static
-поле для обработки счетчика числа существующих объектов.
// Использование static-поля для подсчета экземпляров класса. using System; class CountInst { static int count = 0;
//
Инкрементируем счетчик при создании объекта. public CountInst() { count++;
}
//
Декрементируем счетчик при разрушении объекта.
CountInst()
{ count--;
} public static int getcount() { return count;
}
} class CountDemo { public static void Main() {
CountInst ob; for(int i=0; i < 10; i++) { ob = new CountInst();
Console.WriteLine("Текущее содержимое счетчика: " +
CountInst.getcount());
}
}
}
Результаты выполнения этой программы выглядят так:
Текущее содержимое счетчика: 1
Текущее содержимое счетчика: 2
Текущее содержимое счетчика: 3
Текущее содержимое счетчика: 4
Текущее содержимое счетчика: 5
222 Часть I. Язык C#
Текущее содержимое счетчика: 6
Текущее содержимое счетчика: 7
Текущее содержимое счетчика: 8
Текущее содержимое счетчика: 9
Текущее содержимое счетчика: 10
Каждый раз, когда создается объект типа
CountInst
, static
-поле count инкрементируется. И каждый раз, когда объект типа
CountInst разрушается, 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 { public static void Main() { int i, j;
//
Генерируем объекты с помощью "фабрики" класса. for(i=0, j=10; i < 10; i++, j--) {
MyClass ob = MyClass.factory(i, j);
// Получение
// объекта. ob.show();
}
Глава 8. Подробнее о методах и классах
223
Console.WriteLine();
}
}
В этой версии программы метод factory()
вызывается посредством указания имени класса:
MyClass ob = MyClass.factory(i, j); // Получение объекта.
Этот пример показывает, что нет необходимости создавать объект класса
MyClass до использования “фабрики” класса.
Статические конструкторы
Конструктор класса также можно объявить статическим. Статический конструктор обычно используется для инициализации атрибутов, которые применяются к классу в целом, а не к конкретному его экземпляру. Таким образом, статический конструктор служит для инициализации аспектов класса до создания объектов этого класса. Рассмотрим простой пример.
// Использование статического конструктора. 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 { public 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
-конструкторы должны быть закрытыми, и их не может вызвать ваша программа.