Функциональные элементы класса
2.1. Данные: поля и константы
Данные, содержащиеся в классе, могут быть переменными или константами. При описании данных также можно указывать атрибуты и спецификаторы, задающие различные характеристики элементов. Синтаксис описания элемента данных:
[атрибуты] [спецификаторы] [const] тип имя
[ = начальное_значение ]
Возможные спецификаторы для данных представлены в приложении 10.
По умолчанию элементы класса считаются закрытыми private. Для полей класса этот вид доступа является предпочтительным, поскольку поля определяют внутреннее строение класса, которое должно быть скрыто от пользователя. Все методы класса имеют непосредственный доступ к его закрытым полям.
Поля, описанные со спецификатором static, а также константы существуют в единственном экземпляре для всех объектов класса, поэтому к ним обращаются не через имя экземпляра, а через имя класса. Обращение к полю класса выполняется с помощью операции доступа. Справа от точки задается имя поля, слева – имя экземпляра для обычных полей или имя класса для статических.
Пример:
Создание класса Demo и два способа обращения к его полям.
class Circle
{
public int x=0;
public int y=0;
public int radius=3;
public const double pi = 3.14;
public static string name = "Окружность";
double p;
double s;
}
class Program
{
static void Main()
{
Circle cr = new Circle(); //создание экземпляра класса
Console.WriteLine("pi="+Circle.pi); //обращение к константе
Console.Write(Circle.name); //обращение к статическому полю
//обращение к обычным полям
Console.WriteLine(" с центром в точке ({0},{1}) и радиусом {2}", cr.x, cr.y, cr.radius);
// Console.WriteLine(cr.p); - вызовет ошибку, т.к. поле p, c имеют тип private
Console.Write("Введите коэффициент=");
int kof = int.Parse(Console.ReadLine());
cr.x -= kof; cr.y += kof; cr.radius *= kof;
Console.WriteLine(" Новая окружность с центром в точке ({0},{1}) и радиусом {2}", cr.x, cr.y, cr.radius);
//cr.s = 2 * Circle.pi * cr.radius; - вызовет ошибку, т.к. поле s, c имеют тип private
}
}
Методы
Методы находятся в памяти в единственном экземпляре и используются всеми объектами одного класса совместно, поэтому необходимо обеспечить работу методов нестатических экземпляров с полями именно того объекта, для которого они были вызваны. Для этого в любой нестатический метод автоматически передается скрытый параметр this, в котором хранится ссылка на вызвавший функцию экземпляр.
В явном виде параметр this применяется для того, чтобы возвратить из метода ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра метода, например:
class Circle
{
public int x=0, y=0, radius=3;
public const double pi = 3.14;
public static string name = "Окружность";
public Circle T()//метод возвращает ссылку на экземпляр класса
{
return this;
}
public void Set(int x, int y, int r)
{
this.x = x; this.y = y; radius=r;
}
}
class Program
{
static void Main()
{
Circle cr = new Circle(); //создание экземпляра класса
Console.WriteLine("pi="+Circle.pi); //обращение к константе
Console.Write(Circle.name); //обращение к статическому полю
//обращение к обычным полям
Console.WriteLine("с центром в точке ({0},{1}) и радиусом {2}", cr.x, cr.y, cr.radius);
cr.Set(1, 1, 10);
Console.WriteLine("Новая окружность с центром в точке ({0},{1}) и радиусом {2}", cr.x, cr.y, cr.radius);
Circle b=cr.T();//получаем ссылку на объект cr, аналог b=c
Console.WriteLine("Новая ссылка на окружность с центром в точке ({0},{1}) и радиусом {2}", b.x, b.y, b.radius);
}
}
Конструкторы
Конструктор предназначен для инициализации объекта.
Конструктор вызывается автоматически при создании объекта класса с помощью операции new. Имя конструктора совпадает с именем класса.
Основные свойства конструкторов:
1.Конструктор не возвращает значение, даже типа void.
2.Класс может иметь несколько конструкторов с разными параметрами для разных видов инициализации.
3.Если программист не указал ни одного конструктора или какие-то поля не были инициализированы, полям значимых типов присваивается нуль, полям ссылочных типов – значение null.
До сих пор мы задавали начальные значения полей класса при описании класса. Это удобно в том случае, когда для всех экземпляров класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать с помощью явного задания конструктора.
В следующем примере добавлен конструктор и метод Print для вывода информации об объекте:
class Circle
{
public int x, y, radius;
public const double pi = 3.14;
public static string name = "Окружность";
public Circle(int x, int y, int r)//конструктор
{ this.x = x; this.y = y; radius = r; }
public void Print()
{
Console.Write(name);
Console.WriteLine(" с центром в точке ({0},{1}) и радиусом {2}", x, y, radius);
Console.WriteLine();
}
}
class Program
{
static void Main()
{
Circle a = new Circle(0, 0, 1); //вызов конструктора
a.Print();
Circle b=new Circle(10, 10, 5);//вызов конструктора
b.Print();
}
}
Часто бывает удобно задать в классе несколько конструкторов, чтобы обеспечить возможность инициализации объектов разными способами. Для этого конструкторы должны иметь разные сигнатуры.
Рассмотрим это на примере:
class Circle
{
public int x, y, radius;
public const double pi = 3.14;
public static string name = "Окружность";
public Circle(int x, int y, int r)//конструктор 1
{ this.x = x; this.y = y; radius = r; }
public Circle(int r)//конструктор 2
{ radius = r; }
public void Print()
{
Console.Write(name);
Console.WriteLine(" с центром в точке ({0},{1}) и радиусом {2}", x, y, radius);
Console.WriteLine();
}
}
class Program
{
static void Main()
{
Circle a = new Circle(0, 0, 1); //вызов конструктора 1
a.Print();
Circle b=new Circle(5); //вызов конструктора 2
b.Print();
}
}
Обратите внимание на то, что в конструктое 2 не были инициализированы поля x, y, поэтому им присваивается значение 0.
Если один из конструкторов выполняет какие-либо действия, а другой должен делать то же самое плюс еще что-нибудь, то удобно вызвать первый конструктор из второго. Для этого используется уже известное вам ключевое слово this в другом контексте, например:
public Circle(int r) //конструктор 1
{
radius = r;
}
public Circle(int x, int y, int r):this(r) //конструктор 2
{
this.x = x;
this.y = y;
}
Конструкция, находящаяся после двоеточия, называется инициализатором, то есть тем кодом, который исполняется до начала выполнения тела конструктора. В нашем случае конструктор 1 до выполнения своего кода вызывает конструктор 2.
Деструкторы
Существует специальный вид метода, называемый деструктором, который вызывается сборщиком мусора непосредственно перед удалением объекта из памяти.
В деструкторе описываются действия, гарантирующие корректность последующего удаления объекта. Например, проверяется все ли ресурсы, используемые объектом, освобождены.
Синтаксис деструктора:
[атрибуты] [extern] ~имя_класса() {тело_деструктора}
Деструктор не имеет параметров, не возвращает значения и не требует указания спецификаторов доступа. Его имя совпадает с именем класса и предваряется тильдой (~), символизирующей обратные по отношению к конструктору действия.
Пример:
class DemoArray
{
int[] MyArray;//закрытый массив
string name; //закрытое поле
public DemoArray(int size,int x, string name)//конструктор
{
MyArray = new int[size];
this.name = name;
for (int i=0;i<size; ++i) MyArray[i]=x;
}
public void Print ()//метод
{
Console.Write(name+ " : ");
foreach (int a in MyArray) Console.Write(a+" ");
Console.WriteLine();
}
~DemoArray()//деструктор
{
Console.WriteLine("сработал деструктор для объекта "+this.name);
}
}
class Program
{
static void Main()
{
DemoArray a= new DemoArray(5,2, "один"); a.Print();
DemoArray b = new DemoArray(6,1, "два"); b.Print();
a = b;
a.Print();
}
}
В общем случае применение деструкторов замедляет процесс сборки мусора. Поэтому создавать деструкторы следует только тогда, когда необходимо освободить какие-то ресурсы перед удалением объекта.
Свойства
Иногда требуется создать поле, которое с одной стороны, должно быть доступно для использования, с другой стороны, возможность что-то сделать с этим полем имеет ограничения. Например, полю нельзя присваивать произвольное значение, а только значения из какого-то диапазона. Свойство предлагает простой и удобный способ решения этой проблемы.
Синтаксис свойства:
[атрибуты] [спецификаторы] тип имя_свойства
{
[get код_доступа]
[set код_доступа]
}
Значения спецификаторов для свойств и методов аналогичны.
Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке (set) свойства.
Может отсутствовать либо часть get, либо set, но не обе одновременно. Если отсутствует часть set, то свойство доступно только для чтения. Если отсутствует часть get, то свойство доступно только для записи.
Синтаксически чтение и запись свойства выглядят почти как методы. При этом get должен содержать оператор return, возвращающий выражение, для типа которого должно существовать неявное преобразование к типу свойства.
Пример:
class Circle
{
int x, y, radius; //закрытые поля
public static string name = "Окружность";
public Circle(int r) { radius = r; }//конструктор 1
public Circle(int x, int y, int r):this(r) //конструктор 2
{ this.x = x; this.y = y;}
public void Print()
{
Console.Write(name);
Console.WriteLine(" с центром в точке ({0},{1}) и радиусом {2}", x, y, radius);
}
public int X //свойство для обращения к полю x
{
get {return x;}
set {x = value;}
}
public int Y //свойство для обращения к полю y
{
get {return y;}
set {y = value;}
}
public int R //свойство для обращения к полю radius
{
get {return radius;}
set {radius = value;}
}
public double P //свойство только для чтения
{
get {return 2* Math.PI *radius;}
}
public double S //свойство только для чтения
{
get {return Math.PI *radius*radius;}
}
}
class Program
{
static void Main()
{
Circle a = new Circle(0, 0, 1); //вызов конструктора
a.Print();
//установка новых значений
a.X=1; a.Y=1; a.R=10;
//a.S=100; //ошибка – свойство доступно только для чтения
Console.WriteLine("центр = ({0},{1}) радиус = {2} периметр = {3:f2} площадь = {4:f2}", a.X, a.Y, a.R, a.P, a.S);
}
}
Индексаторы
Индексатор представляет собой разновидность свойства и обычно применяется для организации доступа к скрытым полям класса по индексу, например, так же, как мы обращаемся к элементу массива. Синтаксис индексатора аналогичен синтаксису свойства:
[атрибуты] [спецификаторы] тип this [список параметров]
{
[get код_доступа]
[set код_доступа]
}
Спецификаторы аналогичны спецификаторам свойств и методов. Индексаторы чаще всего объявляются со спецификатором public, поскольку они входят в интерфейс объекта. Атрибуты и спецификаторы могут отсутствовать.
Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке (set) значения некоторого элемента класса. Может отсутствовать либо часть get, либо set, но не обе одновременно. Если отсутствует часть set, индексатор доступен только для чтения, если отсутствует часть get, индексатор доступен только для записи.
Список параметров содержит одно или несколько описаний индексов, по которым выполняется доступ к элементу. Чаще всего используется один индекс целого типа.
В качестве примера рассмотрим индексатор, который позволяет получить n-член последовательности Фиббоначи:
class DemoFib
{
public int this[int i]//индексатор,доступный только для чтения
{
get
{
if (i <=0)
throw new Exception("недопустимое значение индекса");
else
if (i==1 || i==2)
return 1;
else
{
int a=1, b=1, c;
for (int j=3; j<=i; ++j){ c=a+b; a=b; b=c; }
return b;
}
}
}
}
class Program
{
static void Main()
{
Console.Write("n=");
int n=int.Parse(Console.ReadLine());
DemoFib a=new DemoFib();
try
{
Console.WriteLine("a[{0}]={1}",n,a[n]);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
Индексаторы очень удобно применять для создания специализированных массивов, на работу с которыми накладываются какие-либо ограничения. Рассмотрим в качестве примера класс-массив, значения элементов которого находятся в диапазоне [0, 100]. Кроме того, при доступе к элементу проверяется, не вышел ли индекс за допустимые границы.
class DemoArray
{
int[] MyArray; //закрытый массив
public DemoArray(int size) //конструктор
{
MyArray = new int[size];
}
public int LengthArray //свойство, возвращающее размерность
{
get { return MyArray.Length; }
}
public int this[int i] //индексатор
{
get
{
if (i <0 || i >= MyArray.Length)
throw new Exception("выход за границы массива");
else
return MyArray[i];
}
set
{
if (i <0 || i >= MyArray.Length)
throw new Exception("выход за границы массива");
else
if (value >= 0 && value <= 100)
MyArray[i] = value;
else
throw new Exception("присваивается недопустимое значение");
}
}
}
class Program
{
static void Main()
{
DemoArray a = new DemoArray(10);
for (int i=0; i<a.LengthArray; i++)
{
// использование индексатора в режиме записи
a[i] = i * i;
// использование индексатора в режиме чтения
Console.Write(a[i]+" ");
}
Console.WriteLine();
try
{
//a[10]=100;
//a[0]=200;
}
catch (Exception e)
{ Console.WriteLine(e.Message); }
}
}
Язык С# допускает использование многомерных индексаторов. Они применяются для работы с многомерными массивами. Рассмотрим на примере предыдущую задачу при условии, что организуется двумерный массив.
class DemoArray
{
int[,] MyArray;//закрытый массив
int n, m;//закрытые поля: размерность массива
public DemoArray(int sizeN, int sizeM)//конструктор
{
MyArray = new int[sizeN, sizeM];
this.n = sizeN; this.m = sizeM;
}
public int LengthN//свойство, возвращающее количество строк
{
get { return n; }
}
public int LengthM//свойство, возвращающее количество строк
{
get { return m; }
}
public int this[int i, int j] //индексатор
{
get
{
if (i < 0 || i >= n || j < 0 || j >= m)
throw new Exception("выход за границы массива");
else
return MyArray[i, j];
}
set
{
if (i < 0 || i >= n || j < 0 || j >= m)
throw new Exception("выход за границы массива");
else
if (value >= 0 && value <= 100)
MyArray[i, j] = value;
else
throw new Exception("присваивается недопустимое значение");
}
}
}
class Program
{
static void Main()
{
DemoArray a = new DemoArray(3, 3);
for (int i = 0; i < a.LengthN; i++,Console.WriteLine())
{
for (int j = 0; j < a.LengthM; j++)
{
a[i,j]=i*j;//использование индексатора в режиме записи
Console.Write(a[i,j]);//использование индексатора в режиме чтения
}
}
Console.WriteLine();
try
{
//Console.WriteLine(a[3,3]);
//a[0,0]=200;
}
catch (Exception e)
{ Console.WriteLine(e.Message); }
}
}
3. «Один класс – один файл»
С добавлением новых классов в программу резко увеличивается ее размер, что затрудняет ее прочтение. Поэтому следует руководствоваться одним простым принципом «один класс – один файл». Для того чтобы создать новый файл для класса Circle выполним следующие действия:
1.В окне Solution Explorer щелкните правой кнопкой на имени проекта Hello (на рисунке 3.2 выделен жирным).
Рисунок 3.2 – Окно программы
2.Выполните команду Add/Add Class…
Далее откроется окно добавления нового элемента как показано на рисунке 3.3.
Рисунок 3.3 – Окно добавления нового элемента
В поле Name напишите Сircle.cs и нажмите кнопку Add.
Теперь окно Solution Explorer выглядит так, как показано на рисунке 3.4
Рисунок 3.4 – Окно Solution Explorer
А файл Circle.cs выглядит следующим образом:
using System;
namespace ConsoleApplication1
{
public class Circle
{
public Circle()
{
//
// TODO: Add constructor logic here
//
}
}
}
3.Замените namespace ConsoleApplication1 на namespace MyProgram, для того чтобы идентификаторы файлов Program.cs и Circle.cs были определены в одном пространстве имен.
4.Перенесите класс Circle из файла Program.cs в файл Circle.cs.
5.Теперь запустите программу и посмотрите, что она делает.\
Контрольные вопросы:
1.Понятие класса. Разделы класса. Примеры описания класса.
2.В чем состоит различие между классом и объектом?
3.Как определяется класс?
4.Собственные копии чего имеет каждый объект?
5.Методы класса. Объявление и определение методов.
6.Какое имя имеет конструктор?
7.Какие функции выполняет оператор new?
8.Что такое «сборка мусора»? Как эта операция выполняется? Что такое деструктор?
9.Объясните предназначение ключевого слона this?
Дата добавления: 2015-10-26; просмотров: 1793;