Подробнее о методах и классах 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Подробнее о методах и классах



Подробнее о методах и классах

В данной главе продолжается рассмотрение классов и методов. Оно начинается с пояснения механизма управления доступом к членам класса. А затем об­суждаются такие вопросы, как передача и возврат объек­тов, перегрузка методов, различные формы метода Main(), рекурсия и применение ключевого слова static.

Управление доступом к членам класса

Поддержка свойства инкапсуляции в классе дает два главных преимущества. Во-первых, класс связывает данные с кодом. Это преимущество использовалось в предыдущих примерах программ, начиная с главы 6. И во-вторых, класс предоставляет средства для управления доступом к его чле­нам. Именно эта, вторая преимущественная особенность и будет рассмотрена ниже.

В языке С#, по существу, имеются два типа членов клас­са: открытые и закрытые, хотя в действительности дело об­стоит немного сложнее. Доступ к открытому члену свобод­но осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рас­сматривавшихся до сих пор примерах программ. А закры­тый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организует­ся управление доступом.

Ограничение доступа к членам класса является осно­вополагающим этапом объектно-ориентированного про­граммирования, поскольку позволяет исключить невер­ное использование объекта. Разрешая доступ к закрытым данным только с помощью строго определенного ряда методов, можно предупредить присваивание неверных значений этим данным, выполняя, например, проверку диа­пазона представления чисел. Для закрытого члена класса нельзя задать значение непо­средственно в коде за пределами класса. Но в то же время можно полностью управлять тем, как и когда данные используются в объекте. Следовательно, правильно реализо­ванный класс образует некий "черный ящик", которым можно пользоваться, но вну­тренний механизм его действия закрыт для вмешательства извне.

Модификаторы доступа

Управление доступом в языке С# организуется с помощью четырех модификаторов доступа: public, private, protected и internal. В этой главе основное внимание уделяется модификаторам доступа public и private. Модификатор protected при­меняется только в тех случаях, которые связаны с наследованием, и поэтому речь о нем пойдет позднее. А модификатор internal служит в основном для сборки, которая в широком смысле означает в С# разворачиваемую программу или библиотеку, и поэ­тому данный модификатор подробнее рассматривается также позднее.

Когда член класса обозначается спецификатором public, он становится доступ­ным из любого другого кода в программе, включая и методы, определенные в других классах. Когда же член класса обозначается спецификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену (private) данного класса. Как пояснялось в главе 6, если ни один из спецификаторов доступа не указан, член класса считается закры­тым для своего класса по умолчанию. Поэтому при создании закрытых членов класса спецификатор private указывать для них необязательно.

Спецификатор доступа указывается перед остальной частью описания типа отдель­ного члена. Это означает, что именно с него должен начинаться оператор объявления члена класса. Ниже приведены соответствующие примеры.

public string errMsg;

private double bal;

private bool isError(byte status) {//...

Для того чтобы стали более понятными отличия между модификаторами public и private, рассмотрим следующий пример программы.

Листинг 8.1

// Отличия между видами доступа public и private к членам класса.

 

using System;

 

class MyClass

{

private int alpha; // закрытый доступ, указываемый явно

int beta; // закрытый доступ по умолчанию

public int gamma; // открытый доступ

 

// Методы, которым доступны члены alpha и beta данного класса.

// Член класса может иметь доступ к закрытому члену того же класса.

 

public void SetAlpha(int a)

{

alpha = a;

}

 

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 is " + ob.GetAlpha());

Console.WriteLine("ob.beta is " + 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.

Из всего сказанного выше можно сделать следующий важный вывод: закрытый член может свободно использоваться другими членами этого же класса, но недоступен для кода за пределами своего класса.

Возврат объектов из методов

Метод может возвратить данные любого типа, в том числе и тип класса. Ниже в ка­честве примера приведен вариант класса Rect, содержащий метод Enlarge(), в ко­тором строится прямоугольник с теми же сторонами, что и у вызывающего объекта прямоугольника, но пропорционально увеличенными на указанный коэффициент.

Листинг 8.14

// Возврат объекта из метода.

 

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: " + r1.Area());

 

Console.WriteLine();

 

// Создать прямоугольник в два раза больше прямоугольника r1.

Rect r2 = r1.Enlarge(2);

 

Console.Write("Размеры прямоугольника r2: ");

r2.Show();

Console.WriteLine("Площадь прямоугольника r2 " + r2.Area());

}

}

Выполнение этой программы дает следующий результат.

Размеры прямоугольника rl: 4 5

Площадь прямоугольника rl: 20

Размеры прямоугольника г2: 8 10

Площадь прямоугольника г2: 80

Когда метод возвращает объект, последний продолжает существовать до тех пор, пока не останется ссылок на него. После этого он подлежит сборке как "мусор". Сле­довательно, объект не уничтожается только потому, что завершается создавший его метод.

Одним из практических примеров применения возвращаемых данных типа объ­ектов служит фабрика класса, которая представляет собой метод, предназначенный для построения объектов его же класса. В ряде случаев предоставлять пользователям класса доступ к его конструктору нежелательно из соображений безопасности или же потому, что построение объекта зависит от некоторых внешних факторов. В подобных случаях для построения объектов используется фабрика класса. Обратимся к просто­му примеру.

Листинг 8.15

// Использовать фабрику класса.

 

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("a и 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();

}

}

Вот к какому результату приводит выполнение этого кода

a и b: 0 10

a и b: 1 9

a и b: 2 8

a и b: 3 7

a и b: 4 6

a и b: 5 5

a и b: 6 4

a и b: 7 3

a и b: 8 2

a и b: 9 1

Рассмотрим данный пример более подробно. В этом примере конструктор для класса МуСlass не определяется, и поэтому доступен только конструктор, вызывае­мый по умолчанию. Это означает, что значения переменных а и b нельзя задать с по­мощью конструктора. Но в фабрике класса Factory() можно создать объекты, в ко­торых задаются значения переменных а и b. Более того, переменные а и b являются закрытыми, и поэтому их значения могут быть заданы только с помощью фабрики класса Factory().

В методе Main() получается экземпляр объекта класса MyClass, а его фабричный метод используется в цикле for для создания десяти других объектов. Ниже приведе­на строка кода, в которой создаются эти объекты.

MyClass anotherOb = ob.Factory(i, j); // создать объект

На каждом шаге итерации цикла создается переменная ссылки на объект anotherOb, которой присваивается ссылка на объект, формируемый фабрикой клас­са. По завершении каждого шага итерации цикла переменная anotherOb выходит за пределы области своего действия, а объект, на который она ссылается, утилизируется.

Возврат массива из метода

В С# массивы реализованы в виде объектов, а это означает, что метод может также возвратить массив. (В этом отношении С# отличается от С++, где не допускается воз­врат массивов из методов.) В качестве примера ниже приведена программа, в которой метод FindFactors() возвращает массив, содержащий множители переданного ему аргумента.

Листинг 8.16

// Возвратить массив из метода.

 

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 numfactors;

int[] factors;

 

factors = f.FindFactors(1000, out numfactors);

 

Console.WriteLine("Factors for 1000 are: ");

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() { //...

Перегрузка методов

В С# допускается совместное использование одного и того же имени двумя или бо­лее методами одного и того же класса, при условии, что их параметры объявляются по-разному. В этом случае говорят, что методы перегружаются, а сам процесс называ­ется перегрузкой методов. Перегрузка методов относится к одному из способов реализа­ции полиморфизма в С#.

В общем, для перегрузки метода достаточно объявить разные его варианты, а об остальном позаботится компилятор. Но при этом необходимо соблюсти следующее важное условие: тип или число параметров у каждого метода должны быть разными. Совершенно недостаточно, чтобы два метода отличались только типами возвращае­мых значений. Они должны также отличаться типами или числом своих параметров. (Во всяком случае, типы возвращаемых значений дают недостаточно сведений ком­пилятору С#, чтобы решить, какой именно метод следует использовать.) Разумеется, перегружаемые методы могут отличаться и типами возвращаемых значений. Когда вызывается перегружаемый метод, то выполняется тот его вариант, параметры кото­рого соответствуют (по типу и числу) передаваемым аргументам.

Ниже приведен простой пример, демонстрирующий перегрузку методов.

Листинг 8.17

// Продемонстрировать перегрузку методов.

 

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(), оказывается недостаточно для перегрузки данного метода.

В С# предусмотрен ряд неявных (т.е. автоматических) преобразований типов. Эти преобразования распространяются также на параметры перегружаемых методов. В качестве примера рассмотрим следующую программу.

Листинг 8.18

// Неявные преобразования типов могут повлиять

// на решение перегружать метод.

 

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, то компилятор С# автоматически преобразует это зна­чение в тип int и в итоге вызывается вариант MyMeth(int) данного метода. А если ему передается значение типа float, то оно преобразуется в тип double и в результате вызывается вариант MyMeth(double) данного метода.

Следует, однако, иметь в виду, что неявные преобразования типов выполняются лишь в том случае, если отсутствует точное соответствие типов параметра и аргумента. В качестве примера ниже приведена чуть измененная версия предыдущей программы, в которую добавлен вариант метода MyMeth(), где указывается параметр типа byte.

Листинг 8.19

// Добавить метод 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.

Перегрузка методов поддерживает свойство полиморфизма, поскольку именно та­ким способом в С# реализуется главный принцип полиморфизма: один интерфейс - множество методов. Для того чтобы стало понятнее, как это делается, обратимся к конкретному примеру. В языках программирования, не поддерживающих перегрузку методов, каждому методу должно быть присвоено уникальное имя. Но в программировании зачастую возникает потребность реализовать по сути один и тот же метод для обработки разных типов данных. Допустим, что требуется функция, определяю­щая абсолютное значение. В языках, не поддерживающих перегрузку методов, обычно приходится создавать три или более вариантов такой функции с несколько отличаю­щимися, но все же разными именами. Например, в С функция abs() возвращает аб­солютное значение целого числа, функция labs() - абсолютное значение длинного целого числа, а функция fabs() - абсолютное значение числа с плавающей точкой обычной (одинарной) точности.

В С перегрузка не поддерживается, и поэтому у каждой функции должно быть свое, особое имя, несмотря на то, что все упомянутые выше функции, по существу, делают одно и то же - определяют абсолютное значение. Но это принципиально усложняет положение, поскольку приходится помнить имена всех трех функций, хотя они реали­зованы по одному и тому же основному принципу. Подобные затруднения в С# не воз­никают, поскольку каждому методу, определяющему абсолютное значение, может быть присвоено одно и то же имя. И действительно, в состав библиотеки классов для среды.NET Framework входит метод Abs (), который перегружается в классе System.Math для обработки данных разных числовых типов. Компилятор С# сам определяет, какой имен­но вариант метода Abs() следует вызывать, исходя из типа передаваемого аргумента.

Главная ценность перегрузки заключается в том, что она обеспечивает доступ к свя­занным вместе методам по общему имени. Следовательно, имя Abs обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Не­смотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с намного более сложными ситуациями в програм­мировании.

Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не суще­ствует какого-то одного правила, но с точки зрения правильного стиля программи­рования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имя Sqr можно было бы выбрать для методов, воз­вращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции. Такое применение перегрузки методов противо­речит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции.

В С# определено понятие сигнатуры, обозначающее имя метода и список его па­раметров; Применительно к перегрузке это понятие означает, что в одном классе не должно существовать двух методов с одной и той же сигнатурой. Следует подчеркнуть, что в сигнатуру не входит тип возвращаемого значения, поскольку он не учитывается, когда компилятор С# принимает решение о перегрузке метода. В сигнатуру не входит также модификатор params.

Перегрузка конструкторов

Как и методы, конструкторы также могут перегружаться. Это дает возможность конструировать объекты самыми разными способами. В качестве примера рассмотрим следующую программу.

Листинг 8.20

// Продемонстрировать перегрузку конструктора.

 

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.x: " + t2.x);

Console.WriteLine("t3.x: " + t3.x);

Console.WriteLine("t4.x: " + t4.x);

}

}

При выполнении этой программы получается следующий результат.

В конструкторе MyClass().

В конструкторе MyClass (int).

В конструкторе MyClass(double).

В конструкторе MyClass (int, int).

tl.x: 0

t2.x: 88

t3.x: 17

t4.x: 8

В данном примере конструктор MyClass() перегружается четыре раза, всякий раз конструируя объект по-разному. Подходящий конструктор вызывается каждый раз, исходя из аргументов, указываемых при выполнении оператора new. Перегрузка кон­структора класса предоставляет пользователю этого класса дополнительные преиму­щества в конструировании объектов.

Одна из самых распространенных причин для перегрузки конструкторов заключа­ется в необходимости предоставить возможность одним объектам инициализировать другие. В качестве примера ниже приведен усовершенствованный вариант разработан­ного ранее класса Stack, позволяющий конструировать один стек из другого.

Листинг 8.21

// Класс для хранения символов в стеке.

 

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("Поместить симвоы A-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");

 

}

}

Результат выполнения этой программы приведен ниже.

Поместить символы A-J в стек stkl.

Содержимое стека stkl: JIHGFEDCBA

Содержимое стека stk2: JIHGFEDCBA

В классе StackDemo сначала конструируется первый стек (stkl), заполняемый символами. Затем этот стек используется, для конструирования второго стека (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, а затем все остальные операторы, если таковые имеются в исходном конструкторе. Ниже приве­ден соответствующий пример.

Листинг 8.22

// Продемонстрировать вызов конструктора с помощью this.

 

using System;

 

class XYCoord

{

public int x, y;

 

public XYCoord(): this(0, 0)

{

Console.WriteLine("В конструкторе XYCoord()");

}

 

public XYCoord(XYCoord obj): this(obj.x, obj.y)

{

Console.WriteLine("В конструкторе XYCoord(obj)");

}

 

public XYCoord(int i, int j) {

Console.WriteLine("В конструкторе XYCoord(int, int)");

x = i;

y = j;

}

}

 

class OverloadConsDemo

{

static void Main()

{

XYCoord 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)

tl.x, tl.y: 0, 0

t2.x, t2.y: 8, 9

t3.x, t3.y: 8, 9

Код в приведенном выше примере работает следующим образом. Единственным конструктором, фактически инициализирующим поля х и у в классе XYCoord, явля­ется конструктор XYCoord(int,int). А два других конструктора просто вызывают этот конструктор с помощью ключевого слова this. Например, когда создается объект t1, то вызывается его конструктор XYCoord(), что приводит к вызову this(0,0), который в данном случае преобразуется в вызов конструктора XYCoord(0,0). То же самое происходит и при создании объекта t2.

Вызывать перегружаемый конструктор с помощью ключевого слова this полез­но, в частности, потому, что он позволяет исключить ненужное дублирование кода. В приведенном выше примере нет никакой необходимости дублировать во всех трех конструкторах одну и ту же последовательность инициализации, и благодаря при­менению ключевого слова this такое дублирование исключается. Другое преимуще­ство организации подобного вызова перезагружаемого конструктора заключается в возможности создавать конструкторы с задаваемыми "по умолчанию" аргументами, когда эти аргументы не указаны явно. Ниже приведен пример создания еще одного конструктора XYCoord.

public XYCoord(int х): this(х, х) { }

По умолчанию в этом конструкторе для координаты у автоматически устанавли­вается то же значение, что и для координаты у. Конечно, пользоваться такими кон­струкциями с задаваемыми "по умолчанию" аргументами следует благоразумно и осторожно, чтобы не ввести в заблуждение пользователей классов.

Инициализаторы объектов

Инициализаторы объектов предоставляют еще один способ создания объекта и ини­циализации его полей и свойств. (Подробнее о свойствах речь пойдет позднее.) Если используются инициализаторы объектов, то вместо обычного вызова конструктора класса указываются имена полей или свойств, инициализируемых первоначально зада­ваемым значением. Следовательно, синтаксис инициализатора объекта предоставляет альтернативу явному вызову конструктора класса. Синтаксис инициализатора объекта используется главным образом при создании анонимных типов в LINQ-выражениях. (Подробнее об анонимных типах и LINQ-выражениях далее.) Но поскольку ини­циализаторы объектов можно, а иногда и должно использовать в именованном классе, то ниже представлены основные положения об инициализации объектов.

Обратимся сначала к простому примеру.

Листинг 8.23

// Пример, демонстрирующий применение инициализаторов объектов.

 

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-выражениях.

Необязательные аргументы

В версии С# 4.0 внедрено новое средство, повышающее удобство указания аргу­ментов при вызове метода. Это средство называется необязательными аргументами и позволяет определить используемое по умолчанию значение для параметра метода. Данное значение будет использоваться по умолчанию в том случае, если для параме­тра не указан соответствующий аргумент при вызове метода. Следовательно, указывать аргумент для такого параметра не обязательно. Необязательные аргументы позволяют упростить вызов методов, где к некоторым параметрам применяются аргументы, вы­бираемые по умолчанию. Их можно также использовать в качестве "сокращенной" формы перегрузки методов.



Поделиться:


Последнее изменение этой страницы: 2016-12-30; просмотров: 217; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.144.187.103 (0.439 с.)