При использовании интерфейса у нас есть чертеж того, как должна выглядеть деталь, по которому выполняются конкретные детали.
В случае автомобиля интерфейс описывает общий вид автомобиля (4 колеса) и способ управления им (руль, педали). Конкретные автомобили реализуют этот интерфейс, благодаря чему, умея управлять автомобилем можно ездить практически на любом из них.
Операторы is и as
При использовании наследования часто возникают ситуации, когда нужно узнать, объект какого класса скрывается под полиморфным именем. Это можно сделать с помощью оператора is. Он возвращает истину (True), если объект принадлежит указанному классу и ложь (False) в противном случае.
// sh - является квадратом и является фигурой,
// но не является прямоугольником
Console.WriteLine(sh is XShape); // True
Console.WriteLine(sh is XRectangle); // True
Console.WriteLine(sh is XCircle); // False
Для приведения типов с проверкой, возможно ли такое приведение типов можно использовать оператор as
Запись
x = y as XSomeClass;
Эквивалентна
if (y is XSomeClass)
x = (XSomeClass)y;
else
x = null;
Кстати, есть более короткий способ описать подобный if – оператор ?
x = y is XSomeClass ? (XSomeClass)y : null;
Если применить операцию as к предыдущему примеру, то получится.
XCircle c2 = sh as XCircle;
Console.WriteLine(c2 == null); // True, квадрат не приводится к кругу
XRectangle r2 = sh as XRectangle;
Console.WriteLine(r2 == null); // False, sh ссылается на квадрат
Статические атрибуты и методы
Это атрибуты и методы, которые совместно используются всеми объектами класса. Их также называют атрибуты (переменные) класса и методы класса.
Cтатические методы могут обращаться только к статическим атрибутам и методам.
К статическим атрибутам и методам можно обращаться, не имея объекта.
В C# можно объявлять статические конструкторы. Они могут работать только со статическими атрибутами и методами и вызываются при создании объекта класса (до обычного конструктора) или при первом доступе к статическому атрибуту или методу. Не может иметь параметров и может быть только один.
class XSomeClass
{
public static int a = 0;
public int b = 0;
private int c = 0;
private static int d = 0;
public static void f(int x)
{
// Внутри статических методов класса можно обращаться к статическим
// атрибутам
a = x;
// b = 5; // ошибка, обращение к не статическому атрибуту
Console.WriteLine("a = " + a.ToString());
}
public void g()
{
// В обычных методах можно обращаться и к статическим
// и к не статическим атрибутам
b = 2;
Console.WriteLine("a = " + a.ToString());
}
public static void h()
{
// В статических методах можно обращаться
// к private атрибутам класса
d = 9;
// Можно создать объект класса и обращаться
// к его нестатическим атрибутам
XSomeClass zSomeClass = new XSomeClass();
zSomeClass.c = 7;
}
}
class Program
{
static void Main(string[] args)
{
// К статическим атрибутам нужно обращаться через имя класса
XSomeClass.f(5); // a = 5
XSomeClass zSomeClass = new XSomeClass();
zSomeClass.g(); // a = 5
Console.WriteLine(zSomeClass.b.ToString()); // 2
Console.ReadLine();
}
}
С помощью статических методов можно описать поведение, по смыслу относящееся к данному классу, но не зависящее от состояния объекта.
Статические атрибуты удобно использовать для задания настроек, определяющих поведение всех объектов данного класса. Например, когда в программе можно открывать одновременно несколько окон редактирования какого-нибудь объекта, то удобно иметь статический атрибут, содержащий список таких окон, чтобы не открывать повторно окно редактирования которое уже открыто.
Статические атрибуты и методы полезны, когда возникает необходимость в глобальных переменных и функциях, поскольку они доступны везде, где доступен класс, в котором они объявлены.
Классы, содержащие наборы статических методов, которые могут быть полезны в разных частях системы называются Helper. Для объявления таких классов удобно использовать новую возможность .NET 2.0 – статические классы. Они объявляются с помощью ключевого слова static и могут содержать только статические атрибуты и методы. Создать объект такого класса нельзя.
static class XHelper
{
public static int _someAttribute;
public static void UtilityMethod1()
{
// ...
}
public static void UtilityMethod2()
{
// ...
}
}
Паттерн синглтон (singleton)
Часто в системе требуются разные крупные объекты, которые существуют в единственном экземпляре. В этом случае можно написать специальный класс синглтон, который будет обеспечивать такую функциональность.
class XSingleton
{
protected static XSingleton _instance = null;
public static XSingleton Instance
{
get
{
if (_instance == null)
// Здесь, при необходимости
// можно создавать объекты дочерних классов
_instance = new XSingleton();
return _instance;
}
}
// Конструктор может быть protected
// Единственный способ создать объект извне - обратиться к Instance
protected XSingleton()
{
// Инициализация
Console.WriteLine("Create");
}
public void SomeMethod()
{
Console.WriteLine("Do something");
}
}
class Program
{
static void Main(string[] args)
{
// Обращаться к синглтону можно из любого
// места, где доступен класс.
// Объект будет создан при первом обращении
// и будет жить до конца работы программы
XSingleton.Instance.SomeMethod();
// Create
// Do something
// При повторном обращении пользуемся уже готовым объектом
XSingleton.Instance.SomeMethod();
// Do something
Console.ReadLine();
}
}
Этот подход обладает рядом преимуществ перед классом со статическими методами и атрибутами. Одно из очевидных – что инициализация происходит при первом обращении, только если объект используется. Также можно использовать наследование и создавать в зависимости от ситуации разные объекты.
[TODO: Описать подробнее (из статьи)]
Класс object
В .NET все классы (как классы .NET Framework, так и создаваемые пользователем классы) являются наследниками класса object. Благодаря этому, любые объекты приводятся к ссылке на object. Это удобно в случаях, когда нужно передать ссылку на объект не думая о том, какому классу этот объект принадлежит. Например, в .NET 1.1 динамические массивы (ArrayList) хранят ссылки на объекты, как object. object может ссылаться на что угодно, подобно типу pointer в паскале или void* в C++.
XSomeClass zSomeClass = new XSomeClass();
object zObject = zSomeClass;
В классе object есть конструктор и деструктор, а также набор методов, которые наследуются всеми другими классами:
public virtual bool Equals(object);
Сравнивает два объекта. Если операция не переопределена, то сравнивает ссылки. Обратите внимание, что даже если вы переопределите эту операцию, операторы == и != будут по-прежнему сравнивать объекты как ссылки. Чтобы эти операторы сравнивали объекты так, как нужно вам, можно переопределить их следующим образом (причем обязательно оба оператора):
class XSomeClass
{
public int x;
public static bool operator==(XSomeClass s1, XSomeClass s2)
{
return s1.x == s2.x;
}
public static bool operator !=(XSomeClass s1, XSomeClass s2)
{
return s1.x == s2.x;
}
}
}
Console.WriteLine(s1 == s2); // True
public virtual int GetHashCode();
Возвращает хэш-код объекта.
public Type GetType();
Возвращает тип объекта как объект класса System.Type. При этом возвращается именно тип объекта, а не тип ссылки.
public static bool ReferenceEquals(object objA, object objB);
Сравнивает два объекта как ссылки.
protected object MemberwiseClone();
Поэлементное копирование объекта. Этот метод возвращает новый экземпляр того же класса, в котором атрибуты скопированы как ссылки (т.е. для объектов являющихся атрибутами класса, этот метод не вызвается). Объекты value-типов (int, char и т.п.) копируются как значения. Такое копирование называется поверхностным (shallow copy).
public virtual string ToString();
Возвращает объект как строку. Если метод не переопределен, возвращает полное имя класса (с именем пространства имен). Переопределение этого метода бывает очень полезным. Например, если переопределить в своем классе метод ToString и подставлять объекты этого класса в элемент редактирования ListBox, то в списке будет отображаться то, что возвращает для данного объекта метод ToString.
Упаковка и распаковка
[TODO] Написать и придумать пример.
XRifle r1 = new XRifle(30,3);
XRifle r2 = r1;
r2.Fire();
// Это две ссылки на один и тот же объект. Они равны.
if (r1 == r2) Console.WriteLine("References equal");
if (r1.AmmoCount == 27) Console.WriteLine("Same object");
Для ссылок определено специальное значение null (аналог NULL или 0 для указателей), которое обозначает, что ссылка ни на что не ссылается.
Уничтожение объектов (освобождение занимаемой ими памяти) происходит автоматически, когда на объект не остается ни одной ссылки. Этим занимается сборщик мусора – специальный модуль среды исполнения .NET. Уничтожение объекта происходит не именно в тот момент, когда пропала последняя ссылка на него, а когда это удобно сборщику мусора (сборщик мусора работает в отдельном потоке, т.е. параллельно с самой программой). На самом деле, управление временем жизни объекта в C# несколько сложнее, но мы не будем сейчас рассматривать этот вопрос. Сборщик мусора можно вызвать явно (GC.Collect()), но без крайней необходимости это делать нежелательно.
r1 = null;
r2 = null;
// Все ссылки на объект пропали, его можно уничтожить
Переменные, относящиеся к типам-значениям хранятся в стеке и уничтожаются сразу же при выходе из области видимости (без использования сборщика мусора). Такие переменные сравниваются как значения, а при присваивании копируются.
int a = 5;
int b = a; // Значение 5 копируется
a = 7;
if (a - b > 0)
{
int x = 5;
}
// Здесь переменная x будет уничтожена и память будет освобождена
Типы-значения могут превращаться в объекты и обратно с помощью механизма создания объектного образа (упаковки, boxing) и восстановления из объектного образа (распаковки, unboxing). Для каждого из простых типов существует соответствующий класс, в который при необходимости превращается обычная переменная в стеке.
int x = 5;
// Здесь x превращается в объект и для него вызывается
// метод ToString()
Console.WriteLine(x.ToString()); //5 происходит упаковка (boxing)
// Можно писать даже так
Console.WriteLine(7.ToString()); // 7
object o = x; // Происходит упаковка в объект в куче
// Создается новый объект, а не ссылка на значение в стеке
x = 8;
Console.WriteLine(o); // 5
Console.WriteLine(x); // 8
// z - обычное значение в стеке,
// которое будет уничтожено при выходе из области видимости
// сам же объект o будет уничтожен сборщиком мусора
int z = (int)o; // Происходит распаковка (unboxing)
o = null; // o пропало, а z осталось
Console.WriteLine(z); // 5
Механизм упаковки и распаковки позволяет быстро работать с простыми типами, когда они хранятся как значения в стеке, но при необходимости использовать для них возможности классов.
В большинстве случаев упаковка и распаковка происходит неявно при присваивании, вызове методов и т.д. Нужно иметь в виду, что эта операция требует некоторого времени и избегать лишних упаковок и распаковок, там, где это не требуется. Например, если у вас есть ссылка на число через object, то не стоит для вызова ToString присваивать этот объект переменной типа int.
Строки
Отдельно нужно отметить класс string реализующий строки в .NET. Он является ссылочным типом, однако при работе очень похож на тип-значение. Для него переопределены операции == и !=, благодаря чему строки сравниваются как значения, а не как ссылки (такое же переопределение можно сделать и для своих классов):
string x = "abc";
string y = "abc";
if (x == y) Console.WriteLine("x = y");
if (x != "zzz") Console.WriteLine("x != zzz");
В результате работы этого кода будут выданы строки:
x = y
x != zzz
При копировании же, строки копируются как ссылки, т.е. если написать
string z = y;
то получится две ссылки на один и тот же объект в памяти.
Чтобы не вносить путаницы, все операции, модифицирующие строку, не трогают текст строки, а создают для модифицированной строки новый объект в памяти и возвращают ссылку на него. Такие типы называются неизменяемыми (immutable).
Фиксированные строки в C# хранятся в хэш-таблице, где ключем является текст строки, а значением – ссылка на объект класса string. Если в программе появляется такая же строка, то новый объект не создается. Благодаря этому x, y и z из предыдущего примера будут ссылаться на один и тот же объект в памяти. Этот механизм называется интернированием строк и позволяет более экономно расходовать память и быстрее сравнивать строки.
Если же строки создаются динамически, то они не попадают в хэш-таблицу, но их можно поместить туда методом Intern.
Для строк существует специальная константа string.Empty – обозначающая пустую строку и позволяющая писать код более красиво. Она имеет значение "" и казалось бы должна быть интернирована. Однако, начиная с .NET 2.0, проверка будет выдавать false
Дело в том, что начиная с .NET 2.0 можно указывать для сборок атрибут [CompilationRelaxations], отключающий интернирование. Дело в том, что в системных библиотеках, где описан класс string, есть множество строк, которые редко сравниваются между собой (например, сообщения об ошибках) и интернирование там больше вредит, т.к. отнимает лишнюю память.
Операция сложения строк при предназначена для разовых операция сложения строк, но плохо подходит для сложения большого количества строк, т.к. происходит много лишних действий.
DateTime t = DateTime.Now;
string s = "Беликов";
s += " Сергей"; // Создается новый объект, куда копируется значение s,
Это одна из разновидностей алгоритма маляра Шлемеля (описана в книге Джоеля Спольски).
Босс поручил маляру Шлемелю покрасить разметку на дороге. В первый день он притащил бидон с краской и покрасил 300 м. дороги. «Это просто отлично» - сказал его босс, «ты шустрый малый!» и заплатил ему. На следующий день Шлемель покрыл только 150 метров дороги. «Ну, это не так хорошо как вчера, но все равно неплохо», сказал его босс и заплатил ему. На следующий день Шлемель покрыл 30 метров дороги. «Только 30!» вскричал его босс, «Это невозможно! В первый день ты сделал в 10 раз больше работы! Что происходит?» «Я ничего не могу с этим поделать», ответил Шлемель. «Каждый день я удаляюсь дальше и дальше от бидона с краской!».
Чтобы избегать подобных ситуаций, нужно четко представлять себе, как работает программа на низком уровне. Особенно – что происходит с памятью.
Чтобы избежать этой проблемы при объединении строк в .NET есть специальный класс System.Text.StringBuilder. Он решает эту задачу намного эффективнее, т.к. не производит лишних копирований.
t = DateTime.Now;
StringBuilder sb = new StringBuilder();
sb.AppendLine("Беликов");
sb.AppendLine("Сергей");
sb.AppendLine("Владимирович");
sb.AppendLine("19.05.1980");
for (int i = 0; i < 20000; i++)
{
sb.Append(i);
sb.Append("; ");
}
s = sb.ToString();
ts = DateTime.Now.Subtract(t);
Console.WriteLine(ts.TotalMilliseconds); // 15.6
Более того этот класс намного удобнее в использовании. Append и AppendLine позволяют преобразовывать разные типы данных в строки. Также есть метод AppendFormat, позволяющий форматировать данные.
Вложенные классы
namespace NestedClasses
{
class XSomeClass
{
private int b = 0;
/// <summary>
/// Вложенный класс
/// </summary>
protected class XNestedClass
{
public int a = 0;
private int c = 0;
public void f(XSomeClass nest)
{
// Вложенные классы видят все атрибуты и методы того класса,
// в который они вложены
nest.b = 5;
Console.WriteLine("a = " + a.ToString());
}
}
/// <summary>
/// Вложенный класс
/// </summary>
public class XPublicNestedClass
{
public int a = 0;
public void f()
{
Console.WriteLine("a = " + a.ToString());
}
}
public void g()
{
XNestedClass nestedClass = new XNestedClass();
nestedClass.a = 5;
// zNestedClass.c = 3; // ошибка, т.к. класс видит только public
// атрибуты и методы своих вложенных классов
nestedClass.f(this);
Console.WriteLine("b = " + b.ToString());
}
}
class Program
{
static void Main(string[] args)
{
// ошибка, т.к. класс protected
// XNestedClass nestedClass = new XNestedClass();
XSomeClass someClass = new XSomeClass();
someClass.g();// a = 5 b=5
// public классы можно использовать
XSomeClass.XPublicNestedClass publicNestedClass =
new XSomeClass.XPublicNestedClass();
publicNestedClass.f(); // a=0
}
}
}
Вложенные классы обычно используются, когда классы неразрывно связаны друг с другом и не имеют смысла по отдельности. Например, вложенными классами (возможно private) удобно сделать служебные классы для реализации внутренностей класса (например, при реализации списка на ссыклках – класс для элемента списка, содержащий ссылку на следюущий элемент).
Ссылка this
Во всех методах класса, кроме статических, доступна ссылка this на объект этого класса, для которого вызывается метод. Для статических методов он не существует, т.к. они могут вызываться без объекта.
Ссылка this может использоваться, например, чтобы передать указатель на экземпляр класса при вызове метода другого класса, хотя это достаточно редкая ситуация.
// Обратите внимание, что мы можем обращатья к private атрибутам
// другого объекта того же класса. Это не нарушает принцип инкапсуляции
// т.к. мы работаем в рамках того же класса и понимаем, что происходит.
element._prev = this;
}
}
То, что мы в самом классе XElement объявляем атрибуты типа XElement вполне нормальная ситуация, поскольку мы не привязаны к обработке текста компилятором, как в случае традиционных языков. Впрочем, некоторые компиляторы C++ также понимают подобные объявления.
Пример
Чтобы продемонстрировать практическое применение интерфейсов, наследования и полиморфизма, продолжим развитие примера с автоматами. Пусть в игре, помимо автоматов есть пистолеты и ножи, а также солдаты, которые могут использовать это оружие. Добавленный в предыдущем примере подсчет общего количества выстрелов в данном примере уберем.
/// <summary>
/// Обмундирование - то что можно экипировать. Все обмундирование
/// обладает весом.
/// Для отрисовки в инвентаре в текстовом режиме можно было бы использовать
/// ToString(), но мы сделаем абстрактный метод, чтобы подемонстрировать
/// абстрактные методы
/// </summary>
abstract class XEquipment
{
protected double _weight = 0.0;
public XEquipment(double weight)
{
_weight = weight;
}
/// <summary>
/// Вес. Сделан виртуальным, чтобы можно было учитывать кол-во патронов.
/// </summary>
public virtual double Weight
{
get
{
return _weight;
}
}
/// <summary>
/// Отрисовка в инвентаре
/// </summary>
public abstract void Draw();
/// <summary>
/// Пустой объект (паттерн null object). Удобно использовать, чтобы не
/// проверять каждый раз на null (будет понятно далее).
/// Внешние пользователи не должны о нем знать, чтобы не смогли создать
/// другой объект этого класса, поэтому он private.
// Чтобы сделать перегруз, добавим очень тяжелый автомат
XRifle m1000 = new XRifle(100, 30, 0.05);
m1000.SwitchMode();
Ivan.Equip(m1000); // Вы не можете больше поднять
Ivan.Equip(new XPistol(10, 1, 0.01));
Ivan.Equip(new XPistol(10, 1, 0.01));
Ivan.Equip(new XPistol(10, 1, 0.01));
Ivan.Equip(new XPistol(10, 1, 0.01)); // Нет места в рюкзаке
Console.ReadLine();
}
}
То, что автоматы наследуются от пистолетов может быть не совсем естественно, но в данном случае приемлимо. Если пистолеты обретут свойства, которых нет у автоматов (например, возможность использования глушителя), то нужно будет сделать абстрактный класс «Огнестрельное оружие» и сделать пистолеты и автоматы его наследниками.
Рассмотим, какие преимущества нам дало использование интерфейсов, абстрактных классов и полиморфизма.
Благодаря наследованию от абстрактного класса XEquipment и использованию XEmpty мы можем единообразно обращаться со всеми предметами в игре, отрисовывая их в консоли, добавляя их к списку предметов в инвентаре и вычисляя суммарный вес. Благодаря полиморфизму дочерние классы могут реализовать свои варианты отрисовки и вычисления веса (например, с учетом веса патронов). Абстрактный класс здесь удобнее, чем интерфейс, т.к. он предоставляет простейшую реализацию учета веса.
Благодаря интерфейсу IWeapon мы можем отличить предметы, которые являются оружием от других предметов и хранить разные виды оружия под одной ссылкой _CurrentWeapon.
Получившаяся объектная модель легко расширяема. Чтобы добавить в игру новый вид оружия достаточно просто добавить еще один класс наследник XEquipment и реализовать в нем IWeapon. Никаких изменений в уже написанные классы вносить не нужно.
Объектная модель выглядит просто и ястественно, с ней легко и удобно работать. И в то же время, она релизует относительно непростую логику. Если вы попробуете написать такой же пример без использования ООП, он получится существенно сложнее.
Структуры
Структуры – это тип данных, относящийся к значимым типам (value type), который позволяет объединять атрибуты разных типов и методы. Структуры во многом похожи на классы, но обладают рядом особенностей:
1. Структуры это значимый тип
Они хранятся в стеке. При копировании происходит поэлементное копирование. Могут создаваться без использования new.
2. Структуры могут иметь конструктор с параметрами, но не может иметь деструктора
В отличие от классов, если объявлен конструктор с параметром, то все равно можно использовать конструктор без параметра. Переопределить конструктор без параметров нельзя.
3. В структурах может использоваться инкапсуляция
4. Структуры могут содержать статические атрибуты и методы
5. Структуры могут наследоваться от интерфейсов, но не могут наследоваться от классов и других структур
Но неявно наследуются от object и ValueType
6. От структур нельзя наследоваться (они являются sealed типом)
interface IInterface
{
void SomeMethod();
}
// Структура может наследоваться от интерфейса
// но не может наследоваться от классов.
// Хотя неявно наследуется от object и ValueType
struct XStruct : IInterface
{
private int _x;
public int _y;
public static int _z = 0;
// Конструктор без параметров объявлять нельзя
//public XStruct()
//{
// _x = 0;
// _y = 0;
//}
// Конструктор с параметрами объявлять нельзя
public XStruct(int x, int y)
{
_x = x;
_y = y;
}
// Деструкторов у структур не бывает
//public ~XStruct()
//{
//}
public override string ToString()
{
return _x.ToString() + "," + _y.ToString();
}
public void DoSomething()
{
Console.WriteLine(this.ToString());
}
public void SomeMethod()
{
}
}
XStruct._z = 3;
// Не смотря на new, это значение в стеке
XStruct s1 = new XStruct(1, 2);
// Происходит копирование
XStruct s2 = s1;
s2._y = 5;
s1.DoSomething(); // 1,2
s2.DoSomething(); // 1,5
// Можно не использовать new
XStruct s3;
s3._y = 1;
// Структура является неявным наследником класса ValueType,
// который является наследником objet
Console.WriteLine(s1 is ValueType); // true
int x;
Console.WriteLine(x is ValueType); // true
Console.WriteLine(s1 is object); // true
object obj = s1; // boxing
Структуры удобно использовать, когда нужен облегченный класс, не использующий наследование, для которого логично хранение в стеке и копирование как типа-значения. Примером структур в .NET являются точки Point и цвета Color.
Делегаты
Делегаты – это ссылки на методы класса. Относятся к ссылочному типу (reference type).
namespace DelegateExample
{
class Program
{
class IntegralCalculator
{
/// <summary>
/// Делегат для функции, интеграл которой вычисляем
/// </summary>
public delegate double FunctionDelegate(double x);
/// <summary>
/// Вычисляет интеграл указанной функции на отрезке [a, b]
/// </summary>
/// <param name="f">функция</param>
/// <param name="a">левая граница отрезка</param>
/// <param name="b">правая граница отрезка</param>
/// <param name="n">количество отрезков</param>
/// <returns>интеграл от функции</returns>
public static double Calculate(FunctionDelegate f,
// Точное значение 10, но с шагом 100 получается 9.999999
Console.WriteLine(i2.ToString());
// Анонимные делегаты (только в .NET 2.0)
double i3 = IntegralCalculator.Calculate(
delegate (double x)
{
return 2*x;
}
, 0, 10, 100);
Console.WriteLine(i3.ToString()); // 100
double i4 = IntegralCalculator.Calculate(
delegate
{
return 5;
}
, 0, 10, 100);
Console.WriteLine(i4.ToString()); // 50
}
}
}
}
Делегат это не просто аналог указателя на функцию в C++ или других языках, делегаты C# - это полноценные объекты, способные хранить и использовать методы объектов.
Делегаты используются, когда нужно делегировать часть функций некоторого класса другим классам, а создавать для этого специальный класс слишком накладно.
Делегаты позволяют создавать хорошо инкапсулированные классы, которые выполняют только те функции, которыми они должны заниматься.
Например, хорошим примером использования делегата является отображение полосы выполнения (Progress bar) для какого-то внутреннего метода системы, который выполняется достаточно долго. Передавать ссылку на ProgressBar в этот метод будет неправильно, т.к. мы нарушим принцип инкапсуляции – внутренний класс будет знать о пользовательском интерфейсе системы и его нельзя будет использовать, например, в консольном приложении. Более правильно передать в этот метод делегат для отображения прогресса. В консольном приложении через него можно будет передать ссылку на метод, который рисует * в консольном окне, в windows-приложении передать ссылку на метод обновляющий прогресс-бар, а в проекте без пользовательского интерфейса передать ссылку на метод, который ничего не делает или оставить ее null.
В C# делегаты используются в многопоточности (чтобы передавать метод, который должен исполняться в отдельном потоке) и для реализации системы событий.
Анонимные делегаты
В .NET 2.0 появилась возможность создавать анонимные делегаты – ссылающиеся просто на блок кода, а не на метод класса.
delegate int SomeDelegate(int x);
class Program
{
static int TestClosure(SomeDelegate a_Delegate)
{
int y = 3;
// этот y не будет использоваться
return a_Delegate(2);
}
static void Main(string[] args)
{
// сохраняем ссылку на следующий участок кода в виде делегата
SomeDelegate x2 = delegate(int x)
{
return x * x;
};
int y = 5;
SomeDelegate c = delegate(int x)
{
// Делегат может обращаться к переменным, объявленным вне него
return x * x * y;
};
Console.WriteLine(x2(2).ToString()); // 4
Console.WriteLine(c(2).ToString()); // 20
Console.WriteLine(TestClosure(c)); // 20
Console.ReadLine();
}
}
Анонимные делегаты представляют собой замыкания (closure) – процедуру, которая ссылается на переменные, описанные вне нее.
Анонимные делегаты – это очень интересная возможность. По сути, они представляют собой объект, описывающий участок кода. Такое позволяют делать очень немногие языки (например, LISP, в котором есть лямбда-выражения). В .NET 3.0. уже можно использовать настоящие лямбда-выражения (безымянные функции, описываемые в месте их непосредственного использования). С использованием анонимных делегатов метод из предыдущего примера можно вызвать так:
TestClosure(
delegate(int x)
{
return x * x * y;
}
);
А с использованием лямбда выражений (и при условии, что x описана выше) это выглядит так:
TestClosure(
return x * x * y;
);
Хотя компилятор в IL-код все равно сделает из лямбда-выражения анонимный делегат (в целях совместимости кода с .NET 2.0).
Анонимные делегаты удобно использовать, когда нужно получить ссылку на некоторый участок кода, но делать для этого отдельный метод неоправданно. Это, в свою очередь позволяет делать достаточно сложную передачу управления между участками кода, которую раньше можно было реализовать только с помощью goto.
Паттерн команда (Command)
Более мощной альтернативой делегатам является использования специального класса-команды, реализующего некотое действие.
// Абстрактный класс, от которого наследуются команды
abstract class XAbstractComand
{
// Объект-который будет выполнять команду
protected object _receiver;
public XAbstractComand(object receiver)
{
_receiver = receiver;
}
public abstract void Execute();
}
// класс, который будет выполнять команды
class XReceiver
{
public void DoIt()
{
Console.WriteLine("Сделай это");
}
public void DoSomethingElse()
{
Console.WriteLine("Сделай то");
}
}
// Конкретная команда
class XConcreteCommandDoIt : XAbstractComand
{
public XConcreteCommandDoIt(XReceiver receiver)
: base(receiver)
{
}
public override void Execute()
{
(_receiver as XReceiver).DoIt();
}
}
// Конкретная команда
class XConcreteCommandDoSomethingElse : XAbstractComand
{
public XConcreteCommandDoSomethingElse(XReceiver receiver)
: base(receiver)
{
}
public override void Execute()
{
(_receiver as XReceiver).DoSomethingElse();
}
}
// Класс, который будет обращаться к команде
class XInvoker
{
// В качетве аргумента мы передаем команду.
// Какая конкретно команда будет туда подставлена, мы не знаем
// Это напоминает использование делегатов
public void InvokeCommand(XAbstractComand command)
Как можно заметить, команды очень похожи на делегаты. Данный пример достаточно прост и его можно было бы реализовать и с помощью делегатов, однако использование команд дает ряд преимуществ. Команды могут содержать в себе не только действие, но и обратное действие (undo). В этом случае можно составлять целые очереди из разных команд с возможностью отмены (undo). Также в команды можно добавить дополнительные параметры, если это необходимо. Вообще, представление действия в виде отдельного объекта дает множество преимуществ.
Команды можно сделать сериализуемыми для передачи по сети, чтобы передать их, например, серверу для выполнения. Или же сохранить их на диск для последующего выполнения. Такой подход используется в Smart Client – приложениях, поддерживающих offline работу с сервером. Когда сервер недоступен, они копят команды в локальном хранилище. При подключении к серверу, команды из хранилища посылаются ему на выполнение. Очерди команд можно оптимизировать, объединяя команды. Например, если после редактирования элемента пользователь выполнил его удаление, то на редактирование можно не тратить время.
Команды позволяют сделать классы XInvoker и XReceiver независимыми друг от друга и при необходимости заменять как обработчика команд, так и класс, который они вызывают. При прямом обращении к методам друг друга это невозможно.
Использование команд лучше отражает концепцию ООП о том, что объекты общаются между собой посредством сообщений. В некоторых системах все взаимодействие между объектами реализуют подобным образом (обычно еще используя паттерн подписчик – Subscriber и систему событий).
Также этот паттерн называют Action (действие) ил Transaction (транзакция).
События
На основе делегатов в .NET реализован более интересный вариант взаимодействия между объектами – события. Один из объектов является генератором события, а другие объекты, подписываются на сообщения о событиях и реагируют на них с помощью обработчиков.
В коде это выглядит следующим образом:
// Делегат для обработчика событий
delegate void XEventHandler(int x);
// Класс, который генерирует события
class XEventGenerator
{
public event XEventHandler _event = null;
public void RaiseEvent(int x)
{
if (_event != null)
_event(x);
}
}
// Класс, который реагирует на события
class XEventListener
{
public void React(int x)
{
Console.WriteLine("Слышу");
Console.WriteLine(x);
}
}
XEventGenerator g = new XEventGenerator();
XEventListener l1 = new XEventListener();
// Связываем обработчик события с событием
g._event += new XEventHandler(l1.React);
XEventListener l2 = new XEventListener();
// Связываем обработчик события с событием
g._event += new XEventHandler(l1.React);
// Возникает событие
g.RaiseEvent(1);
// Объекты реагируют на это событие
// Слышу
// 1
// Слышу
// 1
// Отсоединяем обработчик от события
g._event -= l1.React;
g.RaiseEvent(2);
// Слышу
// 2
Т.е. вызов события приводит к вызову всех делегатов, которые с ним связаны.
Самое распространенное использование событий – при разработке графических пользовательских интерфейсов. С каждым элеентом управления на форме связан набор событий, которые возникают, когда пользователь взаимодействет с элементом управления. Программа может реагировать на действия пользователя, связывая обработчики событий с соответствующими событиями.
Однако события могут использоваться и в крупных системах для взаимодействия между объектами, когда об изменении одного объекта должны узнать другие объекты.
Обобщенные классы
В .NET 2.0 появилась возможность создавать так называемые обобщенные (generic) классы, методы, делегаты и события, которые в своем описании содержат набор параметров, вместо которых можно подставлять другие типы. Благодаря CLR, создание типа и все проверки происходят на лету в процессе выполнения и он полностью доступен для рефлексии (reflection).
Обобщенные классы описываются следующим образом:
// Обобщенный список
// T - тип элемента, хранящегося в списке
public class GenericList<T>
{
// Вложенные классы тоже могут использовать параметр
private class Node
{
public Node(T t)
{
_next = null;
_data = t;
}
private Node _next;
public Node Next
{
get
{
return _next;
}
set
{
_next = value;
}
}
private T _data;
public T Data
{
get
{
return _data;
}
set
{
_data = value;
}
}
}
private Node _head;
public GenericList()
{
_head = null;
}
// добавление элемента в список зависит от параметра
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = _head;
_head = n;
}
}
class Program
{
static void Main(string[] args)
{
// Объявляем список целых чисел
GenericList<int> zIntList = new GenericList<int>();
// теперь мы можем добавлять в список целые числа
// при этом. поскольку список типизированный,
// добавить туда, например, строки, будет уже не получится
zIntList.AddHead(1);
zIntList.AddHead(2);
zIntList.AddHead(3);
Console.ReadLine();
}
}
Мы можем предполагать что, тип T реализует некоторый интерфейс, например такой:
// Интерфейс Print с единственным методом Print
public interface IPrintable
{
void Print();
}
и использовать его в методе Print
public void Print()
{
Node n = _head;
while (n != null)
{
((IPrintable)n.Data).Print();
n = n.Next;
}
}
В этом случае мы по-прежнему можем объявить
GenericList<int> zIntList = new GenericList<int>();
Но при вызове метода Print возникнет ошибка приведения типов, т.к. int не является наследником IPrintable. Если же подставить в качестве T класс, реализующий интерфейс IPrintable, то все будет работать. Чтобы ограничить набор типов, которые можно подставлять в качестве параметра, используется конструкция where.
public class GenericList<T> where T : IPrintable
В этом случае в качестве T можно будет подставлять только классы и интерфейсы, наследующиеся от интерфейса IPrintable.
Можно указать, что T должен быть ссылочным типом (классом, интерфейсом, делегатом или массивом)
public class GenericList<T> where T : class
Можно указать, что T должен быть не обнуляемым типом-значением (например, структурой)
public class GenericList<T> where T : struct
В этом случае int подставить будет нельзя, т.к. он не является ссылочным типом
Можно указать, что T должен иметь public конструктор без параметров.
public class GenericList<T> where T : new()
Обобщенные классы могут иметь несколько параметров. В этом случае они перечисляются через запятую.
public class GenericList<T1, T2>
Если нужно использовать где-то внутри обобщенного класса значение по умолчанию, то можно использовать конструкцию default(T), которая возвращает значение по умолчанию для указанного типа (null для ссылок, 0 для int и т.д.)
Коллекции
Типичной задачей при создании объектной модели некоторой предметной области является хранение набора однотипных объектов в виде списка, стека, дека, очереди, динамического массива или другой динамической структуры. Такие динамические структуры называются коллекциями. В .NET существует целый набор классов и интерфейсов для создания коллекций. В .NET 2.0 используются обобщенные варианты этих классов и интерфейсов, а в более ранних версиях .NET используются коллекции object, к которому приводятся любые типы .NET.
Базовым интерфейсом для всех коллекций является интерфейс IEnumerable, в котором описан единственный метод, возвращающий enumerator коллекции:
IEnumerator<T> GetEnumerator();
Enumerator – это специальный класс, позволяющий перемещаться по элементам коллекции с помощью следюущих методов:
void Reset (); // Сбросить позицию энумератора на начало
// (перед первым элементом)
bool MoveNext(); // Перейти к следующему элементу
T Current { get; } // Получить текущий элемент
При любом изменении в списке все его enumerator’ы, как правило, сбрасываются (выдают ошибку при попытке вызвать MoveNext), чтобы избежать некорректного обхода списка.
Чтобы сделать класс GenericList из предыдущего раздела перечислимым (enumerable) нужно реализовать в нем интерфейс IEnumerable и создать класс, реализующий IEnumerator<T>, объект которого, привязанный к конкретному списку будет создаваться в методе GetEnumerator(). В целях совместимости обобщенный интерфейс IEnumerable<T> наследуется от IEnumerable и класс должен реализовывать два метода – возвращающий обычный IEnumerator и обобщенный IEnumerator<T>. Реализовать все это с помощью дополнительных классов не так сложно, и оставляется на самостоятельную проработку. Мы же рассмотрим более простой способ добиться такой же функциональности, появившийся в .NET 2.0 – оператор yield return.
// public здесь не используется, т.к. мы явно указали, что
// реализуем метод интерфейса
IEnumerator IEnumerable.GetEnumerator()
{
Node current = _head;
while (current != null)
{
// Оператор yield return возвращает значение
// из текущей итерации энумератора
// При следующем вызове MoveNext этот метод продолжит
// работу с этого места
yield return current.Data;
current = current.Next;
}
}
public IEnumerator<T> GetEnumerator()
{
Node current = _head;
while (current != null)
{
// Оператор yield return возвращает значение
// из текущей итерации энумератора
// При следующем вызове MoveNext этот метод продолжит
// работу с этого места
yield return current.Data;
current = current.Next;
}
}
После описания таких методов компилятор самостоятельно генерирует необходимые классы-наследники IEnumerator. Однако для них будет недоступна операция Reset.
Для классов, наследующихся от IEnumerable можно использовать конструкцию for each для обхода коллекции:
foreach (int x in zIntList)
{
Console.WriteLine(x);
}
Что эквивалентно записи:
IEnumerator<int> e = zIntList.GetEnumerator();
int y;
while (e.MoveNext())
{
y = e.Current;
Console.WriteLine(y);
}
С помощью yield можно реализовать простую коллекцию даже так:
class XSimleIntCollection : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
yield return 1;
yield return 2;
yield return 3;
}
IEnumerator IEnumerable.GetEnumerator()
{
yield return 1;
yield return 2;
yield return 3;
}
}
class Program
{
static void Main(string[] args)
{
XSimleIntCollection c = new XSimleIntCollection();
foreach (int x in c)
{
Console.WriteLine(x);
}
}
}
С помощью энумераторов можно легко реализовать различные варианты обхода списков или, например, фильтрацию, возвращая энумертор, который обходит не все элементы списка, а только некоторые.
На самом деле, для работы foreach не требуется строгое наследование от интерфейса – достаточно чтобы класс просто содержал метод GetEnumerator.
Полноценная колелкция должна поддерживать не только обход всех элементов, но и добавление и удаление элементов. Для этого в .NET Framework есть интерфейс ICollection<T> добавляющий к IEnumerable<T>, от которого он наследуюется следующие методы:
// Количество элементов
int Count { get; }
// Только для чтения
bool IsReadOnly { get; }
// Добавить элемент в коллекцию
void Add (T item);
// Проверить, содержится ли в коллекции указанный элемент
bool Contains (T item);
// Копировать коллекцию в массив, начиная
// с указанного индекса
void CopyTo (T[] array, int arrayIndex);
// Удалить из коллекции указанный элемент
bool Remove (T item);
От ICollection наследуется интерфейс IList, добавляющий работу с элементами по индексам.
В пространствах имен System.Collections и System.Collections.Generic .NET Framework находится множество классов для работы с различными колелкциями – списками, стеками, очередями и т.д. Коллекции широко используются в .NET Framework.
Интерфейс IComparable
В качестве еще одного примера интерфейса рассмотрим интерфейс IComparable из .NET Framework 1.1. В .NET 2.0 добавлен обобщенный вариант этого интерфейса, но мы пока рассмотрим более простой вариант.
Описание этого интерфейса выглядит так:
interface IComparable
{
/// <summary>
/// Сравнивает объект с объектом obj
/// Если этот объект меньше obj, то возвращает отрицательное число
/// Если больше - положительное, если равен, то 0
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
int CompareTo(object obj);
}
Этот интерфейс уже есть в .NET, поэтому повторно его описывать не нужно.
Интерфейс не может содержать атрибуты.
public class XSomeClass : IComparable
{
private int _x;
public XSomeClass(int x)
{
_x = x;
}
public int CompareTo(object obj)
{
// Здесь дополнительно нужно проверить, а является ли obj
// объектом класса XSomeClass, но не будем усложнять пример
return _x - ((XSomeClass)obj)._x;
}
}
class Program
{
static void Main(string[] args)
{
IComparable s1 = new XSomeClass(5);
IComparable s2 = new XSomeClass(10);
// Console.WriteLine(s1 < s2);
// ошибка, операция < в классе не определена
Console.WriteLine(s1.CompareTo(s2)); // -5
}
}
Также как с Equals в object, реализация IComparable не дает возможность использовать > и <, их можно при необходимости реализовать отдельно, также как в примере на ==. IComparable позволяет помещать объекты в списки .NET
Массивы
Для реализации массивов в C# существует специальный класс – System.Array, таким образом, массивы являются ссылочным типом. Выход за границы массива отлавливается в процессе выполнения и генерируется исключительная ситуация (exception).
Работа с массивами происходит следующим образом:
int n = 5; // т.е. размер массива задается динамически
int[] a = new int[n]; // массив целых чисел
for (int i = 0; i < a.Length; i++)
{
a[i] = i;
}
Console.WriteLine(a[3]);//3
int[,] m = new int[n,n + 2]; // матрица целых чисел
Console.WriteLine(m.GetUpperBound(0)); // 4
Console.WriteLine(m.GetUpperBound(1)); // 6
for (int i = 0; i <= m.GetUpperBound(0); i++)
for (int j = 0; j <= m.GetUpperBound(1); j++)
m[i,j] = i + j;
Console.WriteLine(m[3,6]); // 9
int[][] d = new int[n][]; // массив массивов
for (int i = 0; i < d.Length; i++)
{
d[i] = new int[i];
for (int j = 0; j < d[i].Length; j++)
d[i][j] = j + 1;
}
for (int i = 0; i < d.Length; i++)
{
for (int j = 0; j < d[i].Length; j++)
{
Console.Write(d[i][j]);
Console.Write(" ");
}
Console.WriteLine();
}
// 1
// 1 2
// 1 2 3
// 1 2 3 4
Обобщенные методы и делегаты
Описание обобщенных делегатов и обобщенных методов похоже на описание обобщенных классов:
// Обобщенный делегат
delegate T MyDelegate<T>(T x) where T : struct;
class SomeClass
{
// метод, на который мы можем ссылаться с помощью обобщенного делегата
public int f(int x)
{
return 0;
}
// обобщенный метод
public void g<T>(T x) where T : class
{
Console.WriteLine(x.ToString());
}
}
SomeClass c = new SomeClass();
// Подставляем в качестве параметра обобщенного делегата тип int
MyDelegate<int> x = new MyDelegate<int>(c.f);
// Подставляем в качестве параметра обобщенного метода тип double
c.g<double>(4.5);
// Можно использовать более простой тип записи, тогда среда исполнения
// сама определит подходящий тип
c.g(5.5);
Обнуляемые типы
В .NET 2.0 появилась возможность делать типы-значения обнуляемыми (nullable), что позволяет присваивать им null.
// Объявляем переменную nullable-типа
Nullable<int> x = 5;
// Доступ к значению происходит через свойство Value
Console.WriteLine(x.Value.ToString()); // 5
// При этом тип все равно остается типом-значением
Nullable<int> y = x; // происходит копирование значения
y = 7;
Console.WriteLine(x.Value.ToString()); //5
// Обнуляемым типам можно присваивать null
x = null;
y = null;
// Более короткий способ объявления обнуляемого типа
int? z = 15;
Console.WriteLine(z.Value.ToString());
z = null;
Обнуляемые типы удобны, например, когда вы считываете некоторое поле из базы данных, которое может быть null и хотите сохранить его, например, в атрибуте класса. Раньше для этого приходилось либо использовать «волшебные числа», например считать что int.MaxValue – это null (плохой способ), либо самостоятельно реализовывать аналог nullable-типа (тоже не очень эффективно, т.к. вы лишаетесь преимуществ типа-значения).
int? i = null;
// Для обнуляемых типов реализована операция ??,
// которая возвращает значение если тип - не null
// и указанное значение, если null
int j = i ?? 10; // 10
Методы-расширения (extension methods)
В .NET 3.5 появилась возможность добавлять методы к уже существующим классам, интерфейсам, структурам и перечислениям (enum). Это так называемые методы-расширения. Они описываются в статических классах следующим способом:
// Класс, в который хотим добавить метод
class XExtendableClass
{
private int x = 1;
public int f()
{
return x;
}
}
// Класс с методами расширениями
static class XExtensionClass
{
// Метод расширение должен быть статическим и содержать
// атрибут типа расширяемого класса
// с ключевым словом this
public static void g(this XExtendableClass x, string a)
{
// x может быть null
if (x != null)
{
// Console.WriteLine(s.x);
// ошибка, доступа к закрытым атрибутам нет
Console.WriteLine(a + x.f().ToString());
}
}
// Можно расширять и методы стандартных классов
public static string Translate(this string s)
{
if (s == null) return null;
return s.Replace("я", "йа");
}
}
XExtendableClass e = new XExtendableClass();
e.g("x = ");
string s = "яд";
// Этот метод появится у класса string
Console.WriteLine(s.Translate());
Методы расширения работают даже для ссылок типа null, поэтому в них нужно обязательно проверять аргумент на null. Это можно использовать, например, для реализации метода IsNullOrEmpty, который проверяет значение объекта на null или некоторое пустое значение.
Методы-расширения – это удобный способ добавлять в классы новую функциональность, когда использование наследования неудобно и не оправдано. Например, с помощью расширений в коллекции .NET добавлены возможности LINQ.
Методы-расширения это один из вариантов замены класса вспомогательных статических методов (helper).
Методы расширения можно использовать и для констант. Например, можно написать расширения для типа int, чтобы писать такие выражения:
public static DateTime DaysAgo(this int x)
{
return DateTime.Now.AddDays(-x);
}
DateTime d = 3.DaysAgo();
Методы-расширения можно присваивать делегатам.
Особенно интересно использование расширений с интерфейсами. Оно позволяет добавлять новые методы ко всем классам, реализующим интерфейс.
Другие механизмы ООП
В некоторых языках существуют более сложные объектные системы.
Метакласс – класс классов.
Класс для метакласса – объект. Метакласс для класса – класс.
Различают:
· одноуровневые системы – в которых объект является классом
· двухуровневые системы – есть классы и объекты
· трехуровневые системы – есть объекты, классы и метаклассы
· пятиуровневые системы - есть объекты, классы, класс-классы (описывают взаимодействие классов), метаклассы, метакласс-классы (описывают взаимодействие метаклассов). Пример такой системы - SmallTalk.
C++ - двухуровневая система. Но в нем есть шаблоны – классы с параметрами, которые являются частным случаем метакласса.
Метаобъектный протокол (МОП) – механизм, позволяющий объектам управлять поведением классов.
Под управлением понимается возможность получать информацию о классе и изменять класс. Системы, в которых реализован метаобъектный протокол называются рефлексивными.
Объектно-ориентированный язык должен обладать 4 обязательными свойствами:
1. Абстракция (есть классы и объекты).
2. Инкапсуляция (ограничение доступа ко внутренней структуре объектов).
3. Модульность (возможность описания классов в отдельных модулях).
4. Иерархичность (возможность наследования)
и 3 дополнительными:
5. Типизированность (контроль над типом объектов во время жизни)
6. Параллелизм (многопоточность)
7. Перманентность (способность объектов сохранять свое состояние на носителе информации).
Типизированность дает возможность полиморфизма, поскольку чтобы работал полиморфизм нужно уметь определять тип объекта в процессе работы программы (ведь заранее нельзя сказать объект какого класса будет создан).
Для ЯП, базирующиеся на объектах – все это не обязательно. Обычно там есть классы и объекты (без наследования). Пример такого языка – Visual Basic вплоть до 6 версии (не .NET).
C# - ООП язык.
C++ - это объектно-ориентированныя язык. Есть 4 обязат. св-ва и типизируемость (механизм RTTI – real time type information).
Обработка ошибок. Исключения (exceptions)
Для обработки ошибок в C# существует специальный механизм – исключения (exceptions). Исключение — это отклонение от нормального выполнения программы. Исключения могут возникать в самой программе, например при делении на ноль или же в функциях и методах внешних библиотек, вызываемых из программы.
try
{
// Блок, в котором может возникнуть исключение
}
catch (SomeException e)
{
// Обработка исключений типа SomeException или дочерних исключений
}
catch (Exception e)
{
// Здесь будут обработаны исключения всех типов,
// т.к. все исключения наследуются от Exception
}
finally
{
// блок, который выполняется в любом случае, возникла ошибка или нет
}
Блок finally (если он описан) выполняется в любом случае, не зависимо от того, возникло исключение или нет. Если возникает исключение, код описанный в блоке finally выполняется до блоков catch, которые обрабатывают исключения. В блоке finally обычно помещается код, который освобождает ресурсы, выделенные в блоке try, поскольку ресурсы нужно освободить в любом случае, произошла ошибка или нет.
При возникновении ошибки выкидывается (throws) исключение, которое возвращается по вызовам функций вверх до тех пор, пока не будет поймано обработчиком catch данного типа исключений. Блоки catch обрабатываются последовательно. Если возникшее исключение подходит под тип, указанный в локе catch, то управление передается в этот блок, если нет – проверяется следующий блок catch. Если исключение не соответствует ни одному описанию, указанному в блоке catch, то оно проходит дальше до тех пор, пока оно не попадет в соответствующий блок catch или не выйдет за пределы функции main(). В этом случае программа завершит работу с ошибкой «необработанное исключение» (unhandled exception).
Чтобы гарантированно поймать все возможные исключения, в конце добавляется блок с типом Exeption, который, благодаря наследованию отловит исключения всех типов. Если доступ к полям класса Exception не нужен, то можно просто не указывать тип исключения
catch
{
// Здесь будут обработаны исключения всех типов
}
Если не указать в блоке catch тип исключения, то туда попадут исключения всех типов.
Обработка исключений позволяет обработать ситуации некорректной работы программы и продолжить нормальное функционирование программы или же завершить ее работу, если продолжение работы невозможно.
В многопоточных приложениях существует несколько потоков управления и исключения также передаются в параллельных потоках. Например, если поставить обработчик исключений в функции main, который должен отлавливать все исключения, то если исключение возникнет в отдельном потоке, то оно не будет поймано.
В крупных системах, как правило, описывается собственный набор исключений, наследующихся от Exception. Рассмотрим пример описания собственного исключения:
namespace ExceptionsExample
{
/// <summary>
/// Собственный класс для исключений.
/// </summary>
class XMyException : Exception
{
/// <summary>
/// Наследуем конструктор по сообщению об ошибке
/// </summary>
/// <param name="_message">Сообщение об ошибке</param>
public XMyException(string message) : base(message)
{
}
/// <summary>
/// Наследуем конструктор по сообщению об ошибке и
/// внутреннем исключении
/// </summary>
/// <param name="message">сообщение об ошибке</param>
Хорошие программы должны следовать принципу Гиппократа – «не навреди». Применительно к программе это означает, что лучше пусть программа выдаст ошибку и завершит свою работу, чем неправильно обработает важные данные. Авторы книги «Программист-прогматик» выражают эту идею фразой «Мертвые программы не лгут».
Например, не очень хорошой практикой является исопользование блоков try catch, которые отлавливают все типы исключений в тех местах, где могут возникнуть исключения, и продолжать работу как ни в чем не бывало:
try
{
// Блок, в котором может возникнуть исключение
}
catch
{
// Ничего не делаем
}
// Продолжаем работу как будто ничего не произошло
Это можно делать только в очень простых случаях, когда понятно, какого рода исключения возникают и когда они действительно не мешают продолжить нормальную работу.
Разумеется, если можно обработать ошибку и продолжить нормальную работу – это нужно сделать и система исключений позволяет это сделать.
Как правило, код main помещается в блок try-catch, чтобы перехватить все исключения, которые не были перехвачены ранее. Те исключения, которые можно безболезненно обработать в других частях программы, лучше обработать там. В первую очередь нужно отслеживать с помощью исключений самые важные части программы, ошибки в которых недопустимы – операции с деньгами, удаление данных и т.п. а также те части, где возникновение исключений наиболее вероятно (например, пропало подключение к базе, файл не найден и т.п.).
Важно, чтобы ошибки обрабатывались корректно, т.е. пользователю выдавалось осмысленное сообщение об ошибке, а не системное сообщение о необработанном исключении. Если вы хотите добавить некоторую техническую информацию, то ее можно поместить в лог или в окно с сообщением об ошибке.
Механизм исключений в C# достаточно требователен к ресурсам, поэтому не стоит использовать исключения там, где можно обойтись простой проверкой.
Некоторые возможности платформы .NET. Работа с файловой системой
Библиотека классов .NET framework содержит множество классов, которые позовляют решать типичные задачи, возникающие при разработке. Одна из таких типичных задач – работа с файловой системой.
Классы для работы с файловой системой и потоковым вводом/выводом находятся в пространстве имен System.IO. Для работы с файлами и папками можно использовать абстрактный класс FileSystemInfo и его наследники – FileInfo (для работы с файлами) и DirectoryInfo (для работы с папками). Класс FileSystemInfo содержит основные параметры файла – имя и полный путь, дату создания, изменения и последнего доступ и другие атрибуты. Также в этом классе есть два метода:
Delete – удаляет файл или папку
Refresh – обновляет информацию о файле или папке в объекте. Это бывает полезно, когда с файлом работает другая программа. Например, если вы создадите объект FileSystemInfo, и кто-то изменить файл, то чтобы дата изменения отобразилась в объекте, нужно вызвать метод Refresh.
Пример:
FileInfo f = new FileInfo("c:\\test.txt");
Console.WriteLine(f.LastWriteTime.ToString());
Console.ReadLine();
// Если не вызвать метод Refresh,
// то дата будет та же самая, даже если изменить файл
f.Refresh();
Console.WriteLine(f.LastWriteTime.ToString());
Console.ReadLine();
Класс FileInfo добавляет методы для копирования и переносы файлов, а также для чтения из файла и записи в файл.
Класс DirectoryInfo добавляет методы для создания подпапок, переноса папки, а также получения списка файлов и папок в данной папке.
Пример:
// Получаем путь к папке System32.
// Аналогично можно получить доступ к папкам
// Мои документы, Program Files, Application Data и
// возвращающие только файлы или папки соответственно.
foreach (FileSystemInfo fsi in filesAndDirectories)
{
if (fsi is DirectoryInfo)
Console.Write("Папка: ");
else
Console.Write("Файл: ");
Console.WriteLine(fsi.Name);
}
Console.ReadLine();
FileInfo и DirectoryInfo хорошо подходят для хранения информации о файлах и папках в программе, однако иногда бывает нужно выполнить какое-то простое действие, например, просто скопировать файл или папку. Это проще сделать с помощью классов File и Directory. Например, создать текстовый файл и сохнранить в нем текст в файле можно одной строкой:
File.WriteAllText("c:\\test2.txt", "Война и мир");
Для доступа к отдельным дискам существует класс DriveInfo.
// Получаем список всех дисков (жесткие диски, cd, флэшки и т.д.)
DriveInfo[] drives = DriveInfo.GetDrives();
foreach (DriveInfo d in drives)
{
Console.WriteLine(d.Name);
Console.WriteLine(d.DriveType);
// Если диск не вставлен, то формат определить нельзя
if (d.IsReady) Console.WriteLine(d.DriveFormat);
}
Console.ReadLine();
Часто бывает нужно работать с путями к файлам и папкам и создавать для этого FileInfo нецелесообразно. Для таких целей существует класс-хелпер Path, содержащий набор полезных статических методов, например:
Также иногда бывает нужно отслеживать изменения в какой-нибудь папке. Для этих целей можно воспользоваться классом FileSystemWatcher, который может генерировать события, например при появлении файла в папке. Однако на практике он не очень надежен. При интенсивной работе с папкой, некоторые события могут не возникать (есть некоторые встроенные ограничения), поэтому надежнее использовать периодический опрос папки, например по таймеру.
К сожалению, даже при работе с этими классами действует ограничение на длину пути к файлу в 255 символов.
Чтение из файла и запись в файл
Для простых текстовых файлов можно воспрользоваться классами StreamReader и StreamWriter, получаемыми в результате вызова методов OpenText и CreateText классов FileInfo и File.
FileInfo f = new FileInfo("c:\\test.txt");
// Прочитаем содержимое файла
using (StreamReader sr = f.OpenText())
{
while (!sr.EndOfStream)
{
Console.WriteLine(sr.ReadLine());
}
sr.Close();
} // Здесь будет вызван dispose
// Запишем в файл что-нибудь другое
using (StreamWriter fs = f.CreateText())
{
fs.WriteLine("aa");
fs.WriteLine("bb");
fs.WriteLine("cc");
fs.Close();
}
Для более тонкого ввода-вывода существует класс FileStream, который позволяет работать с файлом на уровне байтов.
using (FileStream fs = f.Open(FileMode.Open))
{
// Читаем содержимое файла, как набор байтов
byte[] rawData = new byte[fs.Length];
fs.Read(rawData, 0, rawData.Length);
// С помощью класса Encoding превращаем эти байты в текст
Запись в файл происходит аналогично, если открыть файл в в режиме Write и использовать метод Write.
Потоки ввода-вывода можно также использовать для работы с данными в памяти (MemoryStream), чтобы, например, подготовить данные в памяти, а потом быстро сохранить их в файл.
Работа с изолированным хранилищем
Для хранения информации индивидуально для каждого пользователя существует так называемое изолированное хранилище (IsolatedStorage). Физически оно представляет собой папку в Application Data создаваемую индивидуально для каждого приложения, однако доступ к ней происходит через специальный интерфейс, обеспечиваемый классами IsolatedStorageFile и IsolatedStorageFileStream из пространства имен System.IO.IsolatedStorage.
Для доступа к изолированному хранилищу пользователю не нужно обладать никакими правами, поэтому это хороший способ хранить пользовательские данные не нарушая безопасность давая ему доступ к корню диска C или например, к папке с программой. Изолированное хранилище доступно даже в Silverlight-приложениях.
// Получаем изолированное хранилище
// текущего пользователя для данной сборки
IsolatedStorageFile store =
IsolatedStorageFile.GetUserStoreForAssembly();
// Создаем файл в изолированном хранилище
using (IsolatedStorageFileStream fs =
new IsolatedStorageFileStream("test.txt", FileMode.Create, store))
{
byte[] data = Encoding.Unicode.GetBytes("Проверка");
fs.Write(data, 0, data.Length);
fs.Close();
}
// Читаем
using (IsolatedStorageFileStream fs =
new IsolatedStorageFileStream("test.txt", FileMode.Open, store))
Существуют разные стили именования переменных и функций и оформления исходного кода программы. Например, одну и ту же функцию можно написать так:
double f(double a1,double a2){
double r=a1*a2;
return r;
}
Или так:
double CalculateWay(double speed, double t)
{
double zVelocity = speed * t;
return zVelocity;
}
Обе функции будут правильно работать, но вторая выглядит намного понятнее. Компилятору совершенно все равно, как вы назовете переменные и функции, будете ли вставлять пробелы и пустые строки, но код вашей программы будут читать, а возможно и модифицировать другие программисты и вы сами. Если он будет нечитаем, то вы сами себе усложните задачу. Поэтому, если вы специально не ставите себе такой задачи, нужно внимательно относиться к оформлению кода, особенно при работе в команде. Хорошей практикой является описание правил оформления кода, именования различных объектов и всего к этому относящегося в специальном документе, называемом стандартами кодирования. Не так важно, какой стиль вы выберете, гораздо важнее ему следовать. В начале это будет трудно, но в дальнейшем, правильное написание текста программы станет хорошей привычкой.
Следует обратить внимание на следующие вопросы:
1) Именование переменных, функций и других объектов программного кода
Самое главное – именовать переменные и функции так, чтобы было понятно их предназначение. В данном примере, уже из названия функции понятно, что она вычисляет пройденный путь, умножая скорость на время. Написание длинных имен будет отнимать некоторое время, однако это время легко окупится в дальнейшем, когда вам не придется вспоминать, что означает та или иная переменная или функция.
Другой важный момент – формат имени. Существует множество вариантов именования переменных и функций: someFunction, SomeFunction, some_function. Для переменных можно использовать префиксы, чтобы отличить локальные переменные от аргументов функции и атрибутов классов, или для указания типа переменной, например так: iVariable – целая, dVariable – переменная типа double, pVariable – указатель и т.д.
2) Оформление текста
При оформлении блоков нужно решить, где ставить фигурные скобки – сразу после функции (такой стиль предпочитают программисты Java) или на отдельных строках. Важно использовать отступы в блоках и использовать одинаковую величину этих отступов (в разных средах разработки она может быть разной).
Отдельные участки кода нужно отделять друго от друга пустыми строками, деля текст программы на отдельные блоки. Если такой блок делает что-то сложное и не совсем понятное, полезно написать к нему комментарий.
Операции в выражениях полезно отделять пробелами, чтобы текст не сливался.
3) Формат комментариев
Нужно решить, писать комментарий на той же строке, что и комментируемая операция или на предыдущей. Как использовать многострочные комментарии. В каком формате писать шапки для функций и других объектов текста программы, где описано, что они делают и кто их написал.
4) Именование объектов графического интерфейса пользователя
Как правило, визуальные среды разработки присваивают элементам управления, используемым в графическом интерфейсе пользователя, имена вида: button1, button2. Их обязательно нужно менять на более осмысленные (например buttonStart, buttonStop или же StartButton и StopButton), иначе потом будет очень сложно понять, что делает и где находится кнопка button13. При этом нужно сохранить указание на то, что это за элемент управления – кнопка, поле ввода или что-нибудь еще.
Использование стандартов кодирования дает множество преимуществ. Текст программы намного проще читать (а если вы хотите создавать дейтвительно качественный код, то вам или вашим коллегам регулярно нужно будет читать тексты программ). Вам не придется вспоминать, для чего нужна переменная, функция или элемент управления. С хорошо оформленным кодом приятно работать, а это может существенно повлиять на производительность труда. При передаче исходных кодов заказчику, хорошо оформленный код дает вашей программе дополнительный вес.
Особенно важны стандарты кодирования при командной разработке. При работе в команде необходимо следовать общему стандарту, который лучше закрепить в специальном документе. Тогда вы сможете работать с кодом ваших коллег также как со своим.
Задача сделать код нечитаемым, также актуальна. Это необходимо, если вы не хотите чтобы в исходниках вашей программы смог разобраться кто-то другой и использовать их для каких-то своих целей. Особенно это актуально для некомпилируемых языков, таких как Java, C# и разных скриптовых языков, в которых программа распространяется не в виде машинного кода, а в виде кода на некотором, достаточно высокоуровневом языке. Также исходные тексты иногда запутывают, при передаче исходников заказчику, чтобы формально передав их ему не дать возможности их использовать (хотя это не самая лучшая практика). Для запутывания исходных текстов программ существуют специальные инструменты – обфускаторы (от англ. Obfuscate – сбивать с толку), которые переименовывают все переменные и функции, сбивают все оформление кода, иногда даже вставляют бесполезные куски кода или меняют структуру программы без нарушения работоспособности. Например, в Visual Studio .NET входит программа Dotfuscator, которая позволяет запутывать программы, написанные в этой среде разработки.
Комментарии
Правильно написанные комментарии существенно повышают понятность текста программы. Очень многие либо ленятся писать комментарии, либо делают это неправильно. Ключ к правильному использованию комментариев лежит в понимании того, зачем нужно их писать. Существует три основных ситуации, когда нужны комментарии:
1. Для того, чтобы было понятно, что делает комментируемый участок кода если это не понятно из самого кода
Например, следующий комментарий полезен разве что при изучении языка:
int z = x + y; // присваиваем z сумму x и y
Однако более сложные участки кода требуют комментариев. Иначе вы можете не вспомнить, как они работают, например, если в них обнаружится ошибка (а именно в таких участках ошибки часто и появляются). Но если просто прокомментировать каждую строку сложного участка комментариями, подобными вышеописанному, это не сильно поможет пониманию. Гораздо лучше описать общую идею этого участка.
2. Чтобы было понятно, почему какой-то участок кода написан так, а не иначе
В данном примере понятно, что происходит, но может быть непонятно почему так сделано:
for (int i = 0; i < zLength; i++)
{
// Удаляем всегда нулевой элемент, т.к.
// после удаления нулевым станет следующий элемент
Remove(zArray, 0);
}
В таких ситуациях вероятность запутаться еще выше.
3. Когда комментарии заменяют или дополняют сопроводительную документацию
Сюда относятся шапки над функциями и другими объектами кода (структурами, модулями, классами). Например:
Некоторые среды разработки, при выборе такой функции показывают подсказку с текстом, написанным в комментарии.
Также существуют средства, позволяющие генерировать по тексту программы с такими комментариями сопроводительную документацию. В Visual Studio .NET, если ввести ///, автоматически создается шапка для комментариев функции с тэгами xml. С помощью утилиты, входящей в Visual Studio .NET можно сгенерировать документацию из этих комментариев.
Гибкие методологии разработки. Традиционный подход к разработке
Традиционные подходы к разработке рассматривают разработку программного обеспечения как инженерную дисциплину. При таком подходе сначала подробно разрабатывается архитектура системы, вплоть до мельчайших деталей. Затем пишется код системы, а после этого проводится полномасштабное тестирование.
Однако такой подход, хорошо зарекомендовавший себя в инженерных дисциплинах, не очень хорошо подходит к разработке программного обеспечения:
1) Требования к программному обеспечению очень часто меняются в процессе работы над проектом
Как правило, в начале проекта и заказчик и программисты не так четко представляют себе, как должна выглядеть и работать система. От подробных требований на бумаге также не так много пользы, поскольку по ним сложно понять систему.
2) Разработка детализированной архитектуры занимает очень много времени
При этом нет гарантий того, что красиво выглядящая на бумаге программа будет хорошо работать в реальности. Почти всегда возникают неучтенные детали, подводные камни, которые могут существенно повлиять на систему.
3) Не используется гибкость программного обеспечения
Программное оеспечение (software) происходит от слова soft – мягкий, поскольку в отличие от аппаратного обеспечения (hardware – нечто твердое) оно легко поддается изменениям. Однако, традиционные подходы к программному обеспечению отказываются от этой уникальной возможности, препятствуя изменениям.
Инженерный подход к программированию можно сравнить со строительством здания, когда у вас есть четкий план здания и вы его строите от фундамента – этаж за этажом. При этом внести изменения в фундамент, когда уже построено несколько этажей – практически невозможно.
Гибкие подходы к разработке
Для разработки ПО лучше подходит эволюционный подход, когда система рассматривается как нечто живое и развивающееся. Разработка начинается с самого простого варианта системы. В случае необходимости можно использовать разного рода заглушки (mock). Например, генераторы данных вместо реальной базы данных. В процессе разработки система развивается, в нее добавляются новые функции. При необходимости, уже готовые части переделываются. Части, которые оказались ненужны, упрощаются или удаляются. Получается, что на каждом этапе есть работающая система, но более простая. В конце концов, получается та система, которая нужна, с учетом изменений в требованиях к системе или в понимании этих требований.
Такой подход можно сравнить с выращиванием дерева. Сначала есть просто семечко, затем появляется росток, деревце с листями, цветы, а затем и яблоки. При этом на каждом этапе оно образует законченную систему. Дерево адаптируется к окружающим условиям в процессе роста.
Гибкие методологии обладают следующими преимуществами:
1. Обратная связь
Работающая система дает обратную связь, позволяющую на раннем этапе обнаружить ошибки или неправильно понятые требования.
2. Возможность изменений
Если в процессе работы выяснится, что в системе нужно что-то изменить, это гораздо проще сделать.
3. Качество архитектуры
В архитектуре системы разработанной по таким принципам, нет ничего лишнего, поскольку то, что не нужно отсекается на ранних этапах разработки. В то же время, понимание каких-то важных моментов, которое, как правило, приходит в процессе работы над системой позволяет спроектировать их лучше, чем это можно было сделать заранее.
4. Психологический аспект
Когда видишь, как что-то получается, это придает намного больше уверенности (как программисту, так и заказчику), чем отметки в графике работ. А психологический настрой может очень существенно сказаться на производительности.
Особенно хорошо эффективность эволюционного подхода чувствуется при разработке больших сложных систем, однако даже при разработке небольших программах или отдельных модулей системы он может помочь.
Например, если вы реализуете список, вы можете попытаться сразу реализовать все методы (добавление, удаление, печать и т.д.). Когда вы напишете текст программы, а затем скомпилируете его, наверняка сначала обнаружится несколько синтаксических ошибок. Хотя современные среды разработки позволяют легко их исправить, это займет некоторое время. Исправив синтаксические ошибки и запустив программу вы можете обнаружить, что она работает не так как надо. Но найти ошибку может оказаться не так просто, т.к. вы не знаете в каком месте прогарммы ее искать. Например, если не работает удаление элемента, это может означать ошибку в функции удаления элемента, а может удаление не работает по причине того, что что-то не так при добавлении в список. Если же вообще окажется, что сам принцип построения списка, который вы выбрали, не работает, то вам придется переделывать всю программу. Когда вы пишете программу в таком стиле, вы сосредоточены на том, чтобы не допустить ошибку и держите в голове общую архитектуру программы, чтобы понять, что надо писать сейчас, а что – после. В таком режиме работы очень сложно увидеть какие-то идеи, которые могут улучшить программу или упростить дальнейшую разработку.
Если следовать принципу эволюционирования, то сначала нужно реализовать самый простой вариант системы. В данном случае – сделать класс для реализации списка, максимально простым способом заполнить его (возможно даже не реализуя полноценную функцию добавления элементов) и реализовать печать, чтобы увидеть получился ли список или нет. Скомпилировать и запустить программу. Если обнаружатся ошибки, то их будет намного легче обнаружить, т.к. программа небольшая. Затем реализовать добавление, посмотреть, как работает оно и т.д. Если не работает, выяснить, в чем причина. Если это простая ошибка, то известно, где ее искать – в только что добавленном небольшом кусочке кода. Реализовав добавление, вы можете обнаружить, что список можно сделать проще и лучше или же что он сделан неправильно. Тогда вы можете его переделать, и следующие функции будет реализовать уже проще.
Опасности гибких методологий
Не смотря на все преимущества, гибкие методологии подход таят в себе серьезные опасности:
1. Недостаточное внимание предварительному проектированию
То, что программа будет развиваться по ходу проекта, не отменяет необходимости проектирования архитектуры системы. Проектирование в этом случае может быть не таким тщательным, но совсем отказываться от него нельзя. Нужно продумать общую схему функционирования системы, из каких модулей она будет состоять и как эти модули могут взаимодействовать между собой. Также необходимо проанализировать технические риски проекта (возможные проблемы с производительностью, безопасностью, использованием новых технологий).
Хорошая статья на эту тему: Мартин Фаулер «Проектирования больше нет» (Is Design dead?).
2. Ошибочная оценка сроков и стоимости разработки
Поскольку изменения в требованиях невозможно предугадать, сложно заранее оценить сроки и стоимость разработки. Чтобы избежать ошибки лучше всего сначала договориться о разработке минимальной версии системы, решающей основные задачи заказчика, а дополнительные возможности оценивать и разрабатывать отдельно как доработки к системе. Оплату производить поэтапно. Обязательно нужно указывать, что реализуется только минимальный необходимый вариант. Все остальное дорабатывается и оплачиватеся отдельно.
К сожалению, это достаточно сложно реализовать составляя договор с клиентом.
Еще лучше – договориться о почасовой оплате работы. Но это требует большого доверия к исполнителю со стороны заказчика.
Со срокмаи проблема, как правило, еще хуже. Подавляющее большинство разработчиков оценивают сроки очень оптимистично. Есть хорошее правило – умножить оценку сроков на 2 и прибавить 50%. И в оплате, соответственно, заложиться на такое же увеличение стоимости. Для нефтяных компаний еще раз умножить на 2.
3. Расползание функциональности (feature creep)
Когда требования изменяются и добавляются по ходу проекта и нет четких спецификаций, описывающих, что должна делать система, велик риск расползания функциональности, т.е. добавления в систему новых возможностей, которые не планировались изначально. Чтобы закончить проект в срок и не сделать его убыточным для разработчиков, нужно применять feature cut, т.е. отменять или откладывать реализацию тех функций, которые оказались заказчику менее важны, чем новые. Важность тех или иных функций, разумеется, должен определять заказчик. Менее важные возможности системы можно затем оформить как отдельные доработки к системе.
Иногда новые функции придумываются разработчиками. К сожалению, не всегда то, что кажется разработчикам красивым и удобным нравится и нужно клиенту. Можно предлагать клиенту варианты, но только за отдельную плату.
Часто бывает, что новые функции придумываются заказчиком. Особенно, если он уже увидел промежуточную версию системы и успел с ней поработать. Основная опасность при этом заключается, что доработки кажутся мелкими. Но при этом может возникнуть ситуация как в сказке про кашу из топора. Также нужно учитывать, что доработки, которые кажутся мелкими заказчику, могут быть очень сложно реализуемыми.
Другие применения идей гибких методологий
Подход постепенного и естетсвенного развития применим не только в программировании, но и в других сферах жизни. Например, известно что Курчатов при строительстве института, не стал сразу асфальтировать дорожки, а подождал пока сотрудники протопчут трапинки на территории института и только потом положил асфальт по уже готовым маршрутам. В результате и газон цел и ходить удобно.
Многих учили в школе, что перед написанием сочинения нужно составить план, а потом по нему писать текст. Если писать на бумаге, этот подход еще как-то оправдан. Но когда вы пишете на комьютере, лучше просто сесть и начать писать с того места, с которого хочется. После этого перемещать куски, выделять разделы, написать вводную и заключительную часть. Тект получается намного лучше и органичнее. Иногда структура текста видна сразу и можно сразу набрасать основные разделы, но совсем не обязательно заполнять их строго по порядку.
При внедрении каких-то организационных правил в компании или различных практик в своей жизни также лучше начать с простых вещей и вводить их постепенно.
Японские компании умудряются применять эволюционный подход даже на производстве, используя принцип постоянного улучшения (кайдзен). При этом инициатива идет не только от руководтсва, но и от рядовых сотрудников которые зачастую лучше видят, как можно улучшить процесс производства.
Рефакторинг
Распространенной ошибкой является ситуация, когда уже к написнному программному коду никогда не возвращаются, не читают его и не улучшают. При этом новые части системы подстраиваются под уже написанную часть системы, даже когда проще немного изменить уже существующую часть. Часть системы, написанная с нуля без последующей корректировки, по сути, является черновиком (даже если она не задумывалась таковой), поэтому качество кода в ней, как правило, не так хорошо, как могло бы быть. Многие вещи вообще очень сложно увидеть, пока код не написан.
Качество новой функциональности, которая пристраивается к уже готовой части, зачастую получается даже хуже чем у основной части, по двум причинам. Во-первых, новую функциональность приходится подстраивать под существующую. Во-вторых, видя качество функциональности, с которой приходится работать, программист некачественно делает свою часть системы. Это напоминает образование стихийных помоек, когда один человек бросил бумажку, другой увидел эту бумажку и ему уже намного легче намусорить рядом, поскольку не он все это начал. В результате образуется помойка. То же самое происходит и с программой. Со временем она напоминает кое-как сколоченную конструкцию с кучей подпорок, которая вот-вот развалится и что-то к ней добавлять становится все сложнее и сложнее.
Избежать этих неприятностей, позволяет рефакторинг - пересмотр и совершенствование уже написанного кода. Мартин Фаулер дает следующие определения рефакторинга:
Refactoring (сущ.), рефакторинг: изменение, применяемое ко внутренней структуре программного обеспечения, без изменения его видимого поведения, чтобы сделать это ПО проще для понимания и удешевить дальнейшие изменения.
Refactor (гл.), выполнять рефакторинг: реструктурировать программное обеспечение путем применения набора рефакторингов без изменения видимого поведения ПО.
Рассмотрим основные ситуации, которые требуют улучшения кода:
1. Сложные и запутанные участки кода
Упрощение кода – это одна из основных задач рефакторинга. В программе очень часто обнаруживаются части, которые можно упростить. Более простой код меньше подвержен ошибкам, проще для понимания и часто работает лучше и быстрее. Примером таких рефакторингов является разбиение слишком длинных методов на несколько более простых, разделение перегруженного функциями класса на несколько, перестройка иерархии наследования.
2. Дублирование кода
Часто обнаруживается, что разные части программы дублируют друг друга. В этом случае нужно устранить дублирование и слив эти части в одну, возможно, параметризованную. Это позволит упростить код и сделать его более понятным, поскольку вместо двух частей появляется одна, более явно выделенная. Это позволяет избежать ошибок, т.к. допустить ошибку в двух параллельно развивающихся частях намного проще, чем в одной. Это упрощает дальнейшую разработку, т.к. может появиться ситуация, когда эта параметризованная часть может пригодиться еще где-то. Характерными примерами такого рефакторинга являются выделение базового класса из двух похожих классов, выделение повторяющихся частей кода в отдельный метод, добавление разного рода настроек и параметров в классы и отдельные методы.
3. Ошибкоопасные места
Как правило, при разработке основной функциональности можно увлечься ее разработкой и уделить недостаточно внимания разного рода проверкам и обработке ошибок. Рефакторинг позволяет восполнить эти недочеты. Более того, видя код целиком гораздо проще увидеть места, где может возникнуть проблема.
4. Лишние участки кода
Кажется, что эти участки кода никому не мешают, однако они затрудняют понимание кода, да и просто мешают читать код. Например, если появляются неиспользуемые методы или классы, то их нужно выкидывать (помечать как комментарии).
5. Проблемы с именованием
В процессе развития системы иногда получается, что отдельные классы и методы изменяются настолько, что их имена не совсем точно отражают их предназначение. Или же классы и методы изначально не очень хорошо именованы. В этом случае их нужно переименовать, чтобы сделать код более понятным.
Существуют и другие ситуации, когда можно и нужно применять рефакторинг. Такие ситуации и способы их решения описаны в виде набора рефакторингов, которые еще называют паттернами рефакторинга. Также существуют специальные системы анализа кода, выявляющие проблемные участки и предлагающие выполнить рефакторинг, а иногда и автоматически применить рефакторинг. Такими возможностями, например, обладает Visual Studio .NET.
Преимущества рефакторинга:
1) Рефакторинг, ведет к более ясному пониманию кода
Это позволяет увидеть возможности дальнейшего улучшения и пути дальнейшего развития системы.
2) Рефакторинг улучшает архитектуру системы
В процессе рефакторинга происходит развитие системы. Система развивается естественным образом, выделяя новые части в процессе рефакторинга.
3) Рефакторинг позволяет избежать ошибок
Даже простой просмотр кода позволяет обнаружить ошибки, причем такие, которые очень сложно обнаружить при тестировании. Например, проблемы с динамической памятью.
4) Рефакторинг улучшает обмен знаниями в команде
Читая чужой код, вы можете научиться каким-то приемам, которые использовал автор кода или наоборот, подсказать ему, как можно сделать код лучше.
Рефакторинг нужно выполнять до добавления функциональности, просматривая код, куда будет добавлена функциональность. В этом случае, существующий код можно переделать, чтобы упростить добавление функциональности. Также рефакторинг нужно выполнять после добавления функциональности, т.к. после добавления кода практически всегда становятся видны пути его улучшения. Рефакторинг можно сделать и регулярной процедурой, просматривая всю систему или отдельные ее части.
Рефакторинг нужно выполнять небольшими шагами, т.к. внесение слишком мастштабных изменений может сделать неработоспособной. Также не следует параллельно с рефакторингом добавлять новую функциональность, хотя очень часто при рефакторинге возникают идеи, которые хочется тут же реализовать. Это может привести к тому, что вместо рефакторинга вы будете заниматься разработкой новой функциональности.
Идею рефакторинга хорошо демонстрирует притча о двух дровосеках. Два дровосека поспорили о том, кто из них нарубит больше дров за день. Рано утром они усиленно начали рубить лес. Один из дровосеков вдруг услышал, что второй перестал рубить и с удвоенной силой продолжал рубить, чтобы обогнать первого. Но в результате первый дровосек нарубил намного больше. «Как тебе это удалось? Что ты делал, когда переставал рубить?» - спросил второй. Первый ответил: «Отдыхал и затачивал топор».
Необходимым условием рефакторинга является налаженный процесс тестирования, т.к. выполнение рефакторинга без тестирования может привести к ошибкам и в некоторых случаях даже принести больше вреда, чем пользы. Избежать этого позволяет использование модульного тестирования.
Модульные тесты
Традиционный подход к тестированию заключается в том, что есть выделенный штат тестеров, которые тестируют программу с точки зрения пользователей, т.е. выполняют действия, которые выполняют пользователи и смотрят, корректно ли на них реагирует программа. Иногда этим занимаются и сами программисты.
Такой способ тестирования важен, поскольку хорошо тестирует работу системы целиком, части системы связанные с пользовательским интерфейсом и удобство интерфейса (иногда удобство интерфейса тестирут отдельно и выделяют этот процесс как юзабилити тестирование).
Однако этот способ тестирования обладает рядом недостатков:
1. Тестируется только «внешняя часть системы», близкая к пользовательскому интерфейсу. Ошибки во внутренних механизмах системы при таком способе тестирования отследить достаточно сложно.
2. Отследить все возможные ситуации практически невозможно. И чем сложнее система – тем серьезнее эта проблема. Поскольку система тестируется только целиком, то количество ситуаций – это количество всех возможных комбинаций ситуаций в разных частях системы.
3. Такое тестирование отнимает много времени, поскольку выполняется вручную.
При использовании только обычного тестирования внутренние части системы тестируются, когда вся система уже готова, да и то опосредованно. В большинстве случаев программисты просто добиваются того, чтобы их код компилировался. Но это совершенно не означает, что он будет работать. Некоторые части вообще могут быть не затронуты тестированием.
Модульные тесты позволяют решить эти проблемы. Идея модульного тестирования заключается в создании автоматических тестов, которые тестируют внутренние части системы. Т.е. программа тестирует сама себя.
Предположим, что есть некоторый модуль (состоящий из одного или несколько классов), реализующий работу со списком. Модульный тест для такого модуля представляет собой другой модуль (как правило – один класс), который тестирует работоспособность первого модуля в разных ситуациях. В первом тесте (методе класса) добавляется несколько элементов в список и проверяет, соответствует ли содержимое списка тому, что ожидается увидеть. Второй тест пытается удалить элемент из пустого стека и проверяет, корректно ли ведет себя модуль в этом случае, и т.д. Каждый такой тест выдает только один результат – пройден тест или нет. Также в тестах должны отлавливаться исключения (exceptions).
Важно, чтобы тесты были независимы друг от друга, т.е. ошибка в одном из них не влияла на прохождение другого теста. Для этих целей в модульных тестах используют функцию инициализации теста (SetUp) и очистки после теста (TearDown). Например, при тестировании системы, данные которой хранятся в базе данных, метод SetUp может заполнять базу тестовыми данными, а метод TearDown удалять все данные из базы.
Тесты объединяются в набор тестов (Test Fixture), который можно запускать автоматически после каждого внесения изменений. Если один из тестов перестает срабатывать, легко разобраться в чем причина проблемы и устранить ошибку.
Чем больше количество тестов, тем более тщательным будет тестирование. Идеальный вариант – покрыть все возможные ситуации, но он достижим только для очень простых модулей. В большинстве случаев достаточно написать тесты для нестандартных ситуаций, чтобы модуль их корректно обрабатывал и для основных вариантов использования модуля.
Тестовые данные и ожидаемые результаты можно генерировать программно (описывать прямо в коде), но лучше вынести их в отдельный файл, чтобы их легко можно было редактировать. Вместо других модулей, с которыми взаимодействует данный модуль можно использовать заглушки (mock), которые эмулируют их работу, если эти модули еще не работают. Это дополнительно побуждает следовать принципам ООП и делать модули максимально независимыми друг от друга.
Использование модульных тестов дает множество преимуществ. Модульные тесты:
1. Позволяют на ранних стадиях разработки отлавливать ошибки во внутренних частях системы
2. Обеспечивают обратную связь, т.е. позволяют сразу же проверить работоспособность только что написанного кода, не дожидаясь готовности других частей.
3. Повышают уверенность в разрабатываемом коде, т.к. его работоспособность подтверждается автоматическими тестами.
4. Позволяют вносить изменения в код и выполнять рефакторинг, не опасаясь появления ошибок.
5. Документируют модуль, т.к. модульные тесты являются примерами использования данного модуля.
6. Уменьшают зависимость задач по разработке. При использовании модульных тестов, любой модуль системы можно начинать разрабатывать в любой момент, не дожидаясь готовности остальных модулей. Это дает большую гибкость при планировании разработки и распределении задач между разработчиками.
Недостатком модульных тестов является то, что на их написание и поддержку требуется тратить дополнительное время. Однако если правильно пользоваться тестами и преимуществами, которые они дают, это время быстро окупается.
Другой проблемой является то, что в некоторых случаях модульные тесты применить достаточно сложно. Например, чтобы автоматически тестировать пользовательский интерфейс, нужны специальные средства.
Модульные тесты не отменяют обычное тестирование всей системы целиком. Они дополняют его и делают его проще, поскольку большая часть ошибок во внутренних частях системы отлавливаются на этапе разработки.
Разработка через тестирование
Использование модульных тестов позволяет по-новому взглянуть на процесс разработки и использовать подход, называемый разработкой через тестирование (test driven development, TDD). Суть этого подхода заключается в том, что модульные тесты пишутся до того, как написан код, которые они тестируют.
При этом разработка происходит по следующему алгоритму:
1. Написать тест
При этом программист фокусируется на интерфейсе создаваемого модуля. Это очень хорошо, т.к. позволяет создавать интерфейс исходя из удобства использования модуля, а не из его внутренних механизмов, как это обычно бывает.
2. Заставить тест работать
На данном шаге нужно любыми средствами заставить тест работать – используя разного рода заглушки и подделки. Например, если мы ожидаем, что функция должна вернуть 0, то на первом этапе можно просто вставить return 0 в самом начале функции. Вместо данных, которые использует функция можно использовать тестовые данные. При этом о красоте и качестве кода на данном этапе можно не думать.
3. Рефакторинг
На данном шаге работающий код нужно привести в порядок. Устранить дублирование, улучшить код, написать комментарии и т.д. Поскольку тест для этого кода уже есть и есть представление о том, как он должен работать в целом, это намного проще, чем написание хорошего кода с нуля. Код самого теста также подвергается улучшению.
Далее эти шаги повторяются до тех пор, пока весь модуль не заработает и не будет в достаточной степени покрыт тестами. Изменения вносятся очень небольшими шагами.
Может показаться, что промежуточные шаги с написанием заглушек и подделок не нужны и проще сразу написать окончательную версию кода. Но это не так. Здесь работает тот же принцип эволюционности, только на самом низком уровне. Например, написав функцию из единственного оператора return 0, можно уже проверить, не опечатались ли вы где-нибудь, тот ли тип имеет возвращаемое значение и т.д. Подобные мелочи намного проще отловить в такой простейшей функции, чем в окончательном варианте.
Цель написания кода – чистый и качественный код, который работает. Но достигнуть этой цели можно по- разному.
При традиционных подходах к разработке, сначала идет чистый и качественный код (в большинстве случаев в виде спецификаций на бумаге) который программисты потом пытаются заставить работать. В ходе этого процесса код, как правило, отходит от изначальных спецификаций и архитектуры, обрастает разного рода заплатками и костылями и перестает быть чистым и красивым.
При использовании модульного тестирования, эволюционного подхода и рефакторинга, код сначала заставляют работать, а затем делают его чистым и красивым. При этом, благодаря модульным тестам, работоспособность не нарушается.
Принцип ортогональности
Одна из основных проблем разработки программного обеспечения – это сложность создаваемых систем, а один из основных способов борьбы с этой борьбы – декомпозиция – разбиение сложной системы на более простые части. Однако просто разбить систему на части недостаточно. Например, вы можете разбить программу из 10000 строк кода на блоки по 100 строк, но это не уменьшит ее сложности. Важно, чтобы части системы были логически цельными и как можно более независимыми друг от друга. Авторы книги «Программист прагматик» назвали такую независимость частей системы друг от друга – ортогональностью.
Поскольку декомпозиция, как правило, происходит на разных уровнях системы, т.е. система разбивается на части, эти части разбиваются на еще более простые части и т.д., то и принцип независимости этих частей должен соблюдаться на всех уровнях – от уровня архитектуры всей системы, до уровня отдельных функций.
Зависимость проявляется в том, что одна часть системы полагается на то, что какая-то другая часть системы устроена определенным образом или вообще существует в системе. Также зависимость проявляется в том, что одна часть системы слишком много «знает» о внутреннем устройстве другой части системы. Это приводит к следующим проблемам:
1. Сложность системы недостаточно уменьшается
2. Ошибка в одной части системы может привести к ошибке в другой части системы
3. Изменение одной части системы требует изменения всех зависящих от нее частей
4. Сложно повторно использовать какие-то части системы, т.к. они слишком связаны с другими частями
5. Сложно поделить задачи между программистами при командной разработке
6. Зависимости ведут к другим зависимостям, что еще больше усложняет систему
7. Зависимости не позволяют увидеть структуру системы и пути ее улучшения
Рассмотрим пример. Пусть есть класс, реализующий список. Для вывода на экран в этом модуле используется метод:
public void PrintList()
{
XElement zElement = _first;
while (zElement != null)
{
Console.WriteLine(pElement.Data.ToString());
zElement = zElement.Next;
}
}
Он кажется вполне логичным, но у внем есть два недостатка. Во-первых, он связывает список с консольным выводом, поскольку использует Console. Т.е. если мы захотим вывести стек в файл или в список на форме, придется переделать эту функцию или написать другую. Во-вторых, формат вывода стека задается в этом методе, т.е. стек всегда будет выводиться построчно. Если же понадобится вывести его элементы через запятую, то придется переделать эту функцию.
В данном случае нарушена независимость и целостность. Список не должен зависить от способа отображения данных пользователем, а модуль, реализующий список не должен определять способ вывода данных, т.к. это не его задача.
Можно перенести этот метод функцию из класса списка, в класс, занимающийся взаимодействием с пользователем. Но в этом случае зависимость просто меняет направление. Теперь модуль пользовательского интерфейса будет зависеть от способа реализации списка. Т.е. он должен знать, что на самом деле стек реализован с помощью ссылок в его элементах. А он этого знать не должен, т.к. при этом нарушается принцип инкапсуляции и класс пользовательского интерфейса зависит от реализации списка.
Самое правильное решение в данном случае – обход списка оставить в модуле списка, а печать делегировать модулю, занимающемуся интерфейсом используя делегат:
public delegate void ProcessElementDataDelegate(object data);
При такой реализации, у функции печати появляется множество преимуществ. Она, по сути, превращается в метод обработки списка. Например, вы можете печатать список в файл или в список на форме в любом формате, использовать эту функцию для копирования списка, например, в массив. С другой стороны, если вы захотите переделать список, используя, например, фиксированный массив или список из библиотеки классов, то это изменение не затронет модули, использующие ваш список.
Изначально догадаться реализовать печать списка таким способом достаточно сложно, но здесь на помощь приходит рефакторинг. Такие улучшения естественным образом появляются, если вы правильно выполняете рефакторинг. Например, если помимо печати в консольное окно понадобится вывод данных в список на форме или файл, то вместо написания соответствующего метода лучше выполнить рефакторинг и переделать метод печати, сделав его более универсальным. Если эту возможность и в этом случае не удалось увидеть, то ее можно увидеть после добавления новго метода печати, заметив, что метод печати в консоль и метод печати в файл очень похожи и заменив их параметризованным методом.
Вообще, отделение представления данных от самих данных – это типичная задача декомпозиции, для решения которой придумана архитектура Модель-Вид-Контроллер (MVC – Model, View, Controller). Идея этой архитектуры заключается в разделении системы на три части:
Модель – занимается хранением и обработкой данных и обеспечением доступа к ним. Не зависит от способа представления данных пользователю и способа ввода данных пользователем.
Вид – отвечает за представление данных пользователю.
Контроллер – реагирует на действия пользователя и, соответствующим образом изменяет модель.
Контроллер и Вид часто бывают слиты в один модуль, занимающийся взаимодействием с пользователем.
Метапрограммирование
Языки программирования можно использовать не только для написания непосредственно программ, которые решают определенную задачу, но и для поддержки написания этих программ и разных служебных целей. Такой подход называют метапрограммированием. Примером метопрограммирования являются, например модульные тесты. Они непосредственно не решают поставленную задачу (код тестов даже не включается в код финального продукта), но позволяют обеспечить тестирование основной программы. Другой пример – разного рода генераторы данных и заглушки, которые помогают в разработке.
Еще один интересный способ применения метапрограммирования – генераторы кода – программы, которые генерируют код для основной программы, например классы для взаимодействия с базой данных, разные классы по шаблонам и т.п.
К метопрограммированию можно отнести и разного рода конфигурационные файлы, которые позволяют настраивать работу уже готового приложения. Это очень хороший способ сделать приложение более гибким и настраиваемым, что никогда не повредит. В некоторых случаях можно даже создать небольшой скриптовый язык, управляющий некоторыми аспектами работы программы которые могут поменяться.
Язык XML. Возникновение XML
В настоящее время основным стандартом представления информации в сети интернет является язык HTML (Hypertext Markup Language – язык разметки гипертекста). Он был построен на основе метаязыка разметки текста - SGML (Standart Generalised Markup Language), созданного издателями еще в 80-е годы. Выбор HTML в качестве стандарта объяснялся его простотой и удобством по сравнению с SGML, и в эпоху зарождения интернета этот выбор был оправдан. По сути, HTML-документ представляет собой просто текст с описанием способов его отображения и связями между документами. Однако HTML ничего не говорит о смысле того, что на нем описано. Текст структурирован только с точки зрения отображения, а не семантики.
С развитием интернета появилась необходимость отделить данные от способа их отображения. Частично эта задача была решена с помощью каскадных таблиц стилей (CSS). Появилась необходимость создания динамически изменяющихся и интерактивных страниц, манипуляции гипертекстом. Эта задача была решена с помощью создания объектной модели документа (DOM), скриптовых языков, языков веб-программирования и технологии Flash. Сам язык HTML тоже развивается. Однако дальнейшее развитие этих направлений упирается в саму структуру языка HTML.
Еще одним развивающимся направлением является электронная коммерция и другие распределенные системы. Но в них нет единообразного способа представления передаваемых данных. Стандарты были созданы только для электронной коммерции, но они оказались слишком сложны и недостаточно универсальны.
Способом решения указанных проблем является использование нового языка представления данных, который мог бы прийти на смену HTML, стать стандартом обмена данными через интернет и использоваться в других областях. Таким решением является XML.
XML
XML расшифровывается как Extensible Markup Language – Расширяемый Язык Разметки. Он является некоторым возвратом к SGML, поскольку тоже является метаязыком - более простым, чем SGML, но более универсальным, чем HTML. HTML является подмножеством XML.
Также как и HTML, XML описывает текст с помощью тегов и их атрибутов, однако эти теги и атрибуты не являются фиксированными и могут быть свои для каждого документа. Таким образом, документ в формате XML является самоописываемым. В отличие от HTML, XML описывает не способ отображения текста на экране, а структуру и смысл самого текста. То есть он описывает данные, а не способ их представления.
Рассмотрим пример. Пусть у нас есть таблица с ценами на комплектующие.
Категория
Наименование
Цена
Процессоры
[BOX] AMD Athlon 64 3200+ Venice Socket 939
179.90
Процессоры
[BOX] AMD Athlon 64 X2 4200+ Socket 939
440.80
Видеокарты
128Mb PCI-E PCX 6600 GT TV DVI [MSI NX6600GT-TD128E] OEM
156.00
Видеокарты
256Mb PCI-E X800GTO TV DVI [Sapphire part number: 1024-AC60-xx-xx] OEM
Когда пользователь просматривает такую таблицу в браузере, он может понять, что это прайс-лист. Для компьютера же это просто таблица с тремя колонками, которая на HTML описывается так:
С помощью CSS можно описать стили, для ячеек таблицы в отдельном файле и, таким образом отделить способ представления данных от самих данных. Однако полностью отделить данные от представления CSS не позволяет. К тому же данные все равно структурированы с точки зрения отображения.
Обработать такую таблицу какой-нибудь программой тоже тяжело. Даже если приписать ко всем объектам идентификатор и имя, и использовать DOM. Чтобы передать кому-то эти данные придется их сначала преобразовать в более удобный вид.
Теперь посмотрим, как те же данные описывается с помощью XML.
Такой документ понятен даже без браузера. Он имеет древовидную структуру, но уже не с точки зрения представления информации, как в HTML, а с точки зрения семантики. Такой способ описания данных очень прост, естественен, удобен для обработки и хорошо согласуется с объектной моделью.
Обратите внимание, что категорию товара мы вынесли в атрибут тэга Item. Можно было бы все характеристики товара сделать атрибутами или наоборот, хранить их всех в тэгах. Обычно в атрибуты выносятся характеристики объекта, описываемого тэгом, а во внутренние тэги – данные объекта. Если проводить аналогию с русским языком, то атрибуты – это прилагательные, а внутренние тэги – существительные. Если товары разных категорий будут иметь разный набор характеристик, то, определив категорию в атрибуте, мы можем по ней узнать, сколько тегов должно быть внутри тэга Item и какие они.
С помощью XML можно описать практически любые структурированные данные, даже имеющие достаточно сложную структуру. В данном примере, данные хорошо укладываются в таблицу, т.к. все товары имеют одинаковое количество параметров, однако с помощью XML можно описывать и более сложные данные, не укладывающиеся в табличное представление.
XML – это не просто язык описания структурированных данных, а целый набор технологий позволяющий работать с такими данными.
Для связи документов XML между собой и создания ссылок на данные внутри XML-документа созданы технологии XLink, XPath и XPointer.
Поскольку в отличие от HTML, в XML можно использовать свои собственные тэги и атрибуты, то необходимы способы проверки соответствия XML-документа определенному формату. Для этого существуют схемы DTD, XDR и язык определния схем XSD.
На основе XML можно создавать базы данных и для него даже есть свой язык запросов XQuery. На сегодняшний день современные реляционные СУБД также поддерживают интергацию с XML. Например, можно стандартными средствами получить результат запроса к таблице в виде XML-текста и редактировать этот текст таким образом, чтобы изменения отображались в таблицу. XML данные можно хранить в ячейках таблицы и обращаться с помощью запросов к данным внутри текста XML в этих ячейках.
XML очень удобен для хранения различных данных программы, которые слишком малы или имеют слишком сложную или изменяющуюся структуру для использования базы данных. Например, его можно использовать для конфигурационных файлов или просто файлов с данными и многие современные программы уже делают это. Учитывая, что для работы с XML в современных языках есть специальные классы и внешние библиотеки, это удобнее чем хранить данные в ini-файлах или реестре.
Для обмена данными в современных распределенных системах, основанных на веб-службах (web-services) используется протокол SOAP, основанных на XML.
Если же данные XML-файла нужно отобразить пользователю, например в браузере, то для этого существуют технологии XSL.
Описав прайс, с помощью XML мы описали структуру данных, но не описали, каким образом их отображать. Это можно сделать различными способами, одним из которых являются таблицы стилей XSL.
Таблицы стилей XSL
Для описания способа отображения данных в XML используются таблицы стилей XLS (Extensible Stylesheet Language – расширяемый язык таблиц стилей). XSL является гораздо более мощной технологией, чем каскадные таблицы стилей (CSS).
XSL состоит из двух языков – XSLT, предназначенного для определения преоблазований и XSL-FO, предназначеггого для описания способа отображения элементов.
XSL-FO во многом повторяет HTML, также описывая специальные теги для указания шрифтов, отображения таблиц и т.п.
XSLT является очень мощным языком, позволяющим, по сути, компилировать XML-текст в любой другой формат: текст, XML-текст другого формата, HTML и т.д. При этом можно группировать и фильтровать элементы и производить над ними различные преобразования. Например, при отображении можно легко выкинуть некоторые столбцы таблицы, выкинуть всю графику со страницы или поменять порядок элементов.
При этом сами данные, хранящиеся в XML, остаются теми же. Благодаря такому подходу можно легко сделать несколько версий страницы на сайте: версию для печати, версии на разных языках, WAP-версию и т.д. Способ отображения документа XML, описываемый XSL, может зависеть от самих данных. Например, можно отображать все отрицательные числовые значения красным цветом. Также имеются возможности использования в XSL каскадных таблиц стилей CSS.
Результатом работы процессора XSL может быть не только HTML, хотя это является наиболее часто используемой функцией. Документ XML можно перевести в WML, в обычный текстовый документ и т.д.
Также с помощью XSLT можно перевести один документ XML в другой документ XML, отличающийся по структуре и составу, что открывает множество интересных возможностей.
Рассмотрим пример XSL таблицы стилей для XML-документа с прайсом из предыдущего раздела, которые позволяют отображать его в html:
Правила отображения задаются с помощью тэгов xsl:template. Атрибут match этих тэгов указывает шаблон (в формате XPath), который ищется в XML-документе, а содержимое – текст результирующего документа. Тэги xsl:value-of вытаскивают содержимое и атрибутов (которые обозначаются символом @) из тэгов XML-документа (также с помощью XPath).
Чтобы связать xml-документ со схемой, нужно указать в XML-документе тэг: