C# (произносится Си-Шарп) - это новый язык программирования от компании Microsoft. Он входит в новую версию Visual Studio - Visual Studio.NET. Кроме C# в Visual Studio.NET входят Visual Basic.NET и Visual C++. Кроме того фирма Borland объявила, что последующие версии C++ Builder и Delphi будут поддерживать платформу .NET (последнее лежит в русле политики Borland - так, например, нынешние версии C++ Builder и Delphi поддерживают, например, такую технологию от Microsoft, как ActiveX).
Одна из причин разработки нового языка компанией Microsoft - это создание компонентно-ориентированного языка для новой платформы .NET. Другие языки были созданы до появления платформы .NET, язык же C# создавался специально под эту платформу и не несет с собой груза совместимости с предыдущими версиями языков. Хотя это не означает, что для новой плятформы это единственный язык.
Еще одна из причин разработки компанией Microsoft нового языка программирования - это создание альтернативы языку Java. Как известно, реализация Java у Microsoft не была лицензионно чистой - Microsoft в присущей ей манере внесла в свою реализацию много чего от себя. Компания Sun, владелица Java, подала на Microsoft в суд, и Microsoft этот суд проиграла. Тогда Microsoft решила вообще отказаться от Java, и создать свой Java-подобный язык, который и получил название C#. Что будет с Java после выхода C# - пока неизвестно. Скорей всего эти языки будут существовать оба, хотя ясно, что одна из целей разработки C# - это противоборство именно с Java (недаром C# называют еще Java-killer'ом).
Если перевести слова NET Runtime на русский язык, то мы получим что-то вроде "Среда выполнения". Именно вы этой среде и выполняется код, получаемый в результате компиляции программы написанной на C#. NET Runtime основын не на ассемблере (т. е. не на коде, родном для процессора), а на некотором промежуточном коде. Отдаленно он напоминает виртуальную Java машину. Только если в случае Java у нас был только один язык для виртуальной машины, то для NET Runtime таких языков может быть несколько. Теоретически программа для среды NET Runtime может выполняться под любой операционной системой, в которой NET Runtime установлена. Но на практике пока единственная платформа для этого - это Windows.
NET Runtime состоит из нескольких частей. Одна из них - это Common Language Runtime. Это, говоря кратко, это некоторый набор стандартов, котрые должны поддерживать все языки платформы .NET. Например, в предыдущих версиях Visual Studio была такая проблема, что разные языки по разному хранили данные одного по идее типа. Так, скажем, тип целого в Visual Basic'е занимал два байта, а в Visual C++ - четыре. А это порождало кучу проблем при совместном использовании языков. Так вот, Common Language Runtime как раз в частности и определяет стандартные для все языков .NET типы данных. И уже есть гарантии, что целый тип в одном языке будет в точности соответствовать одноименному типу в другом.
Еще одна важная часть NET Runtime - это набор базовых классов. Их очень много (порядка несколько тысяч). Кроме того, эти классы относятся не к конкретному языку, а к NET Runtime. Т. е. мы получаем набор классов, общий для всех языков .NET, что достаточно удобно.
Далее. Именно NET Runtime берет на себя некоторые рутинные функции. Например в нем организована сборка мусора. И если раньше программисту приходилось самому освобождать объекты, созданные динамически, то теперь эту задачу берет на себя среда NET Runtime. Еще одно свойство среды NET Runtime - это проверка типов. Означает это вот что. Когда программа выполняется, то в принципе некоторой функции можно подсунуть параметр неправильного типа. Скажем вместо целого подставить действительное число или еще что-нибудь в этом роде. Языки типа C++ свои параметры функций не проверяют, в результате чего записанная переменная большего размера может повредить чужую область памяти и программа может просто рухнуть. Еще классический пример на эту тему - это выход за пределы массива. В NET Runtime же такого невозможно. NET Runtime сама позаботится о проверке типов и других вещах.
Существует несколько языков для NET Runtime. В настоящее время это C#, VB.NET и Visual C++. Кроме того фирма Borland объявила, что ее продукты C++ Builder и Delphi тоже будут поддерживать NET Runtime.
Ну, хватит нам заниматься теорией. Пора переходить к практике. Пишем первую программу на C#. Запускайте Visual Studio.NET. Для создания нового пустого проекта C# нажимаем на кнопку в New Project (самая левая на панели инструментов), или нажимаем комбинацию клавиш Ctrl + Shift + N, или просто заходим в меню File и далее выбираем New и затем Project:
В появившемся окне New Project слева выбираем, естественно, Visual C#, а справа тип приложения - Console Application:
В качестве имени проекта (Name) напечатайте first или что-то в этом роде. Нажмитие на кпопку для закрытия данного диалогового окна.
Теперь приступаем к коду. Наша первая программа просто выведет некоторое фиксированное слово в консольное окошко. Вот ее листинг.
using System;
namespace first
{
///
/// Summary description for Class1.
///
class Class1
{
///
/// The main entry point for the application.
///
[STAThread]
static void Main(string[] args)
{
//
// TODO: Add code to start application here
//
Console.WriteLine("Привет из C#");
}
}
}
Запускаем программу, нажав Ctrl+F5. Результат будет таким:
Разберем текст программы поподробнее. Как вы знаете из предыдущего урока, в .NET Runtime существуют пространства имен. Одно из таких пространств - это System. Вообще-то оно добавляется автоматически в любой проект на C#. Так как мы добавили в нашу программу строчку
... using System; ...
то мы можем вместо длинных имен использовать более короткие. В частности, вместо System.Console можно писать просто Console. Что мы делаем в строчке
...
Console.WriteLine("Привет из C#");
...
Далее мы в нашей программе оъявляем класс Class1. Что такое классы мы посмотрим в последующих уроках, сейчас же достаточно сказать, что в C# не существует глобальных функций, так что нам ничего не остается, как завести сначало класс и затем функцию Main в нем (функция Main обязательно должна быть в каждой программе на C#, и именно с этой функции и начинается выполнение программы. Обратите также внимание, что эта функция пишеться с прописной (большой) буквы. C# различает строчные и прописные буквы, так что это важно). Кроме того, эта функция объявлена с модификатором static. Это означает, что она не относится к конкретному экземпляру класса Class1, а прнадлежит всему классу. В нашей функции Main мы просто выводим на экран некоторую строчку методом WriteLine.
С первой программой на C# все.
Для каждого типа данных C# существует соответствующий тип данных в CRL (Common Language Runtime). Подробности CRL см. в уроке 2. Это, в частности, означает, что каждый тип имеет два названия - полный (из CLR, его можно использовать в любом языке .NET) и сокращенный, который используется в C#. Что за название использовать - это дело вкуса. Но короткие названия во-первых, короче, и во-вторых, как-то привычнее, так что мы в наших уроках будем использовать короткие названия C#. На этом же уроке мы приведем как полные названия (из CLR), так и краткие.
Основные типы данных перечислены в следующей таблице:
| Тип C# | Тип CLR | Размер в байтах | Пояснение |
| int | Int32 | 4 | Целое (со знаком) |
| float | Single | 4 | Вещественное число |
| char | Char | - | Символ (Unicode) |
| bool | Boolean | - | Логический тип |
| short | Int16 | 2 | Короткое целое (со знаком) |
| long | Int64 | 8 | Длинное целое (со знаком) |
| string | String | - | строка |
| byte | Byte | 1 | байт |
| decimal | Decimal | 8 | Вещественное число фиксированной точности |
Если вы предпочитаете использовать длинные имена, то вы должны писать что-то вроде System.Int32 для типа int и т. п. (т. е. приписывать слово System перед CLR-именем). Или же вы должны добавить строчку
using System;
в начале программы (хотя, впрочем, она там скорей всего и так есть).
Таким образом следующие три объявления переменной k равносильны:
int k;
using System; ... Int32 k;
и
System.Int32 k;
Разумеется, аналогично мы имеем дело и другими типами языка C#.
Объявление переменной можно совместить с инициализацией (заданием начального значения):
int z=88;
Набор операторов для C# достаточно стандартен + , -, *, / - действуют как и в любом другом языке. Отметим только, что / (деление) применительно к целым числам дает целую часть от деления. Так, фрагмент
int k=100999, n=1000, s; s=k/n; Console.WriteLine(s.ToString());
выведет на экран 100, а не 101, т. е. никакого округления не происходит.
Есть еще один оператор - %. Это - остаток от деления. Следующий фрагмент выведет на экран 999:
int k=100999, n=1000, s; s=k%n; Console.WriteLine(s.ToString());
Как и в других C-подобных языках, в C# существуют операторы инкремента и декремента. Так, после следующего фрагмента k увеличится на 1, а n - уменьшится на 1:
k++; n--;
Как и в других C-подобных языках, в C# существуют следующие логические операторы:
| Оператор | Описание | Пример |
| && | Логическое И. Результат равен true, только если оба операнда равны true | (x==8) && (y==5) |
| || | Логическое ИЛИ. Результат равен false, только если оба операнда равны false | (y>8) || (y<5) |
| ! | Отрицание. Изменяет логическое значение на противоположное | if(!(a==b))... |
Все эти операторы возвращают результат типа bool.
Обратите внимание, что для логического равно (т. е. для ответа на вопрос "Верно ли, что что-то равно чему-то") используется знак двойного равенства (==). Знак же одинарного равенства (=) используется для присваивания. Для знака == существует парный знак != ("не равно"). Так, приведенный выше пример для оператора ! можно переписать так:
if(!(a==b))...
Отметим, что в C#, в отличии от многих других языков программирования, нельзя вместо false использовать 0, а вместо true - любое ненулевое число. Так, следующий фрагмент содержит ошибку:
int k; ... if(k) //Ошибка! ...
Массивы в C# несколько отличаются от других C-подобных языков. Начнем сразу с примеров. Пример первый:
... int[] k; //k - массив k=new int [3]; //Определяем массив из 3-х целых k[0]=-5; k[1]=4; k[2]=55; //Задаем элементы массива //Выводим третий элемент массива Console.WriteLine(k[2].ToString()); ...
Смысл приведенного фрагмента ясен из комментариев. Обратите внимание на некоторые особенности. Во-первых, массив определяется именно как
int[] k;
а не как один из следующих вариантов:
int k[]; //Неверно!
int k[3]; //Неверно!
int[3] k; //Неверно!
Во-вторых, так как массив представляет из себя ссылочный объект, то для создания массива необходима строка
k=new int [3];
Именно в ней мы и определяем размер массива. Хотя, вообще говоря, возможны конструкции вида
int[] k = new int [3];
Элементы массива можно задавать сразу при объявлении. Вот пример:
int[] k = {-5, 4, 55};
Разумеется, приведенные конструкции применимы не только к типу int и не только к массиву размера 3.
В C#, как и в C/C++, нумерация элементов массива идет с нуля. Таким образом в нашем примере начальный элемент массива - это k[0], а последний - k[2]. Элемента k[3], разумеется, нет.
Теперь переходим к многомерным массивам. Вот так задается двумерный массив:
int[,] k = new int [2,3];
Обратите внимение, что пара квадратных скобок только одна. Естественно, что в нашем примере у массива 6 (=2*3) элементов (k[0,0] - первый, k[1,2] - последний).
Аналогично мы можем задавать многомерные массивы. Вот пример трехмерного массива:
int[,,] k = new int [10,10,10];
А вот так можно сразу инициализировать многомерные массивы:
int[,] k = {{2,-2},{3,-22},{0,4}};
Приведенные выше примеры многомерных массивов называются прямоугольными. Если их представить в виде таблицы (в двумерном случае), то массив будет представлять из себя прямоугольник.
Наряду с прямоугольными массивами существуют так называемые ступенчатые. Вот пример:
//Объявляем 2-мерный ступенчатый массив int[][] k = new int [2][]; //Объявляем 0-й элемент нашего ступенчатого массива //Это опять массив и в нем 3 элемента k[0]=new int[3]; //Объявляем 1-й элемент нашего ступенчатого массива //Это опять массив и в нем 4 элемента k[1]=new int[4]; k[1][3]=22; //записываем 22 в последний элемент массива ...
Обратите внимание, что у ступенчатых массивов мы задаем несколько пар квадратных скобок (столько, сколько размерность у массива). И точно так же мы что-нибудь делаем с элементами массива - записываем, читаем и т. п.
Самая важная и интересная возможность у ступенчатых массивов - это их "непрямоугольность". Так, в приведенном выше примере в первой "строке" массива k три целых числа, а во второй - четыре. Часто это оказывается очень к месту.
If служит для разветвления программы на два направления. Если некоторое условие выполняется, то программа идет в одну сторону, если не выполняется - то в другую. Вот сразу пример, определяющий, четное или нечетное число ввел пользователь:
...
class Class1
{
...
static void Main(string[] args)
{
int k = Int32.Parse(Console.ReadLine());
if(b)
{
Console.WriteLine("Четное число");
}
else
{
Console.WriteLine("Нечетное число");
}
Console.ReadLine();
}
}
Как и в других C-подобных языках, фигурные скобочки можно не писать в случае одного оператора. Также написание веточка else тоже не является необходимым - все зависит от конкретной задачи.
Оператор switch примеряется тогда, когда программа должна разделится более чем на два направления (т. е. будем двигаться или сюда, или сюда, или сюда). Вот пример:
int k = Int32.Parse(Console.ReadLine());
Console.WriteLine(k.ToString());
switch (k){
case 1:
case 2:
Console.WriteLine("Неудовлетворительно");
break;
case 3:
Console.WriteLine("Удовлетворительно");
break;
case 4:
Console.WriteLine("Хорошо");
break;
case 5:
Console.WriteLine("Отлично");
break;
default:
Console.WriteLine("Ошибка");
break;
}
В приведенном примере в зависимости от введенного пользователем числа на экран выводится та или иная оценка. Если число k не лежит в промежутке от 1 до 5, то выполняются операторы в веточке default и выводится надпись "Ошибка". Ветока default не обязательна. Обратите внимание на оператор break. Если его не написать, то будут выполнятся операторы из следующей веточки case до строки с break'ом (т. е. в данном примере если пользователь введет 1 или 2, то программы выведет "Неудовлетворительно"). Обратите внимание, что если в некоторой веточке case или default есть операторы, то написание break обязательно. Так, в следующих двух кусках кода есть ошибки:
...
case 1:
Console.WriteLine("Совсем неудовлетворительно");
//Ошибка! Тут пропушен break
case 2:
Console.WriteLine("Неудовлетворительно");
break;
......
default:
Console.WriteLine("...");
//Ошибка! Тут пропушен break
}
Начнем сразу с примера цикла for:
int k = Int32.Parse(Console.ReadLine());
int sum=0;
for(int i=1; i<=k; i++){
sum+=i;
}
Console.WriteLine(sum);
Этот пример подсчитывает сумму чисел от 1 до введенного пользователем числа k. Сумма записывается в переменную sum и выводится на экран.
Очень часто циклы используются для некоторых действий с массивами. Так как нумерация элементов массива идет с нуля, то типичный цикл будет выглядеть так:
int[] a = {-5, 4, 55};
int sum=0;
for(int i=0; i<3; i++){
sum+=a[i];
}
В этом примере начальное значение для счетчика цикла равно нулю, и в условии продолжения цикла мы пишем знак "меньше", после которого ставится количество элементов в массиве. Разумеется, если в цикле должен выполнится только один оператор, то фигурные скобки можно не писать. Тут все, как в других C/C++-подобных языках.
Теперь рассмотрим пример цикла foreach:
int[] m = {-5, 4, 10};
int sum=0;
foreach(int i in m){
sum+=i;
}
В данном примере мы суммируем все элементы массива m, записывая сумму в sum.
В приведенном примере наш цикл перебирает все элементы массива m. На это нам указывает строка
...
foreach(int i in m){
...
которая интерпретируется так: для каждого целого числа из массива m делам что-то там. Если бы элементами массива были бы не целые, а, скажем, вещественные, то мы записали бы что-то вроде:
...
foreach(float i in m){
...
Т. е. мы пишем именно тип элементов массива. На самом деле foreach используется не только для массивов, но и для других объектов (например, для хэш-таблиц). Но это будет рассмотрено в последующих уроках.
Циклы while бывают двух видов - собственно цикл while и do-while. Оба эти цикла используются, как правило, тогда, когда точно не известно, сколько раз цикл должен выполнится. Например, при вводе пользователем пароля или при подсчете чего-либо с определенной точностью. Оба эти цикла будут выполняться до тех пор, пока условие в круглых скобках после слова while будет истинно. Как только условие станет равным false, выполнение цыкла прекращается. Самое важное отличие между while и do-while в том, что while может не выполниться ни одного раза, тогда как do-while по крайней мере один раз выполнится. Вот примеры их использования:
string password;
do{
password=Console.ReadLine();
}while(password!="wi98zK");int k=0; //Количество попыток
//заводим новую последовательность случайных чисел
Random rnd=new Random(112); //Пишем любой параметр
while(rnd.Next(1, 6)!=5)
{
k++;
};
Console.WriteLine("С "+(k+1).ToString()+"-го раза выпало 5");
В первом примере цикл будет вращаться до тех пор, пока пользователь не введет правильный пароль (wi98zK), во втором - пока некоторое случайное число не окажеться равным 5. При этом если число с самого начала оказалось равным пяти, то цикл вообще выполняться не будет.
Начиная с этого урока, приступаем к изучению классов.
Сначала пару слов о том, что такое классы. Представьте себе, что у вас есть некоторый объект, который характеризуется рядом свойств. Например, работник на некой фирме. У него есть такие свойства, как фамилия, возраст, стаж и т. п. Так вот, в этом случае удобно каждого работника описывать не рядом независимых переменных (строкового типа для фамилии, целого типа для возраста и стажа), а одной переменной типа Worker, внутри которой и содержаться переменные для фамилии, возраста и стажа. Это здесь самое важное - что в переменной типа Worker содержаться другие переменные. Конечно, типа Worker среди встроенных типов данных нет - ну так эта не беда - мы можем ввести его.
Еще одна важная вещь про классы - это то, что в классах помимо переменных разных типов содержатся функции (или, что тоже самое, методы) для работы с этими переменными. Скажем, в нашем примере с классом Worker логично ввесли специальные функции для записи возраста и стажа. Функции будут, в частности, проверять правильность вводимой информации. Например, ясно, что возраст у работника не может быть отрицательным или боольшим 150 ;). Так вот наша функция и будет проверять правильность введеного пользователем возраста.
Давайте рассмотрим первый пример класса. Создайте новое конслольное приложение для C# и введите следующий текст:
using System;
namespace test
{
//Начало класса
class Worker
{
public int age=0;
public string name;
}
//Конец класса
class Test
{
[STAThread]
static void Main(string[] args)
{
Worker wrk1 = new Worker();
wrk1.age=34;
wrk1.name="Иванов";
Console.WriteLine(wrk1.name+", "+wrk1.age);
}
}
}
Запустите программу. Она, как и следовало ожидать, выведет на экран "Иванов, 34".
Давайте кратко обсудим наш код. Во-первых, в строчках
...
class Worker
{
public int age=0;
public string name;
}...
мы оределили наш класс Worker. Внутри этого класса существует две переменные - целая age для возраста и name строкового типа для имени. Обратите внимание, что, в отличие от C/C++, мы можем задавать некоторое начальное значение прямо сразу после объявления переменной:
...
public int age=0;
...
Начальное значение задавать вовсе не обязательно - это видно по переменной name.
Перед переменными мы пишем ключевое слово public. Значение у него такое же, как и в C++ - а именно это означает, что эта переменная (или функция) будет видна вне класса. Если мы не напишем перед переменной никакого модификатора доступа, или напишем private, то пременая не будет видна снаружи класса и ее смогут использовать только фунции этого же класса (т. е. она будет для "внутреннего использования").
Далее в строчке
...
Worker wrk1 = new Worker();
...
мы заводим экземмпляр класса в куче (heap) и возвращаем на него ссылку.
Затем в строчках
...
wrk1.age=34;
wrk1.name="Иванов";
Console.WriteLine(wrk1.name+", "+wrk1.age);
...
мы используем наш класс. А именно присваиваем некоторые значения для возраста и имени и выводим их потом на экран.
С этим уроком все.
Для начала кратко обсудим, что такое конструктор вообще. Конструктор - это функция (метод) класса. Раз эта функция, то описываем мы ее почти точно так же, как и любую другую функцию класса - пишем параметры в круглых скобках и т. п. Когда конструктор вызывается? В момент создания переменной. При этом у класса может быть несколько конструкторов - но при этом они должны различаться или типом параметров, или их количеством.
Этот урок будет основан на предыдущем. Откройте проект, созданный на прошлом уроке. Давайте добавим в наш класс Worker конструктор:
...
class Worker
{
public int age=3;
public string name;
//Конструктор 1
public Worker(int age, string name)
{
this.age=age;
this.name=name;
}
}
...
Обратите внимание на ряд особенностей конструктора. Во-первых, конструктор называется как класс. Раз наш класс называется Worker, значит и конструктор должен называться точно также. И во-вторых, конструктор, в отличие от других методов класса, не возвращает никакого значения (даже типа void). Если вы знакомы с языком C++, то, несомненно, это вам все знакомо.
Что делает наш конструктор? Он записывает передаваемые в него параметры во внутренние переменные класса. Обратите внимание, что называются они одинаково - age и age, name и name. Компилятор сначала ищет локальную переменную с таким именем, и, если не находит, то переменную класса. Поэтому age (и name) - это передаваемый в конструктор параметр. Если же нам надо сослаться на переменную класса (при существавании переменной с таким же именем, как и передаваемый в функцию параметр), то мы используем ключевое слово this. Оно всегда указывает на текущий экземпляр нашего класса. Таким образом в строчках
...
this.age=age;
this.name=name;
...
мы передаем параметры конструктора в переменые класса.
Теперь изменим тестовый класс test следующим образом:
...
static void Main(string[] args)
{
//Вызываем конструктор
Worker wrk1 = new Worker(40, "Вася");
Console.WriteLine(wrk1.name+", "+wrk1.age);
}
...
Теперь после объявления переменной мы вызываем конструктор с двумя параметрами, который запишет в переменные age и name экземпляра класса Worker значания 40 и "Вася". Строчкой ниже эти значения выводятся на экран.
Представьте себе, что у нас есть некоторый класс (быть может весьма большой), который почти подходит для нашей конкретной задачи. Почти, но не совсем. Что-то (некоторые методы) в этом классе надо изменить, что-то - добавить. Наследование как раз и служит для этого. При наследовании мы объявляем наш класс потомком другого класса. И наш класс-потомок (или, как его еще называют, дочерний класс) автоматически приобретает всё, что было в родительском классе. Это мы делаем буквально парой строчек. Затем в дочерний класс мы можем что-нибудь добавить (чего не было в классе родительском). Кроме того, в дочернем классе мы можем что-нибудь из родительского класса переопределить - так что оно уже будет работать по-другому.
Давайте рассмотрим это все на примере. У нас будет класс Worker и производный от него класс Boss. Класс Boss будет отличаться от класса Worker во-первых, наличием переменной numOfWorkers (для количества подчиненных) и, во вторых, метод для задания возраста (setAge) будет работать в Boss не так:
using System;
namespace test
{
//Класс Worker
class Worker
{
protected int age=0;
public void setAge(int age)
{
if(age>0 && age<100)
this.age=age;
else
this.age=0;
}
public int getAge()
{
return age;
}
}
//Класс Boss
class Boss : Worker{
public int numOfWorkers; //Количество подчиненных
public new void setAge(int age)
{
if(age>0 && age<45)
this.age=age;
else
this.age=0;
}
}
class Test
{
static void Main(string[] args)
{
Worker wrk1 = new Worker();
Boss boss = new Boss();
wrk1.setAge(50);
boss.setAge(50);
boss.numOfWorkers=4;
Console.WriteLine("Возраст работника "+wrk1.getAge());
Console.WriteLine("Возраст босса "+boss.getAge()+
".\nКоличество подчиненных "+boss.numOfWorkers);
}
}
}
Обратите внимание, как мы вводим новый класс Boss:
...
class Boss : Worker{
...
Такая запись и означает, что новый класс Boss будет потомком другого класса (Worker в данном случае). Так как наш класс - потомок другого класса, то он автоматически умеет все тоже самое, что и родительский класс. В частности, в нем есть переменная age для хранения возраста. Далее, в класс Boss мы вводим переменную numOfWorkers для хранения количество подчиненных у нашего босса. В родительском классе такой переменной не было. Кроме того, нас не устроила реализация метода setAge из родительского класса Worker (по каким-то там внутренним инструкциям фирмы возраст босса не может превышать 45 лет, тогда как возраст работника не может превышать 100), поэтому мы просто написали в классе Boss метод с таким же именем. Обратите внимание на слово new при объявлении метода:
...
public new void setAge(int age)
...
Таким образом мы в производном классе можем что-нибудь добавлять и что-нибудь изменять по отношению к классу родительскому. Обратите внимание, что убирать что-нибудь, полученное в наследство от родительского класса, мы не можем.
При запуске наша программа даст вполне ожидаемый результат - в возраст работника запишется 50, в возраст босса - 0.
В переменной типа базового класса можно хранить и переменные производных класов. Например, текст метода Main из класса Test прошлого урока можно переписать следующим образом:
...
static void Main(string[] args)
{
Worker wrk1 = new Boss();
...
Здесь мы объявили переменную wrk1 типа Worker. Вот другой пример на эту же тему:
Worker wrk1;
Boss boss = new Boss();
wrk1=boss;
...
Здесь мы опять записываем в переменную wrk1 (типа Worker) переменную boss (типа Boss).
Но, несмотря на то, что мы записываем в переменную wrk1 или с помощью оператора new или посредством присваивания экземпляр производного класса, все равно методы производного класса мы использовать не можем (на самом деле из производного класса мы не сможем использовать не только методы, но и переменные)!
Например, в следущие строки выведут на экран 50, а не ноль:
wrk1.setAge(50); //Вызывается метод класса Worker, а не класса Boss
Console.WriteLine("Возраст работника " + wrk1.getAge());
Как с этим бороться, мы рассмотрим на следующем уроке, а пока поясним, почему это может оказаться важным. Например, в вашей фирме есть работники разных типов - программисты, директора, бухгалтера и т. п. Так как у все категорий работников есть что-общее, что вы можете создать базовый класс Worker и производные от него специализированные классы. Далее вы можете в вашей программе объявить массив типа Worker. Так вот, в этом массиве можно будет хранить всех работников (т. е. экземпляры всех классов, производных от класса Worker). И это удобно - одним циклом вы сможете начислить всем зарплату и т. п.
Иногда для некоторый класс играет чисто вспомогательную роль для другого класса и используется только внутри него. В этом случае логично описать его внутри существующего класса. Вот пример такого описания:
using System;
namespace test
{
class ClassA
{
//Вложенный класс
private class ClassB
{
public int z;
}
//Переменная типа вложенного класса
private ClassB w;
//Конструктор
public ClassA()
{
w=new ClassB();
w.z=35;
}
//Некоторый метод
public int SomeMethod()
{
return w.z;
}
}
class Test
{
static void Main(string[] args)
{
ClassA v=new ClassA();
int k=v.SomeMethod();
Console.WriteLine(k);
}
}
}
Здесь класс ClassB объявлен внутри класса ClassA. Объявлен он со словом private, так что его экземпляры мы можем создавать только внутри класса ClassA (что мы и делаем в конструкторе класса ClassA). Методы класса ClassA имеют доступ к экземпляру класса ClassB (как, например, метод SomeMethod).
Вложенный класс имеет смысл использовать тогда, когда его экземпляр используется только в определенном классе. Кроме того, при использовании вложеных классов улучшается читаемость кода - если нас не интересует устройство основного класса, то разбирать работу вложенного класса нет необходимости.
Обратите также внимание, как вложенные классы показываются на вкладке ClassView:
Перегрузка операторов - это задание для известных операторов (например, для +) новых значений. Например, мы можем перегрузить оператор + для векторов так, чтобы стало возможным использование в нашей программе конструкции вида:
... Vector a, b, c; ... c = a + b; ...
Здесь Vector - это введеный нами класс. Если мы не сделаем перегрузку операторов, то мы не сможем использовать знак "плюс" для экземпляров нашего класса Vector. Вот как это можно сделать для бинарного (т. е. имеющего два параметра) оператора:
using System;
namespace test
{
class Vector
{
float x, y; //Координаты
//Конструктор
public Vector(float x, float y)
{
this.x=x;
this.y=y;
}
//Чтение x-координаты
public float GetX()
{
return x;
}
//Чтение y-координаты
public float GetY()
{
return y;
}
//Перегрузка оператора +
public static Vector operator +(Vector w, Vector v)
{
Vector res = new Vector(w.x + v.x, w.y + v.y);
return res;
}
}
class Test
{
static void Main(string[] args)
{
Vector a=new Vector(1, 2), b=new Vector(2, -1), c;
c = a + b;
Console.WriteLine("x=" + c.GetX() + ", "
+ "y=" + c.GetY());
}
}
}
Код для нашего класса приведен полностью, хотя перегрузка оператора занимает всего несколько строк. Вот они:
...
public static Vector operator +(Vector w, Vector v)
{
Vector res = new Vector(w.x + v.x, w.y + v.y);
return res;
}
...
Синтаксис здесь такой - если мы перегружаем оператор op, то мы должны в качестве имени оператора написать operator op (в нашем случае мы перегружаем +, поэтому пишем operator +). Далее, раз это метод, то мы пишем в круглых скобках его параметры. Для бинарного оператора их два (в отдичие от C++, где пареметр - это только правый операнд, левый же передается неявно - через указатель this). У нас они оба имеют тип Vector. Перед operator op пишем тип возвращаемого значения. Так как сумма двух векторов - это опять вектор, то перед именем нашего метода пишем Vector. Модификатор доступа public понятен - без него мы бы не смогли использовать перегруженный оператор вне класса. А вот слово static важно - в C# перегруженные операторы идут как static. Не забывайте его писать.
Внутри перегруженного оператора мы заводим переменную res типа Vector для возврата через нее результата. Ее мы определяем через конструктор с двумя параметрами, который мы ранее написали для нашего класса. Сложение двух векторов происходит так, как и должно происходить - покоординатно.
В классе test мы испытываем наш перегруженный оператор. Все должно работать как и положено, и результат сложения должен вывестись на консоль в виде "x=3, y=1".
Унарные операторы - это те, которые имеют только один операнд (параметр). Типичные примеры унарных операторов - это унарный минус (меняющий знак на противоположный) и оператор инкремента ++.
Рассмотрим перегрузку унарных операторов на примере рассмотренного на прошлом уроке класса Vector. А именно, добавим в наш класс унарный минус, который будет менять вектор с координатами x и y на вектор с координатами -x и -y. Для этого добавьте в наш класс Vector следующие строки:
public static Vector operator -(Vector v)
{
Vector res = new Vector(-v.x, -v.y);
return res;
}
Обратите внимание, что при перегрезке унарных операторов (как, впрочем, и преперегрузке бинарных), мы должны поставить модификатор static.
Для испытания перегруженного оператора изменить класс Test приблизительно следующим образом:
class Test
{
static void Main(string[] args)
{
Vector a=new Vector(1, 2), b;
b = -a;
Console.WriteLine("x=" + b.GetX() + ", "
+ "y=" + b.GetY());
}
}
Как и следовало ожидать, результатом выполнения программы будет вывод на экран строки "x=-1, y=-2".
Не надо думать, что при перегрузке операторов тип возвращаемого значения всегда совпадает с типом класса (для которого происходит перегрузка). Это зависит от конкретной задачи. Вот пример перегрузки оператора !, который возвратит модуль вектора:
public static double operator !(Vector v)
{
double res = Math.Sqrt(v.x*v.x + v.y*v.y);
return res;
}
Здесь мы воспользовались методом Sqrt класса Math для возврата квадратного корня. Sqrt - это статический метод класса Math, поэтому мы вызываем его не для конкретного экземпляра класса, а для самого класса. О статических методах мы поговорим позже, пока же измените тестовый класс Test следующим образом:
class Test
{
static void Main(string[] args)
{
Vector a=new Vector(3, 4);
Console.WriteLine(!a);
}
}
Наша программа должны вывести 5 (длину вектора (3, 4)).
Переменные существуют только в своей области видимости, при выходе из которой переменная "умирает". Область видимости переменной в первом приближении начинается в строке, где переменная объявлена и кончается на закрывающей фигурной скобке (переменная должна быть объявлена между этой скобкой и парной к ней открывающей). Вот поясняющий пример:
int k=3;
//Все OK, переменая k видна
Console.WriteLine(k);
Тут значение переменной k доступно и выведется на экран.
...
{
int k=3;
}
//Переменая k не видна
Console.WriteLine(k); //Ошибка!
В этом же примере вывести k на экран не получится - она умерла на закрывающей фигурной скобке.
Во вложенных областях видимости мы, как правило, не можем объявлять переменные с одинаковыми именами:
int k=4;
{
//Ошибка!
int k=3;
}
Исключение составляют параметры в методах класса:
class Vector
{
float x, y; //Координаты
...
public Vector(float x, float y)
{
this.x=x;
this.y=y;
}
Здесь мы имеем две пары одноименных переменных - x в параметре конструктора и x как переменная класса. Так как для уточнения переменной класса мы можем использовать this, то ошибки не будет.
Как показано в уроке 13 переменная базового класса может использоваться для хранения переменних производных классов. Но при этом мы не сможем использовать методы из производного класса. Сайчас мы с вами и посмотрим, как эту неприятность можно обойти. Для этого как раз и служит механизм виртуальных функций.
Если в родительском классе некоторая функция объявлена как виртуальная, то в производном классе ее можно переопределить. В этом, собственно говоря, ничего нового нет - этомы могли делать и без всяких виртуальных функций. Новое заключается в том, что если мы запишем в переменную типа родительского класса экземпляр проиводного, то для такого экземпляра мы сможем вызывать переопределенную функцию производного класса. Вот пример, поясняющий это:
using System;
namespace test
{
//Класс Worker
class Worker
{
protected int age=0;
virtual public void setAge(int age)
{
if(age>0 && age<100)
this.age=age;
else
this.age=0;
}
public int getAge()
{
return age;
}
}
//Класс Boss
class Boss : Worker
{
public int numOfWorkers; //Количество подчиненных
override public void setAge(int age)
{
if(age>0 && age<45)
this.age=age;
else
this.age=0;
}
}
class Test
{
static void Main(string[] args)
{
Worker boss = new Boss();
boss.setAge(50);
Console.WriteLine("Возраст босса "+boss.getAge());
}
}
}
Как вы видите, тут функцию setAge в родительском классе Worker мы определили с ключевым словом virtual, а одноименную функцию в производном классе Boss - с ключевым словом ovеrride.
Обратите внимание на то, что из какого конкретно класса вызывается функция (из родительского или производного) определяется на этапе выполнения программы, а не на этапе компиляции. В принципе в переменную родительского типа мы могли бы записать экземпляр именно родительского класса. В этом случае, естественно, вызвалась бы функция родительского класса. Вот поясняющий это утверждение пример:
class Test
{
static void Main(string[] args)
{
Worker boss;
bool b;
//Присваиваем значение в переменную b
...
if(b)
{
//В переменной boss - экземпляр класса Boss
boss=new Boss();
}
else
{
//В переменной boss - экземпляр класса Worker
boss=new Worker();
}
//Вызываем метод класса Boss или Worker
boss.setAge(50);
Console.WriteLine("Возраст "+boss.getAge());
}
}
С этим уроким все!
Методы класса могут быть объявлены как абстрактные. Это означает, что в этом классе нет реализации этих методов. Абстрактные методы пишутся с модификатором abstract. Класс, в котором есть хотя бы один абстрактный метод, называется абстрактным (а таком классе могу быть и обычные методы). Нельзя создавать экземпляры абстрактного класса - такой класс может использоваться только в качестве базового класса для других классов. Для потомка такого класса есть две возможности - или он реализует все абстрактные методы базового класса (и в этом случае для такого класса-потомка мы сможем создавать его экземпляры), или он реализует не все абстрактные методы базового класса (в этом случае он является тоже абстрактным классом и единственная возможность его использования - это производить от него классы-потомки). Вот пример, иллюстрирующий использование абстрактных классов:
using System;
namespace test
{
abstract class Figure
{
//Площадь фигуры
public abstract double square();
public abstract double perimeter();
}
class Triangle: Figure
{
double a, b, c; //Стороны
//Конструктор
public Triangle(double a, double b, double c)
{
this.a=a;
this.b=b;
this.c=c;
}
public override double square()
{
//Используем формулу Герона
double p = (a+b+c)/2;
return Math.Sqrt(p*(p-a)*(p-b)*(p-c));
}
public override double perimeter()
{
return a+b+c;
}
}
class Rectangle: Figure
{
double a, b; //Стороны
//Конструктор
public Rectangle(double a, double b)
{
this.a=a;
this.b=b;
}
public override double square()
{
return a*b;
}
public override double perimeter()
{
return (a+b)*2;
}
}
class Test
{
public static void Main()
{
Figure f1, f2;
f1=new Triangle(3, 4, 5);
f2=new Rectangle(2, 6);
System.Console.WriteLine(f1.perimeter()+", "+ f1.square());
System.Console.WriteLine(f2.perimeter()+", "+ f2.square());
}
}
}
Тут мы объявляем абстрактный класс Figure, от которого производим два класса - Rectangle (класс прямоугольника) и Triangle (треугольника). В классе Figure есть два абстрактных метода - square (для подсчета площади) и perimeter (для периметра). Так как для призвольной фигуры формул для площади и для периметра не существует, то эти методы объявлены в классе Figure и переопределены в производных классах (с ключевым словом override). Далее в классе Test мы проводим испытание - заводим две переменные типа ссылка на базовый класс Figure, ниже в эти ссылки мы записываем созданные экземпляры производных классов Triangle и Rectangle. Обратите внимание, что ссылку на абстрактный класс мы создать можем, а экземпляр - нет. Далее мы выводим на экран периметр и площадь для наших фигур.
На прошлом уроке мы рассмотрели абстрактные классы. Их основное назначение - быть предками для других классов. На другом конце иерархии стоят так называемые sealed-классы. От них нельзя производить другие классы. Синтаксис для тах такой же, как и для обычных классов, за исключением ключевого слова sealed. Вот пример:
...
sealed class MyClass
{
//Методы и переменные класса
int x;
...
}
class MyClass2: MyClass //Ошибка!
{
...
}
...
Как вы видите, от sealed-класса MyClass нельзя делать классы-потомки.
Обычно переменные класса принадлежат конкретному экземпляру класса. Для обращения к таким переменным мы пишем что-то вроде
MyClass z; k=z.data;
Здесь переменная data принадлежит классу MyClass. Для каждого экземпляра класса она своя.
Наряду с обычными переменными существуют и статические переменые класса. Их основное отличие от обычных переменных в том, что они относятся к классу целиком. Т. е. на все экземпляры класса у нас есть только одна переменая. Получить доступ к такой переменой можно только для самого класса без создания его экземпляра. Через конкретный же экземпляр класса доступа к такой перемнной нет. Т. е. использовать такую статическую переменную можно только для самого класса без создания экземпляра.
Для объявления переменной статический надо использовать ключевое слово static. Вот пример:
class MyClass
{
//Статическая переменная
public static int data;
//Конструктор
public MyClass()
{
data++;
}
}
class Test
{
public static void Main()
{
MyClass a=new MyClass();
MyClass b=new MyClass();
System.Console.WriteLine(MyClass.data);
}
}
Указанный фрагмент выведет на экран, естественно, 2 (переменая data увеличится в кждом из двух конструкторов).
На предыдущем уроке мы рассмотрели статические переменные класса. При этом наша переменная data была объявлена как public. Это не слишком хорошо - переменные класса лучше прятать от посторонних глаз. Но, так как доступ к нашей переменной нам все-таки нужен, то мы добавим в наш класс функции для чтения и записи переменной data. Вот новый вариант нашего класса:
class MyClass
{
//Статическая переменная
static int data;
//Статический метод для чтения переменной
public static int GetData()
{
return data;
}
//Статический метод для записи переменной
public static void SetData(int newData)
{
data=newData;
}
//Конструктор
public MyClass()
{
data++;
}
}
class Test
{
public static void Main()
{
MyClass a=new MyClass();
MyClass b=new MyClass();
//Вызов статического метода GetData
System.Console.WriteLine(MyClass.GetData());
//Вызов статического метода SetData
MyClass.SetData(5);
System.Console.WriteLine(MyClass.GetData());
}
}
Указанный фрагмент выведет, разумеется, 2 и 5.
Обратите внимание, что к статическим переменным и методам мы всегда получаем доступ через имя класса, а к нестатическим - через экземпляр класса. Но это не значит, что мы не имеем доступа к статическим переменным класса из нестатических методов класса - имеем, и даже у нас есть пример этому - конструктор нашего класса MyClass.
Статическими к классе могут быть и конструкторы. Если обычный конструктор вызывается в момент создания экземпляра класса, то статический - в самом начале, т. е. перед созданием первого экземпляря класса.
В статическом конструкторе можно задать начальные значения для статических переменных класса. Вот пример:
class MyClass
{
public static int data;
//Статический конструктор
static MyClass()
{
data=10;
}
//Конструктор
public MyClass()
{
data++;
}
}
class Test
{
public static void Main()
{
MyClass a=new MyClass();
MyClass b=new MyClass();
System.Console.WriteLine(MyClass.data);
}
}
Указанный фрагмент выведет на экран 12. Действительно, сначала вызовется статический конструктор, который присвоит значение 10 перемнной data. Затем для каждого созданного экземпляра класса MyClass вызовется обычный конструктор, к котором переменная data увеличится каждый раз на 1.
Обратите внимание, что оба конструктора без параметра. Противоречия тут нет, так как один из них - статический, а другой - нет.
В C# нет глобальных переменных, функций или констант. Все они обязательно должны принадлежать некоторому классу. И если у вас имеется класс, содержащий в себе, например, некоторые нужные вам константы, то экземпляры такого класса вам не нужны (так как вы создали этот класс исключительно как хранилище). Чтобы запретить создание экземпляров класса, вы объявляете конструктор в нем как закрытый (то есть с модификатором private или вовсе без модификатора - так как отсутствие модификатора и подразумевает private). Это означает, что создать экземпляр такого класса не удастся (что нам и надо).
Вот пример такого класса:
namespace test
{
//Класс без экземпляра
class MyClass
{
//Закрытый конструктор
private MyClass()
{
}
public static int MyPhoneNumber=1239999;
}
class Test
{
public static void Main()
{
System.Console.WriteLine(MyClass.MyPhoneNumber);
}
}
}
Тут у нас класс MyClass имеет закрытый конструктор, так что создавать его экземпляры мы не можем. Так, следующий код будет ошибочным:
class Test
{
public static void Main()
{
MyClass a=new MyClass(); //ошибка!!!
}
}
Использовать же статические переменные и константы такого класса мы можем (так, мы выводим на экран константу MyPhoneNumber).
Разумеется, в таком классе может содержать и методы:
class MyClass
{
//Закрытый конструктор
private MyClass()
{
}
public static int MyPhoneNumber=1239999;
public static int SomeNumber=1;
public static int GetSomeNumber(){
SomeNumber++;
return SomeNumber;
}
}
class Test
{
public static void Main()
{
System.Console.WriteLine(MyClass.GetSomeNumber()); //Выведется 2
System.Console.WriteLine(MyClass.GetSomeNumber()); //Выведется 3
}
}
Указанный фрагмент выведет 2 и 3.
В функцию можно передавать заранее неизвестное число параметров. Например, функция может складывать некоторое заранее неизвестное количество чисел и возвращать их сумму.
Вот конкретный пример реализации такой функции:
using System;
class MyClass{
public static int Sum(params int[] args)
{
int res=0;
for(int i=0; i<args.GetLength(0); i++){
res+=args[i];
}
return res;
}
}
namespace test
{
class Test
{
public static void Main()
{
System.Console.WriteLine(MyClass.Sum(1, 3));
System.Console.WriteLine(MyClass.Sum(12));
System.Console.WriteLine(MyClass.Sum(-1, 9, 2));
}
}
}
Здесь в классе MyClass мы объявляем статческую фунцкию Sum, в которую мы можем передавать переменное число параметров. Для этого в круглых скобках после имени функции мы пишем конструкцию
... public static int Sum(params int[] args) ...
Синтаксис у нее такой - сначала пишем ключевое слово params, затем - тип параметров (int в данном случае), после которого ставим пустые квадратные скобки и в конце этой конструкции пишем произвольное имя параметра (в нашем примере он назван args).
В классе test мы испытываем наш класс MyClass, а именно, вызываем функцию Sum с разным числом параметров. Все работает как надо и наша программа покажет на экране 4, 12 и 10.
Строки в C# - это экземпляры класса System.String. Вообще говоря в C# есть тип string, но класс System.String яляется более продвинутым, так что его использование часто оказывается более оправданным и простым. Этот класс имеет множество методов и свойств, некоторые из которых перечислены ниже:
Свойство Length. Возвращает длину строки. Пример использования:
String s="qqq";
int k=s.Length; //В k запишется 3
Compare. Статический метод, сравнивающий две строки. Возвращает 0, есть строки равны, отрицательное значение, если первая строка меньше второй и положительное значение, если первая строки больше второй (больше и меньше в алфавитном смысле, разумеется). Пример использования:
namespace test
{
class Test
{
public static void Main()
{
String s1="arbour", s2="ace", s3="azote";
System.Console.WriteLine(String.Compare(s1, s1)); //Выдаст 0, т. к. "arbour" равно "arbour".
System.Console.WriteLine(String.Compare(s1, s2)); //Выдаст -1, т. к. "arbour" меньше "ace".
System.Console.WriteLine(String.Compare(s1, s3)); //Выдаст 1, т. к. "arbour" больше "azote".
}
}
}
Equals. Метод, возвращает true, если строки равны, false - если не равны. Может быть как статическим, так и не статическим. Пример использования:
String s1="qqq", s2="www";
System.Console.WriteLine(String.Equals(s1, s2).ToString());
//Статический метод
System.Console.WriteLine(System.Console.WriteLine(String.Equals(s1, s2).ToString());
//Не статический метод
System.Console.WriteLine(s1.Equals(s2).ToString());.ToString());
Метод Substring. Поволяет извлечь из строки подстроку. Пример использования:
String s1="abcdefg", s2;
s2=s1.Substring(3, 2);
System.Console.WriteLine(s2); //Напечатается "de"
Параметры тут такие: первый - с какого места извлекаем (нумерация с нуля) и второй - сколько символов извлекаем.
Метод Insert. Вставляет в строку другую строку. Пример использования:
String s1="abcdefg", s2;
s2=s1.Insert(1, "xyz");
System.Console.WriteLine(s2); //Напечатается "axyzbcdefg"
Первый параметр тут - это куда вставляем (нумерация, как всегда, с нуля), второй - что за строчку вставляем.
Метод IndexOf. Позволяет найти в строке подстроку. Пример использования:
String s1="abcabcab", s2="bc", s3="zzz";
System.Console.WriteLine(s1.IndexOf(s2)); //Напечатается 1
System.Console.WriteLine(s1.IndexOf(s3)); //Напечатается -1
Этот метод возвращает номер позиции, на котором в строке находится передаваемая в качестве параметра подстрока. Если такой построки нет, то возвращается -1.
Метод Replace. Производит замену в строке. Пример использования:
String s1="abcabcab", s2="bc", s3;
s3=s1.Replace(s2, "q");
System.Console.WriteLine(s3); //Напечатается aqaqab
Методы EndWith и StartsWith. Проверяют, не завершается ли или не начинается ли строка с или на заданную строку соответственно. Пример использования:
String s1="arbour";
if(s1.StartsWith("ar"))
System.Console.WriteLine("Строка начинается на \"ar\"");
else
System.Console.WriteLine("Строка не начинается на \"ar\"");
Методы ToUpper и ToLower переводят строку в верхний или нижний регистр соответственно. Пример использования:
String s1="aRbRur";
s1=s1.ToLower();
Методы Trim, TrimEnds и TrimStart. Удаляют пробельные символы из начала и конца строки (Trim), только с конца строки (TrimEnds) и только с начала строки (TrimStart). Пример использваония:
String s1=" ar brur ";
System.Console.Write(s1.Trim());
Заметьте, что при изменении строки фактически старый экземпляр класса System.String уничтожается, и создается новый с там же именем и измененным содержанием. Это означает, что при интенсивной работе со строками программа может работеть не так быстро, как хотелось бы. Если мы не хотим, что бы каждый раз создавался новый экземпляр класса, то вместо класса System.String надо использовать класс StringBuilder, котрый мы рассмотрим на следующем уроке.
На этом уроке мы рассмотрим другой класс для работы со строками - класс StringBuilder. Он принадлежит пространству имен System.Text
Этот класс работет быстрее, чем класс String, так как при изменении строки, созданной как экземпляр класса String, у нас создается каждый раз новый экземпляр класса, а старый уничтожается, при использовании же класса StringBuilder расходов на создание-уничтожение экземпляра класса нет - мы работаем всегда с одним экземпляром.
Обратите внимание, что для этого класса мы не можем использовать простое присваивание:
StringBuilder s="abc"; //Неправильно!
В этом случае мы долны действовать так:
StringBuilder s=new StringBuilder("abc"); //Правильно
У класса StringBuilder нет статических методов. Все его методы - динамические. Ниже перечислены основные свойства и методы класса StringBuilder:
Свойство Length. Возвращает длину строки. Пример использования:
int k=s.Length;
Свойство только для чтения MaxCapacity. Дает максимальное количество символов, которые можно записать в объект типа StringBuilder. Пример использования:
System.Console.WriteLine(s.MaxCapacity);
Метод Append. Прибавляет строку к существующей. Пример использования:
StringBuilder s1=new StringBuilder("Cogito ");
StringBuilder s2=new StringBuilder("ergo ");
s1.Append(s2);
s1.Append("sum");
System.Console.WriteLine(s1); //Напечатается "Cogito ergo sum"
Метод Equals. Служит для сравнения двух строк. Возвращает true или false. Пример использования:
if(s1.Equals(s2))
System.Console.WriteLine("Строки равны");
else
System.Console.WriteLine("Строки не равны");
Метод Insert. Вставляет символы в заданную позицию (Нумерация идет с нуля). Пример использования:
StringBuilder s1=new StringBuilder("abcde");
s1.Insert(2, "xyz");
System.Console.WriteLine(s1); //Напечатается "abxyzcde"
Метод Remove. Удаляет символы из строки. Пример использования:
StringBuilder s1=new StringBuilder("abcde");
s1.Remove(1, 2);
System.Console.WriteLine(s1); //Напечатается "ade"
Певый параметр у Remove - это с какой позиции удаляем (нумерация с нуля), второй - сколько символов удаляем.
Метод Replace. Заменяет символы. Пример использования:
StringBuilder s=new StringBuilder("abcdeabcde");
s.Replace("abc", "ZZZ");
System.Console.WriteLine(s); //Напечатается "ZZZdeZZZde"
Если мы передаем в некоторую функцию параметр, то функция по умолчанию работает с копией параметра, а не с оригиналом. Это называется перередача по значению. При этом при изменении передаваемого параметра внутри функции значение соответствующей переменной вне функции не изменится. Иногда же нам надо, что бы функция действительно изменяла передаваемые в нее параметры. Для этого мы должны передавать их по ссылке. Для этого мы используем ключевое слово ref. Вот пример:
class Test
{
static void SomeFunction1(int a)
{
a=12; //Изменяем копию параметра функции
}
static void SomeFunction2(ref int a)
{
a=55; //Изменяем оригинал параметра функции
}
public static void Main()
{
int z=23;
SomeFunction1(z);
Console.WriteLine(z); //Напечатается 23
SomeFunction2(ref z);
Console.WriteLine(z); //Напечатается 55
}
}
Обратите внимание, что при вызове функции SomeFunction2 мы опять используем ключевое слово ref (как и при объявлении этой функции):
... SomeFunction2(ref z); ...
Параметр out аналогичен параметру ref, только при его использовании в функцию можно передавать неинициализированные переменные (т. е. переменные с незаданными начальными значениями). Вот пример:
class Test
{
static void SomeFunction2(ref int a)
{
a=55;
}
static void SomeFunction3(out int a)
{
a=66;
}
public static void Main()
{
int z=0; //Переменную z надо обязательно инициализировать
SomeFunction2(ref z);
Console.WriteLine(z); //Напечатается 55
int y; //Переменную y можно не инициализировать
SomeFunction3(out y);
Console.WriteLine(y); //Напечатается 66
}
}
При вызове такой функции обязательно использование ключевого слова out.
На этом уроке мы напишем функцию для решения квадратного уравнения. Эта функция будет передавать корни уравнения через ссылку - т. е. через свои два параметра. Всего же параметров будет пять - остальные три - это коэффициетны a, b, и c. Тип у функции будет int - она будет возвращать фактическое число корней (т. е. 0, 1 или 2).
Вот текст:
using System;
namespace test
{
class Test
{
static int GetSolution(out double x1, out double x2, double a, double b, double c)
{
double d=b*b-4*a*c; //Дискриминант
x1=x2=0;
//Два разных корня
if(d>0)
{
x1=(-b+Math.Sqrt(d))/(2*a);
x2=(-b-Math.Sqrt(d))/(2*a);
return 2;
}
//Два совпадающих корня
if(d==0)
{
x1=-b/(2*a);
x2=-b/(2*a);
return 1;
}
//Корней нет
return 0;
}
public static void Main()
{
double x1, x2;
int n = GetSolution(out x1, out x2, 1, -5, 6);
switch(n)
{
case 1:
Console.WriteLine("Два совпадающих корня x1={0}, x2={1}", x1, x2);
break;
case 2:
Console.WriteLine("Два разных корня x1={0}, x2={1}", x1, x2);
break;
case 0:
Console.WriteLine("Корней нет");
break;
}
}
}
}
Запускаем программу. Программа должна выдать на экран "Два разных корня x1=3, x2=2".
При перегрузке мы имеем в одной области видимости два или более одноименных метода. Для того, чтобы при компиляции не было ошибок, эти методы должны различаться или типом параметров, или их количеством (или и тем и другим).
Вот пример:
using System;
namespace test
{
class SomeClass{
//Первый метод SomeFunc.
public static int SomeFunc()
{
return 0;
}
//Второй метод SomeFunc.
public static int SomeFunc(int k)
{
return k*k;
}
//Третий метод SomeFunc.
public static float SomeFunc(float k)
{
return k;
}
}
class Test
{
static void Main(string[] args)
{
Console.WriteLine(SomeClass.SomeFunc());
Console.WriteLine(SomeClass.SomeFunc(10));
Console.WriteLine(SomeClass.SomeFunc((float)10));
}
}
}
Результатом выполнения прораммы будет 0 100 10.
Тут в классе SomeClass мы имеет три метода с одним названием - SomeFunc. Для простоты мы сделали все методы статическими, но это не важно. Наши методы различаются количеством параметров и типом. В тестовом классе Test мы вызываем эти методы. Обратите внимание на подсказку редактора - при написании метода SomeFunc возникает желтенькое окошко в котором написано "1 of 3" (т. е. первый метод из трех):
Если нажимать стрелочки вверх-вниз на клавиатуре, то появятся подсказки и по другим двум методам.
Обратите внимание на два момента. Во-первых, методы не могут различаться только типом возвращаемого значения - они должны различаться именно по параметрам. Например, такой метод добавить в класс SomeClass нельзя:
public static bool SomeFunc()
{
return true;
}
Во-вторых, в вызове третьего метода мы использовали приведение типов:
...
Console.WriteLine(SomeClass.SomeFunc((float)10));
...
Если бы мы вызвали метод без слова float, то вызвался бы перегруженный вариант для целых чисел.
Массивы в C# основаны на классе System.Array. У этог класса, как и у любого другого, есть разные полезные методы.
Вот пример:
using System;
namespace test
{
class Test
{
static void Main(string[] args)
{
//Объявление массива.
int [] num = {4, -5, 2, 0, 23};
//Выводим массив.
foreach (int i in num)
{
Console.WriteLine(i.ToString());
}
//Переворачиваем массив.
Console.WriteLine("Перевернутый массив");
Array.Reverse(num);
foreach (int i in num)
{
Console.WriteLine(i.ToString());
}
//Сортируем массив.
Array.Sort(num);
Console.WriteLine("Отсортированный массив");
foreach (int i in num)
{
Console.WriteLine(i.ToString());
}
//Обнуляем массив.
Array.Clear(num, 0, 5);
Console.WriteLine("Обнуленный массив");
foreach (int i in num)
{
Console.WriteLine(i.ToString());
}
}
}
}
Тут мы используем статические методы класса Array для сортировки, переворачивания и очистки массива. Разумеется, эти методы не единственные.
В классе Array есть, например, еще встроенный статический метод IndexOf предназначенный для поиска элемента в массиве. Вот пример его использования:
...
int k=-5;
Console.WriteLine("Число {0} находится на {1} месте.", k, Array.IndexOf(num, k));
...
Этот метод возвращает индекс искомого элемента (нумерация с нуля). Если такого элемента нет, то возвращается -1.
Обратите внимание, что эти методы действуют для встроенных типов (в нашем примере массив был типа int). Для пользовательских типов данных их применение тоже возможно, но для этого на приложить некоторые дополнительные усилия.
Как видите, с массивами в C# можно автоматически делать много чего полезного. В Visual C++, например, такого удобства не было.
Сначала пара слов о том, что такое делегаты. В языке программирования могут быть переменные, содержащие значения целого или вещественного типов. С такими типами все вы хорошо знакомы. Так вот, в первом приближении делегаты - это тоже некоторый тип данных. Что же за тип может хранится в делегатах? В делегатах могут хранится функции (вернее указатели на функции). Т. е. переменной типа делегат можно сначала присвоить одну функцию, поработать с ней, а затем присвоить другую функцию. Это все в первом приближении. Теперь несколько подробностей. Во-первых, делегаты бывают разных типов. Тип делегата определяется типов и количеством параметов и типом возвращаемого значения. Это значит, что если, например, делегат типа void и у него только один параметр типа int, то в такой делегат мы можем записать только функцию типа void и с одним единственым параметроам типа int. Во-вторых, делегаты, в отличие от указателей на функции языка C++, предаставляют из себя безопасные типы данных.
Вот пример делегата:
using System;
namespace test
{
//Объявление делегата.
delegate float MyFunc(float x);
//Класс для тестирования делегата.
class Test
{
//Первая функция.
static float f1(float x)
{
return x;
}
//Вторая функция.
static float f2(float x)
{
return x*x;
}
//Третья функция.
static float f3(float x)
{
return (float)Math.Sqrt(x);
}
//Метод Main.
static void Main()
{
//Создаем экземпляр делегата.
MyFunc f = new MyFunc(f1);
int a; //Ответ пользователя.
Console.WriteLine
("Выберите функцию:\n1.f1=x.\n2.f2=x*x.\n3.f3=sqrt(x).");
//Преобразуем ответ пользователя к типу Int32
a=Int32.Parse(Console.ReadLine());
switch(a)
{
case 1:
//Записываем в делегат 1-ю функцию.
f=new MyFunc(f1);
break;
case 2:
//Записываем в делегат 2-ю функцию.
f=new MyFunc(f2);
break;
case 3:
//Записываем в делегат 3-ю функцию.
f=new MyFunc(f3);
break;
}
//Печатаем результат.
int x=4;
Console.WriteLine("f({0})={1}", x, f(x));
}
}
}
В этой программе мы сначала объявляем делегат:
...
delegate float MyFunc(float x);
...
Синаксис объявления делегата такой - сначала пишем ключевое слово delegate, затем - тип возвращаемого значения (float в нашем примере), потом - произвольное имя делегата (у нас это MyFunc), после которого в круглых скобках перечисляем параметры (у нас только один параметр типа float). Объявленный делегат появится на вкладке ClassView (со специальным значком для делегатов):
Далее мы в нашем тестовом классе объявляем несколько методов - f1, f2 и f3. Все эти методы имеют тип float и один параметр типа float (как и у делегата MyFunc). Функции делают свой подсчет по-разному - первая просто возвращает параметр, вторая - квадрат параметра, третья - корень из параметра.
Далее мы в методе Main создаем экземпляр нашего делегата и в зависимости от ответа пользователя записываем в него f1, f2 или f3. Потом в строке
...
Console.WriteLine("f({0})={1}", x, f(x));
...
мы выводим значение делегата при некотором x (равном 4 в нашем примере). Вернее сказать, мы выводим не значение делегата, а значение функции, которую мы записали в делегат.
В зависимости от ответа пользователя в делегат запишется тот или иной вариант функции и программа выведет сам x (4), x умножить на x (16) или корень из x (2):
На этом занятии мы с вами рассмотрим создание событий в C#. С помощью событий класс извещает, что в нем что-то произошло. События часто создаются в классах используемых для создания пользовательского интерфейса - например у кнопки есть событие, наступающее при щелчке на этой кнопке. События также могут присутствовать и в классах, не предназначенных для создания пользовательского интерфейча - например, они могут быть в классах консолького приложения. Именно для такого класса мы и рассмотрим создание события.
События в C# основаны на делегатах. Это означает, что событие имеет тип определенного делегата. Вот пример класса с событием в нем:
using System;
namespace test
{
//Объявление делегата.
delegate void EventHandler();
//Объявление класса с событием.
class MyEvent
{
//Объвление события на основе делегата.
public event EventHandler f;
//Объвление метода, в котором вызывается событие.
public void func()
{
f();
}
}
//Класс для тестирования события класса MyEvent.
class Test
{
//Обработчик для события.
public static void z()
{
Console.WriteLine("Вызов обработчика");
}
//Метод main.
static void Main(string[] args)
{
//Создаем экземпляр класса с событием.
MyEvent w=new MyEvent();
//Добавление обработчика события.
w.f+=new EventHandler(z);
//Вызов метода, в котором вызывается событие.
w.func();
}
}
}
Сначала мы в строке
...
delegate void EventHandler();
...
объявляем делегат. Параметров у него нет (хотя могут и быть), тип - void.
Затем мы объявляем класс MyEvent, внутри которого объвляем событие f (имя произвольное):
...
public event EventHandler f;
...
Синтаксис тут такой - модификатор доступа (у нас public), затем ключевое слово event, потом имя делегата, на основании которого мы создаем наше событие (у нас это EventHandler) и, наконец, имя произольное событие (f). Обратите внимание, что событие появится на вкладке ClassView:
Далее в нашем классе с событиями мы в некотором методе это событие вызываем:
...
public void func()
{
f();
}
...
Класс с событием создан. Далее мы пишем класс Test для тестирования события. В этом классе мы создаем обработчик z для события (имя обработчика произвольное). Этот метод мы объвляем как статический.
Далее мы объявляем экземпляр нашего класса MyEvent (того самого, в котором мы объявили событие):
...
MyEvent w=new MyEvent();
...
Остается теперь указать, что за обработчик будет у события f класса MyEvent. Это мы делаем в строке
...
w.f+=new EventHandler(z);
...
Этой строчкой мы указываем, что событие f будет обрабатывать метод z класса Test.
Если мы запустим наше программу, то на вызовется обработчик для события (т. е. выведется надпись "Вызов обработчика"):
Обратите внимание, что обработчик для события мы пишем в тестовом классе (т. е. в классе Test, в котором мы объявили экземпляр нашего класса с событиями). Так мы делаем всегда, и не только на пратформе .NET.
На этом уроке мы расмотрим еще один пример класса с событием. Класс наш будет представлять из себя игральную кость, в классе будет единственный метод rnd, возвращающий случайное число от 1 до 6. Если этот метод возвратит 6, то сгенерируется событие max.
namespace test
{
//Объявление делегата.
public delegate void EventHandler();
//Класс "Игральная кость".
class MyDie
{
Random r;
//Объвление события на основе делегата.
public event EventHandler max;
//Конструктор.
public MyDie()
{
r=new Random();
}
//Объявление метода, в котором вызывается событие.
public int rnd()
{
//Случаное число от 1 до 6.
int res = r.Next(6)+1;
if(res==6)
{
//Вызываем событие.
max();
}
return res;
}
}
//Класс для тестирования события.
class Test
{
//Обработчик для события.
public static void z()
{
Console.WriteLine("Вызов обработчика");
}
//Метод main.
static void Main(string[] args)
{
//Создаем экземпляр события.
MyDie w=new MyDie();
//Добавление обработчика события.
w.max+=new EventHandler(z);
//Вызов метода, в котором вызывается событие.
for(int k=0; k<10; k++)
{
Console.WriteLine("{0}", w.rnd());
}
}
}
}
При запуске нашей программы к классе Test мы создаем новый экземпляр класса MyDie игральной кости, приписываем к событию max класса MyDie обработчик z, и подкидываем кость 10 раз. Если выпадет шестерка, то возникнет событие max и выполнится обработчик для него. Результат выполнения программы может быть, например, таким:
Сначала несколько слов о том, что такое атрибуты. Итак, атрибут - это некоторая дополнительная информация, которая может быть приписана к типам, полям, методам, свйствам и некотрым другим конструкциям языка. Атрибуты поещаются в исполняемый файл и могут оттуда при необходимости извлекаться.
Все атрибуты (в отличие, скажем, от атрибутов языка IDL) являются классами (потомками класса System.Attribute). В отличие от атрибутов IDL набор атрибутов .NET открыт для дополнения, т. е. вы можете определять собственные атрибуты и применять их к вышеуказанным элементам вашего кода.
Атрибуты делятся на предопределенные (встроенные) и пользовательские (которые пишет программист).
Встроенные атрибуты могут использоваться, например, при сериализации (сохранении в поток) данных класса. Скажем, вам надо, чтобы у класса сохранялись не все данные - в этом случае вы можете пометить те данные, которые не надо сохранять, специальным атрибутом.
Еще пример применения атрибутов. Компоненты, которые вы располагаете на форме (кнопки, метки и т. п.) имеют некоторый набор свойств (шрифт, местоположение, видимость и т. п.). В IDE Visual Studio вы можете выбрать в окне Properties один из двух способов расположения этих свойств - по алфавиту или по категориям. Так вот, в какую категорию попадет то или иное свойство, определяется специальным встроенным атрибутом.
Атрибуты в C# заключаются в квадратные скобки.
Вот пример определения и использования пользовательского атрибута:
using System;
namespace test
{
//Объявление атрибута.
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
class TestAttribute: System.Attribute
{
//Поле атрибута.
private string name;
//Конструктор атрибута.
public TestAttribute(string name)
{
this.name = name;
}
//Свойство только для чтения.
public virtual string Name
{
get
{
return name;
}
}
}
//Конец объявления атрибута.
//Применение атрибута к классу.
[TestAttribute("Igor Alexeev")]
class Test
{
static void Main()
{
GetAttribute(typeof(Test));
}
public static void GetAttribute(Type t)
{
TestAttribute att=
(TestAttribute) Attribute.GetCustomAttribute(t, typeof(TestAttribute));
Console.WriteLine("{0}", att.Name);
}
}
}
Как видно, атрибут TestAttribute является потомком класса System.Attribute (как всегда). Перед определением нового атрибута мы видим строку
... [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] ...
Этой строкой определяется область применения нашего атрибута - первый параметр (AttributeTargets.All) говорит о том, что мы сможем применять наш атрибут TestAttribute к чему угодно, второй (Inherited=false) - что атрибут не будет наследоваться (т. е. если мы применим атрибут TestAttribute к некоторому классу, то у потомков этого класса атрибута TestAttribute не будет), третий (AllowMultiple = true) - что атрибут к каждому элементу может пременяться только один раз (заметим в скобках, что для определения области применения нашего пользовательского атрибута мы используем другой атрибут - AttributeUsage).
Далее мы определяем для нашего атрибута внутреннюю переменную name типа string и конструктор с параметром типа string. В конструкторе мы записываем значение в переменную name. Еще чуть ниже мы пишем в классе атрибута свойство только для чтения:
...
public virtual string Name
{
get
{
return name;
}
}
...
После создания класса атрибута мы применяем его к другому классу Test. Для этого мы должны создать экземпляр атрибута TestAttribute непосредственно перед классом Test:
...
[TestAttribute("Igor Alexeev")]
class Test
{
...
Делаем мы это в квадратных скобках. При этом у нас вызывается конструктор с параметром атрибута.
В классе Test мы в методе GetAttribute просто выводим на консоль значение свойства name атрибута. Для этого мы воспользовались статическим методом GetCustomAttribute класса Attribute. Этот метод принимает в качестве параметров тип, к которому атрибут применяется (т. е. класс Test в нашем случае) и собственно атрибут, который мы применяем (у нас это TestAttribute). Возвращает же он экземпляр атрибута, который мы и используем для получения значения свойства name.
Метод GetAttribute мы вызываем в констукторе класса Test.
Результатом выполнения нашей программы будет вывод на консоль слов "Igor Alexeev".
Параметры командной строки передаются, естественно, в методе Main. В отличие от некоторых языков программирования (C/C++) параметр только один - это массив передаваемых параметров. Аналога первого параметра языка C/C++ (количества параметров) нет. Передаются параметры в виде массива строк.
Вот пример прораммы, выводящий на экран все передаваемые в нее параметры командной строки и их количество:
using System;
namespace test
{
class Test
{
//Передаем в Main параметры командной строки.
static void Main(string[] args)
{
//Выводим количество параметров.
Console.WriteLine(args.GetLength(0));
//Выводим каждый параметр.
foreach (string s in args)
{
Console.WriteLine(s);
}
}
}
}
Обратите внимание, что в сам exe-файл не считается параметром (как в C/C++).
До этого мы в большинстве случае помещали метод Main в тот же класс, который испытывали:
using System;
namespace test
{
class Test
{
//... (поля, методы, ... класса)
static void Main()
{
//...
}
}
}
Но для больших, настояших программ это не самый лучший способ. Гораздо лучше вынести метод Main в отдельный класс:
using System;
namespace test
{
class Test
{
//... (поля, методы, ... класса Test)
}
//Класс приложения для вызова метода Main.
class App
{
static void Main()
{
//Создаем экземпляр класса Test.
Test c = new Test();
//...
}
}
}
Вообще говоря в программе у вас, как правило, будет много классов. И лучше каждый из них хранить в отельном файле (с расширением *.cs).
Для вывода на консоль мы использовали следующую конструкцию:
...
int x=23, y=-4;
...
Console.WriteLine("x={0}, y={1}", x, y);
...
Здесь мы используем внутри кавычек подстановочные знаки {0}, {1} и т. д. (нумерация в них идет с нуля). Переменные при этом выводятся в формате по умолчанию. Для вывода в определеном формате надо использовать подстановочные знаки с параметрами. Вот некоторые их них:
А вот пример их использования:
...
int a=38;
//Выведется 0038
Console.WriteLine("a={0:d4}", a);
double pi=3.1415926;
//Выведется 3.14
Console.WriteLine("pi={0:f2}", pi);
int b=255;
//Выведется FF.
Console.WriteLine("b={0:X}", b);
int c=255;
//Выведется ff.
Console.WriteLine("c={0:x}", c);
double d=1003.214;
//Выведется $1, 003.14 в английской версии Windows и
//1 003,14 р. в русской.
Console.WriteLine("d={0:c}", d);
double e=213.1;
//Выведется 2.131000e+002
Console.WriteLine("e={0:e}", e);
...
Параметры подстановочных знаков можно использовать как строчные, таки и прописные - это все равно. Исключение - вывод числа в шестнадцатеричном виде (при использовании h цифры a, ..., f будут строчными, при использовании H - прописными).
Класс System.Object является предком для всех классов. Это происходит неявным образом - при объявлении класса мы System.Object не указываем в качестве класса предка. Хотя это и можно сделать (результат будет одним и тем же):
class Test:System.Object
{
...
}
То, что все классы являются потомками класса System.Object, позволяет нам использовать методы последнего.
Еще одно из следствий этого - это возможность записать в переменную типа System.Object экземпляр любого класса:
//Некоторый класс.
class Test
{
public string s;
}
class App
{
static void Main()
{
Object ob;
//Записываем в ob экземпляр класса-потомка Test.
ob=new Test();
//Приводим ob к типу Test.
((Test)ob).s="some string";
//Выводим значение поля s.
Console.WriteLine(((Test)ob).s);
}
}
Указанный фрагмент выведет, естественно, строку "some string".
В классе System.Object имеются следующие методы:
Вот пример использования этих методов:
Object ob1, ob2;
ob1=new System.Object();
ob2=ob1;
//Выведется True.
Console.WriteLine(ob1.Equals(ob2));
ob2=new System.Object();
//Выведется False.
Console.WriteLine(ob1.Equals(ob2));
//Выведется некотрое число.
Console.WriteLine(ob1.GetHashCode());
//Выведется System.Object.
Console.WriteLine(ob1.GetType());
//Выведется System.Object.
Console.WriteLine(ob1.ToString());
Виртуальные методы класса System.Object часто переписывают в классах-потмках.
Часть методов класса System.Object, рассмотренных на прошлом уроке, были виртуальными. Это значит, что мы можем переписать их в классе-потомке. Давайте для примера перепишем в нашем классе Vector метод ToString так, чтобы он возвращал не имя класса, а координаты вектора в красивом виде. Это будет выглядеть вот так:
class Vector
{
public float x, y; //Координаты.
//Переопределяем метод ToString.
public override string ToString()
{
return "x = " + x +", y = " + y;
}
}
//Тестовый класс.
class App
{
static void Main()
{
Vector v=new Vector();
v.x=2;
v.y=3;
//Выводим координаты вектора.
Console.WriteLine(v.ToString());
}
}
А в следующем примере мы переопределим виртуальный метод Equals таким образом, чтобы считать равными два вектора с одинаковой длиной (вспомним теорему Пифагора):
class Vector
{
...
//Переопределяем метод Equals.
public override bool Equals(object ob)
{
Vector t=(Vector)ob;
return (t.x*t.x+t.y*t.y)==(this.x*this.x+this.y*this.y);
}
}
//Тестовый класс.
class App
{
static void Main()
{
Vector v1=new Vector();
v1.x=2;
v1.y=3;
Vector v2=new Vector();
v2.x=-3;
v2.y=2;
Console.WriteLine(v1.Equals(v2));
}
}
В тестовом классе мы проверяем действие переопределенного метода для равных по длине векторов (2, 3) и (-3, 2). Указанный фрагмент выведет True.
Константы удобны использовать для величин, которые в программе не меняются. Использование констант позволяет изменить некоторую величину везде в программе за пару секунд - легче изменить значение в одном месте, чем по всей программе.
Константа определяется ключевым словом const. Вот пример:
class MyClass
{
//Объявление константы.
public const int SomeValue=20;
...
}
Обратите внимание, что константы, как и все в C#, определяется в классе.
Часто в программе заводят некоторый вспомогательный класс, главное предназначение которого - это хранение в одном месте всех констант программы. Например, такой класс может выглядеть так:
abstract class Constants
{
public const int SomeValue1=20;
public const int SomeValue2=100;
public const double SomeValue3=0.35;
}
Мы этот класс проеделили как абстрактный (см. урок 19) для того, чтобы нельзя было создавать экземпляры этого класса. Он используется у нас только для хранения констант.
Использовать этот класс можно так:
class App
{
static void Main()
{
double z;
z=Constants.SomeValue2 * Constants.SomeValue3;
Console.WriteLine("z={0}", z); //Выведется 35.
}
}
Члены класса (поля, методы и т. п.) могут иметь разные модификаторы доступа. Вот они:
| Модификатор доступа | Описание |
| public | Доступность откуда угодно. |
| private | Доступность только из этого же самого класса (по умолчанию). |
| protected | Доступность только из этого же самого класса или его потомков. |
| internal | Доступность из любого класса той же программы. |
| protected internal | Доступность или из любого класса той же программы или из этого же самого класса или его потомков (т. е.или как protected или как internal) |
Модификаторы доступа пишутся первыми - перед всеми другими коючевыми словами (например, типом переменной). Модификатор static может стоять как перед модификатором доступа, так и после:
...
static public int a;
public static int b;
...
В отличие от C/C++ модификаторы доступа пишутся для любого члена класса (как в Java):
class SomeClass
{
//Правильно.
public void f1()
{
//...
}
public void f2()
{
//...
}
...
Ссылка на текущий экземпляр класса делается через ключевое слово this. Вот традиционный пример:
class SomeClass
{
public int a;
SomeClass(int a)
{
this.a=a;
}
}
В этом примере у нас и переменная класса, и параметр метода названы одинаково - a. Для уточнения, что мы используем именно переменную класса, а не параметр метода, и служит слово this.
В C++ же для избегания конфликта имен обычно к переменной класса прибывлялся префикс m_.
Еще одна возможность по использованию ключевого слова this - это вызов одного конструктора из другого. Вот пример:
class SomeClass
{
public int a;
public int b;
//Конструктор с 2-я параметрами.
public SomeClass(int a, int b)
{
this.a=a;
this.b=b;
}
//Конструктор без параметров,
//вызывающий конструктор с 2-я параметрами.
public SomeClass():this(1, 1)
{
}
}
//Тестовый класс.
class App
{
static void Main()
{
SomeClass s=new SomeClass();
Console.WriteLine("a={0}, b={0}", s.a, s.b);
}
}
Здесь в классе SomeClass два конструктора. Второй конструктор (без параметров) вызывает первый (передавая в него значения 1 и 1):
... public SomeClass():this(1, 1) ...
Указанный фрагмент выведет, разумеется, a=1, b=1.
Класс Environment позволяет получить информацию об окружении программы (текущий каталог, версия Windows и т. п.) через свои статические члены. Вот пример использования этого класса:
string s;
//Текущая папка.
s=Environment.CurrentDirectory;
Console.WriteLine("Текущая папка: {0}.", s);
//Системная папка.
s=Environment.SystemDirectory;
Console.WriteLine("Системная папка: {0}.", s);
//Имя компьютера.
s=Environment.MachineName;
Console.WriteLine("Имя компьютера: {0}.", s);
//Oперационная система и ее версия.
OperatingSystem os=Environment.OSVersion;
Console.WriteLine("Операционная система: {0}, версия {1}.",
os.Platform, os.Version);
//Версия платформы .NET.
Version ver=Environment.Version;
Console.WriteLine("Версия платформы .NET: {0}.{1}.",
ver.Major, ver.Minor);
Указанный фрагмент выведет на экран имя папки, в котром запущено приложение, имя системной папки Windows, NetBIOS имя компьютера, операционую систему и ее версию, платформу .NET и ее версию:
Класс Environment, рассмотреннуй на прошлом уроке, позволяет выяснять и местоположение различных специальных папок - SendTo, StartMenu, Program Files и др. Это может оказаться очень удобным для, например, добавлении ярлыка на вашу программу в папку Автозагрузки или на Рабочий Стол.
Специальные папки образуют перечисление Environment.SpecialFolder. Вот его некоторые члены:
| Специальная папка | Описание |
| Cookies | Папка с cookie |
| DesktopDirectory | Рабочий стол |
| Favorites | Избранное |
| History | История (Internet Explorer) |
| Personal | Папка "Мои документы" |
| ProgramFiles | Папка "Program Files" |
| Recent | Папка со списоком последних открываемых документов |
| SendTo | Папка SendTo |
| StartMenu | Главное меню |
| Startup | Папка автозагрузки |
| System | Системная папка Windows |
Обратите внимание, что некоторые из этих папок общие для всех пользователей, а некоторые персональны для каждого пользователя.
Вот пример работы со специальными папками:
//Получаем папку SendTo.
Environment.SpecialFolder p=Environment.SpecialFolder.SendTo;
//Получаем путь к папке SendTo.
string s=Environment.GetFolderPath(p);
Console.WriteLine("Папка SendTo: {0}.", s);
//Получаем папку Startup.
p=Environment.SpecialFolder.Startup;
//Получаем путь к папке Startup.
s=Environment.GetFolderPath(p);
Console.WriteLine("Папка Startup: {0}.", s);
Тут мы сначала записываем в переменную типа Environment.SpecialFolder нужную папку, затем извлекаем путь к нужной папке через метод GetFolderPath класса Environment.
Класс Environment позволяет получить и имена всех логических дисков компьютера. Делается это через его статический метод GetLogicalDrives(). Вот пример:
string[] drives=Environment.GetLogicalDrives();
foreach(string s in drives)
{
//Выводим имена всех дисков.
Console.WriteLine(s);
}
А вот результат выполения программы:
Метод GetLogicalDrives() возвращает строковый массив с именами всех дисков.
Для операций ввода-вывода служит пространство имен System.IO.
Вот краткий обзор наиболее важных классов и перечислений из этого пространства имен:
Для работы с файлами и папками в пространстве имен System.IO существуют следующие классы:
Directory - содержит ряд статических методов для выполнения различных операций с папками (создание, удаление, проверка существования, получение текущей папки и т. п.).
DirectoryInfo - аналогичен классу Directory, только его методы не статические - для использования этого класса надо создать его экземпляр (параметром для конструктора служит имя папки).
File - содержит ряд статических методов для выполнения различных операций с файлами (создание, удаление, копирование и т. п.).
FileInfo - аналогичен классу File, только его методы не статические - для использования этого класса надо создать его экземпляр (параметром для конструктора служит имя файла).
Классы Directory и File яляются непосредственными потомками класса Object, а классы DirectoryInfo и FileInfo - потомками абстрактного класса FileSystemInfo.
Класс Directory предназначен для работы с папками. Этот класс содержит статические методы для работы с папками (в отличие от класса DirectoryInfo, который содержит аналогичные методы, применяемые для экзампляра класса).
Вот основные методы класса Directory (все они статические):
А вот пример их употребления:
using System;
//Подключаем необходимое пространство имен.
using System.IO;
namespace constest
{
...
class Class1
{
...
static void Main(string[] args)
{
//Создаем папку.
Directory.CreateDirectory("C:\\temp");
//Проверка существования папки.
if(Directory.Exists("C:\\temp1"))
{
Console.WriteLine("Папка \"temp1\" существует");
}
else
{
Console.WriteLine("Папка \"temp1\" не существует");
}
if(Directory.Exists("C:\\temp"))
{
Console.WriteLine("Папка \"temp\" существует");
}
else
{
Console.WriteLine("Папка \"temp\" не существует");
}
//Перемещение папки.
Directory.Move( "C:\\temp", "C:\\temp2");
//Удаление папки.
Directory.Delete( "C:\\temp2");
}
}
}
Обратите внимание, что метод Move можно использовать не только для перемещения, но и для переименования папки (что мы, фактически, в нашем примере и делаем). Для этого папка, задаваемая первым параметром (т. е. та, которую перемещаем) должна находиться на том же уровне, что и папка, задаваемая вторым параметром (т. е. куда перемещаем).