Объявление и работа с классами
Объявление класса:
class имя_класса
{
// объявление данных
// описание методов
}
Рассмотрим это на простом примере.
namespace Klass1
{
Class kl1
{
int n; // данные класса
int []a;
public kl1(int k1) // конструктор
{
n=k1;
a=new int[k1];
for(int i=0;i<n;i++)
{
Console.Write("Input "+i+" ");
a[i]=Convert.ToInt32(Console.ReadLine());
}
}
~kl1() // деструктор
{
Console.WriteLine("I am the destructor");
}
// методы класса
public void param(out int s)
{
s=0;
for(int i=0;i<n;i++)
s+=a[i];
}
public void val(ref int p)
{
//нахождение суммы элементов, начиная с заданного элемента
int sum=0;
if(p>=n)p=-5;
else{
for(int i=p;i<n;i++)
sum+=a[i];
p= sum;}
}
public void out_a()
{
for(int i=0;i<n;i++)
Console.WriteLine(" Element a("+i+")="+a[i]);
}
public int get_n()
{
return n;
}
}
class Class1
{
static void Main(string[] args)
{
kl1 kk1; //объявление указателя на класс
int l,m;
Console.Write("Input the count of Elem ");
m=Convert.ToInt32(Console.ReadLine());
kk1=new kl1(m); //создание класса
l=kk1.get_n(); // вызов метода класса
Console.WriteLine("Элементов: "+l);
kk1.out_a();
kk1.param(out l);
Console.WriteLine("Сумма = "+l);
l=m-3; // присвоение значения l обязательно
kk1.val(ref l);
Console.WriteLine("Вторая сумма = "+l);
Console.ReadLine();
} } }
Структура класса. Все компоненты класса имеют атрибуты доступа:
private (закрытый): этот атрибут доступа выбирается по умолчанию, к компонентам класса с атрибутом доступа private могут обращаться только методы этого же класса.
protected (защищенный): к компонентам класса с атрибутом доступа protected могут обращаться только методы этого же класса и методы классов - наследников.
public (открытый): к компонентам класса с атрибутом доступа public можно обращаться с любого места.
В отличие от С++ в C# атрибут доступа действует только до знака –разделителя ;После этого будет опять установлен атрибут доступа по умолчанию private.
Наш класс кl1 имеет в качестве данных переменную n и массив a, оба они имеют атрибут доступа private.
Функция, имя которой совпадает с именем класса, является конструктором. Естественно, что конструктор должен иметь атрибут доступа public, иначе невозможно создать экземпляры класса (объекты). Конструктор может иметь формальные параметры по общим правилам, но он не может иметь возвращаемого значения и указывать тип возвращаемого значения запрещено. Допускается наличие в одном классе более одного конструктора, но у них должен быть разный состав формальных параметров. Выбор конструктора в таком случае осуществляется на основе фактических параметров. В конструкторе обычно пишут операции инициализации и ввод исходных данных. Конструкторы, как и другие функции в составе класса могут работать со всеми переменными и массивами своего класса, которые являются как бы глобальными переменными для них. В C# тексты методов пишут прямо в самом классе, сразу за их заголовком. Это не является недостатком и не засоряет определение класса, потому что предусмотрена возможность свертывания методов до одной строки. Обратите на это внимание при работе за компьютером!
Функция, имя которой имеет структуру ~имя_класса() называется деструктором. В C# самому вызвать деструктор невозможно. Деструктор вызывается автоматически программой, называемой «сборщик мусора». Как вы наверно уже заметили, в C# нет операторов освобождения памяти, это выполняет та же программа «сборщик мусора», которая удаляет ставшие ненужными объекты. Деструктор должен обеспечить корректную ликвидацию объекта.
Далее следуют другие функции – члены класса. Их оформление ничем не отличается от рассмотренных в предыдущем параграфе правил работы с функциями. Они могут работать со всеми данными своего класса.
В C# разрешена перегрузка метода: два или более методов в одном классе (в том числе и конструктор) могут иметь одно и то же имя, при условии, что у них разный состав (их количество и/или типы) формальных параметров. Такие методы называют перегруженными. Отличие только в типе возвращаемого значения недостаточно для перегрузки, и такие методы не считаются перегруженными.
Переходим к рассмотрению главной функции Main(). Сначала объявим указатель на класс kl1 kk1; в C# нет явных указателей, но таким образом объявляем именно указатель. Класс будет создан оператором kk1=new kl1(m); это означает запуск конструктора класса и в нашем случае выделение памяти под массив и ввод этого массива. Далее следуют вызовы методов класса традиционным способом. Обратите внимание на использование модификаторов out, ref.
Перегрузка операторов
Цель перегрузки операторов: определение новых правил выполнения существующих операций применительно к созданному пользователем классу. При этом приоритет операций не меняется. Перегрузить можно как бинарные, так и унарные операции, а также операции отношений. Для перегрузки операции необходимо в составе класса задать функцию с новыми правилами выполнения существующей операции; из наиболее известных операторов нельзя перегрузить оператор присваивания (=), а также составные операторы присваивания ( += и т.п.).
Общий формат перегрузки:
public static тип_возвращаемого_значения operator Знак_операции ( операнды )
{
// текст функции перегрузки
}
Пример. Напишем функцию перегрузки, которая будет выполнять операции над одномерным массивом: поэлементное сложение двух массивов, прибавление константы всем элементам массива, увеличение всех элементов массива на единицу, сравнение двух массивов (считаем, что один массив меньше другого, если все его элементы меньше элементов другого массива). В C#, если имеется перегрузка операции <, то должна быть и перегрузка противоположной операции >. Её отсутствие – синтаксическая ошибка. Для простоты – пусть оба массива имеют равное количество элементов.
namespace ConApp5
{
class array
{
int[] a;
int n;
public array(int k)
{
n = k;
a=new int [k];
}
public array(array z)
{
// вспомогательный конструктор, он может быть использован
// только после вызова основного
int m = z.a.Length;
a = new int[m];
}
public void inpt()
{
//ввод массива
for (int i = 0; i < a.Length; i++)
{
Console.Write("a[" + i + "]=");
a[i] = Convert.ToInt32(Console.ReadLine());
}
}
public void outp()
{
// вывод массива
for (int i = 0; i < a.Length; i++)
Console.WriteLine("a[" + i + "]=" + a[i]);
}
public static array operator +(array op1, array op2)
{
// перегрузка операции сложения элементов двух
// одинаковых массивов
array temp = new array(op1.n);
for (int i = 0; i < temp.n; i++)
{
temp.a[i] = op1.a[i] + op2.a[i];
}
return temp;
}
public static array operator +(array op1, int op2)
{
// перегрузка операции добавления константы элементам массива,
// константа на втором месте
array temp = new array(op1.n);
for (int i = 0; i < temp.n; i++)
{
temp.a[i] = op1.a[i] + op2;
}
return temp;
}
public static bool operator <(array op1, array op2)
{
// перегрузка операции сравнения двух массивов (операция >)
bool b1 = true;
for (int i = 0; i < op1.n; i++)
{
if (op1.a[i] > op2.a[i]) b1 = false;
}
return b1;
}
public static bool operator >(array op1, array op2)
{
// перегрузка операции сравнения двух массивов (операция <)
bool b1 = true;
for (int i = 0; i < op1.n; i++)
{
if (op1.a[i] < op2.a[i]) b1 = false;
}
return b1;
}
public static array operator -(array op1, array op2)
{
// перегрузка операции вычитания элементов двух
// одинаковых массивов
array temp = new array(op1.n);
for (int i = 0; i < temp.n; i++)
{
temp.a[i] = op1.a[i] - op2.a[i];
}
return temp;
}
public static array operator ++(array op)
{
// перегрузка операции инкремента
for (int i = 0; i < op.n; i++)
op.a[i]++;
return op;
}
}
class Program
{
static void Main(string[] args)
{
array a1, a2, a3;
bool q1, q2;
a1 = new array(4);
a1.inpt();
a2 = new array(4);
a2.inpt();
a3 = new array(4);
a3 = a1 + a2; //сложение двух массивов
a3.outp();
a1++; // инкремент
a1.outp();
a3 = a1 + 10; // сложение массива и константы
a3.outp();
q1 = a1 < a2; //сравнение
q2 = a1 > a2;
Console.WriteLine("BOOL " + q1 + " " + q2);
Console.ReadLine();
} } }
Обратите внимание, что операции декремента, сложениz константы и массива и многие другие не перегружены и их использование для одномерного массива будет ошибкой! Рекомендуем читателю расширить состав операций над одномерным массивом самостоятельно.
Индексаторы
Индексаторы позволяют организовать доступ к элементам одного массива в составе класса через имя класса как для получения значения элемента массива, так и для его изменения. Определение индексатора:
Тип_данных_элементов массива this [int индекс]
{
get
{
//возврат значения
}
set
{
// присвоение значения
}
}
Индексатор может иметь и атрибут доступа (по умолчанию как всегда private), индексатор public может обращаться и к массивам private.
Пример. Имеем класс Array и в нем массив a
namespace Index_1
{
class Array
{
int []a;
public int len;
public bool err;
public Array(int n)
{ // конструктор
a=new int[n];
len = n;
}
//начинается описание индекса
public int this[int index]
{
get // возвращение значения элемента массива
{
if(ok(index))
{
err=false;
return a[index];
}
else
{
err=true;
return 0;
}
}
Set // присвоение значения элементу массива
{
if(ok(index))
{
a[index]=value;
err=false;
}
else
{
err=true;
}
}
}
bool ok(int index)
{
// вспомогательная функция, проверяет правильность индекса
if((index>=0)&&(index<len))return true;
else return false;
}
}
class Class1
{
static void Main(string[] args)
{
Array c1=new Array(5);
for(int i=0;i<c1.len;i++)
c1[i]=2*i; // работает метод set
for(int i=0;i<c1.len;i++)
Console.WriteLine("Array["+i+"]="+c1[i]);
// работает метод get
Console.ReadLine();
}
}
}
Благодаря индексатору можно писать c1[i] вместо c1.a[i]. Кроме того, индексатор у нас имеет атрибут доступа public; если мы хотим обратиться к массиву без индексатора c1.a[i] то пришлось бы изменить и атрибут доступа массива.
Мы ограничились рассмотрением одномерного индексатора. Можно аналогичным образом ввести и многомерный индексатор (например, для двумерного массива).
В следующем примере класс используется в качестве возвращаемого значения. Напомним, что в качестве возвращаемого значения можно использовать и массив. В принципе, в качестве возвращаемого значения можно использовать класс любой сложности. Ниже приведен пример программы нахождения сумм строк двумерного массива.
namespace Katse
{
class Class1
{
struct mas1
//class mas1
//Работают оба варианта, структура (класс) для представления массива
{
public double []d1;
public mas1(int n)
{
d1=new double[n];
}
public double this[int index]
{ // индексатор
get{return d1[index];}
set{d1[index]=value;}
}
}
static mas1 sum11(double [,]x)
{mas1 my=new mas1(x.GetUpperBound(0)+1);
// создаем экземпляр структуры mas1
for(int i=0;i<=x.GetUpperBound(0);i++)
for(int j=0;j<=x.GetUpperBound(1);j++)
my[i]+=x[i,j];
return my;
}
static void Main(string[] args)
{
double [,]arr;
arr=new double[5,3];
mas1 m1=new mas1(5);// экземпляр структуры для представления данных
for(int i=0;i<5;i++)
{for(int j=0;j<3;j++)
{arr[i,j]=(2+i)*(j+4);
Console.Write("Rida ["+i+","+j+"]="+arr[i,j]+" ");}
Console.WriteLine();}
m1=sum11(arr);
for(int i=0;i<5;i++)
Console.WriteLine("Summa "+i+" on "+m1[i]);
//обращение m1[i] разрешено только благодаря индексатору
Console.ReadLine();
} } }
Свойства
Свойства – это обобщение понятия данных. Значения свойств можно вычислить на основе значений данных, а также через свойство можно присвоить значения данным. Часто свойство вычисляют на базе нескольких или даже целого множества данных (Например, свойство треугольника -периметр - можно вычислить, зная его стороны; свойство сумма элементов массива на основе . . . ). В таком случае, естественно, вопрос о вычислении данных на основе значения свойства бессмысленный, так как эта задача не решается однозначно.
Определение свойства:
Тип_данных_свойства имя_свойства
{
get {
//получение значения свойства
}
set {
// использование переданного значения свойства
} }
Пример на использование свойств.
namespace ConApp7
{
class Prop1
{
double[] mas;
double lim;
public Prop1()
{
int n;
Console.Write("Элементов ? ");
n = Convert.ToInt32(Console.ReadLine());
mas = new double[n];
for (int i = 0; i < n; i++)
{
Console.Write("Mas[" + i + "]=");
mas[i] = Convert.ToDouble(Console.ReadLine());
}
}
double sum()
{ // функция суммирования
double s=0;
for(int i=0;i<mas.Length;i++)
if(mas[i]>lim)s+=mas[i];
return s;
}
public double sum_prop
{ // свойство, значение которой вычисляется через функцию
get { if (sum() > 0) return sum(); else return -25; }
}
public double lim_prop
{ // свойство, представляющее поле данных
get { return lim; }
set { if (value > 0)lim = value; else lim = 0; }
}
}
class Program
{
static void Main(string[] args)
{
Prop1 pr2=new Prop1();
// обращение к свойству для присвоения значения
pr2.lim_prop = 23.5;
// обращения к совйствам для получения значения
Console.WriteLine("Сумма " + pr2.sum_prop);
Console.WriteLine("Граница " + pr2.lim_prop);
Console.ReadLine();
} } }
Возникает вопрос: в каких случаях целесообразно использовать свойства? В чем их преимущество по сравнению с прямым обращением к полям данных или к функциям? При присвоении значений полям через свойства можно выполнять проверку корректности новых значений. К свойствам, значения которых должны вычисляться, обращаться легче по сравнению с обращением к функциям (не надо заботиться об аргументах). Как видно даже из этого простейшего примера, алгоритм вычисления значения свойства может быть записан как в самом свойстве, так и в функции, к которой обращается свойство. Кроме того, методика объектно-ориентированного программирования не рекомендует прямые обращения к полям. Те поля, к которым необходимо прямое обращение и которые, таким образом входят в интерфейс класса, можно представить свойствами.
На использование свойств налагаются довольно серьезные ограничения. Свойству не соответствует поле памяти, оно лишь представляет данные. Поэтому его нельзя передавать методу в качестве ref- или out-параметра. Свойство не должно изменять состояние базовой переменной при вызове get.
Помните, при работе с массивами мы использовали записи mas.Length и mas.GetLength(0). Теперь мы знаем, что первая из них является свойством класса «Массив», а вторая функцией из этого же класса.