Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Подробнее о методах и классахСтр 1 из 10Следующая ⇒
Подробнее о методах и классах В данной главе продолжается рассмотрение классов и методов. Оно начинается с пояснения механизма управления доступом к членам класса. А затем обсуждаются такие вопросы, как передача и возврат объектов, перегрузка методов, различные формы метода 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 с.) |