
Using System;
using System.Collections.Generic;
Namespace PlaygroundConsole {
public class Planet {
private float _x = 0;
private float _y = 0;
private string _name = null;
// Исключение для случая пустого имени.
public class NoNameException : Exception {
public NoNameException(string msg) : base(msg) { }
}
public Planet(float x, float y, string name) {
SetName(name);
SetX(x);
SetY(y);
}
public float GetX() { return _x; }
public float SetX(float x) { _x = x; }
public float GetY() { return _y; }
public float SetY(float y) { _y = y; }
public string GetName() { return _name; }
public string SetName(string name) {
if (string.IsNullOrEmpty(name)) {
throw new NoNameException("Планета должна иметь название");
} else _name = name;
}
public override string ToString() {
return string.Format("{0} [{1}; {2}]", _name, _x, _y);
}
}
public class Program {
Static void Main(string[] args) {
List<Planet> list = new List<Planet>();
try {
// Заполняем список объектами.
list.Add(new Planet(1.0F, 1.0F, "Меркурий"));
list.Add(new Planet(1.0F, -0.5F, "Венера"));
list.Add(new Planet(-1.0F, 1.0F, "Земля"));
list.Add(new Planet(2.0F, -0.2F, "Марс"));
list.Add(new Planet(1.0F, 5.0F, "Юпитер"));
list.Add(new Planet(-8.0F, -5.0F, "Сатурн"));
list.Add(new Planet(13.0F, 1.0F, "Уран"));
list.Add(new Planet(14.0F, -6.0F, "Нептун"));
// Выводим список на экран первым способом.
for (int i = 0; i < list.Count; i++) {
Console.WriteLine(list[i]);
}
Console.WriteLine();
// Выводим список на экран вторым способом.
foreach (Planet item in list) {
Console.WriteLine(item);
}
Console.ReadKey(true);
} catch (Planet.NoNameException ex) {
Console.Error.WriteLine(ex.Message);
}
}
}
}
Любой класс, определённый пользователем или находящийся в сборке, по умолчанию наследует от класса Object и автоматически перенимает методы этого класса. Например, в строке 34 с помощью ключевого слова override перегружается метод ToString() класса Object.
Обратите внимание, что при описании классов уровень доступности (private, protected, public) указывается отдельно для каждого атрибута и метода. Если уровень доступа не указан, член класса по умолчанию будет приватным.
Стоит отметить, что для хранения данных здесь применяется параметризованный тип List из сборки System.Collections.Generic. Вспомните класс «Ассоциативный массив» и попробуйте рассмотреть класс Dictionary из этой же сборки.
Нужно отметить важную особенность языка – любая переменная не примитивного типа является указателем. Это подразумевает передачу параметров методам по ссылке, без копирования объекта в стек (исключение составляют примитивные типы, но и в этом случае существует способ передачи по ссылке, о чём будет сказано ниже).
Создание объектов производится с помощью оператора new (строка 41), они размещаются в динамической памяти, но об освобождении её программисту обычно беспокоиться не нужно: среда выполнения содержит «сборщик мусора» (garbage collector), который автоматически проверяет существующие объекты на наличие ссылок на них. Если на объект не ссылается ни один другой, он уничтожается. Также нужно обратить внимание на то, что доступ к методам и свойствам (атрибутам) объектов осуществляется с помощью точки, а не стрелки, как это делается в C++ (например, строки 35 и 45).
Перечисления определяются и применяются похожим на C++ образом:
public enum ObjectType { Simple, Complex };
//...
ObjectType o = ObjectType.Simple;
if (o == ObjectType.Simple) Console.WriteLine("Simple");
else Console.WriteLine("Complex");
Чтение файлов значительно упрощено по сравнению со штатными средствами C++. Данные файла можно считать построчно в массив:
using System;
using System.IO;
namespace PlaygroundConsole {
class Program {
static void Main(string[] args) {
string fileName = null;
string[] data = null;
Console.WriteLine("Введите имя файла: ");
fileName = Console.ReadLine();
if (!File.Exists(fileName)) {
Console.Error.WriteLine("Файл \"{0}\" не найден", fileName);
} else {
data = File.ReadAllLines(fileName);
foreach (string str in data) {
Console.WriteLine(str);
}
}
}
}
}
Если опустить предварительную подготовку, считывание имени файла и пр., то операция чтения данных занимает всего одну строку (17).
Возможность считать файл побайтно (например, бинарный) также сохранена, однако для этого применяются другие средства (например, класс System.IO.StreamReader). Аналогичным образом можно и записать данные в файл: для этого можно использовать метод WriteAllLines(string path, string[] contents) класса System.IO.File.
Встроенный класс string предоставляет ряд методов для работы с объектами своего типа: выбор подстроки (Substring), поиск подстроки (IndexOf), замена подстроки (Replace) и др. Среди этого набора имеется метод Split, разбивающий строку на подстроки с использованием указанного символа-разделителя:
// ...
string testString = "1 2 3";
string[] chunks = testString.Split(' '); // chunks = { "1", "2", "3" }
// ...
Преобразование строковых данных к определённому типу делается с помощью методов Parse:
// ...
int a = 0, b = 0, c = 0;
string testString = "1 2 3";
string[] chunks = testString.Split(' '); // chunks = { "1", "2", "3" }
a = int.Parse(chunks[0]); // a = 1
b = int.Parse(chunks[1]); // b = 2
c = int.Parse(chunks[2]); // c = 3
// ...
Следует отметить, что если преобразуемая строка не содержит данных ожидаемого типа (набор букв алфавита вместо числа), то метод Parse вызовет исключение. В таком случае безопаснее использовать метод TryParse:
// ...
int a = 1, b = 0, c = 0;
string testString = "aaa 2 3";
string[] chunks = testString.Split(' '); // chunks = { "aaa", "2", "3" }
int.TryParse(chunks[0], out a); // a = 0
int.TryParse(chunks[1], out b); // b = 2
int.TryParse(chunks[2], out c); // c = 3
// ...
При попытке обработать неожиданные данные переменная-результат будет инициализирована значением по умолчанию (для числовых типов это 0). Результат сохраняется во второй параметр, передаваемый с ключевым словом out – передача примитивного типа по ссылке, а также явное указание компилятору о том, что объект будет инициализирован внутри метода (сравните строки 2 и 6).
Ещё одной особенностью языка является использование свойств (properties), которые предназначены для хранения данных и доступа к ним. Свойства для внешнего кода во многом схожи с публичными атрибутами:
public class Planet {
// Исключение для случая пустого имени.
public class NoNameException : Exception {
public NoNameException(string msg) : base(msg) { }
}
// Свойства.
public float X { get; set; }
public float Y { get; set; }
public string Name { get; set; }
public Planet(float x, float y, string name) {
if (string.IsNullOrEmpty(name)) {
throw new NoNameException("Планета должна иметь название");
} else {
Name = name;
X = x;
Y = y;
}
}
public override string ToString() {
return string.Format("{0} [{1}; {2}]", Name, X, Y);
}
}
Использование свойств позволяет отказаться от набора геттеров и сеттеров (getSimething() и setSomething(...)) и предоставить прямой доступ к атрибутам. Но отсутствие контроля доступа к данным, особенно к тем, от которых зависит состояние и поведение объекта, нарушает принцип инкапсуляции. С помощью свойств можно соблюдать этот принцип, для чего потребуется завести дополнительные переменные того же типа, что и свойства, и использовать их внутри тел методов get и set:
public class Planet {
private string _name = null;
// Исключение для случая пустого имени.
public class NoNameException : Exception {
public NoNameException(string msg) : base(msg) { }
}
// Свойства.
public float X { get; set; }
public float Y { get; set; }
public string Name {
get { return _name; }
set {
if (string.IsNullOrEmpty(value)) {
throw new NoNameException("Планета должна иметь название");
} else _name = value;
}
}
public Planet(float x, float y, string name) {
Name = name;
X = x;
Y = y;
}
}
public override string ToString() {
return string.Format("{0} [{1}; {2}]", Name, X, Y);
}
}
Поскольку контролировать доступ к свойствам X и Y не нужно, то их методы работают по умолчанию и ввода дополнительных переменных не требуется. Для свойства Name требуется проверка устанавливаемого значения, поэтому нужно перегрузить методы get и set и дополнительная переменная для хранения, а свойство будет выступать в роли посредника между кодом и переменной. Код, касающийся свойства Name в примере выше, по сути, дублирует геттер и сеттер (передаваемый параметр в этом случае называется value, см. строки 15 и 17) для атрибута _name.
Для знакомства с языком предлагается с его помощью выполнить следующую задачу.