Синтаксис описания шаблона
Три основных принципа языков объективно-ориентированного программирования
По Бьерну Страуструпу, автору C++, язык может называться объектно-ориентированным, если в нем реализованы три концепции: объекты, классы и наследование. Однако теперь принято считать, что такие языки должны держаться на других трех китах: инкапсуляции, наследовании и полиморфизме.
Инкапсуляция
Инкапсуляция, или утаивание информации (information hiding), — это возможность скрыть внутреннее устройство объекта от его пользователей, предоставив через интерфейс доступ только к тем членам объекта, с которыми клиенту разрешается работать напрямую. Необходимо пояснить разницу между абстрагированием и икапсуляцией. Инкапсуляция подразумевает наличие границы между внешним интерфейсом класса (открытыми членами, видимыми пользователям класса) и деталями его внутренней реализации. Преимущество инкапсуляции для разработчика в том, что он может открыть те члены класса, которые будут оставаться статичными, или неизменяемыми, скрыв внутреннюю организацию класса, более динамичную и в большей степени подверженную изменениям. В С# и С++ инкапсуляция достигается путем назначения члену класса модификатора доступа — public, private или protected.
Наследование
Наследованием называют возможность при описании класса указывать на его происхождение (kind-of relationship) от другого класса. Наследование позволяет создать новый класс, в основу которого положен существующий. В полученный таким образом класс можно внести свои изменения, а затем создать новые объекты данного типа. Этот механизм лежит в основе создания иерархии классов. После абстрагирования наследование — наиболее значимая часть общего планирования системы. Производным (derived class) называется создаваемый класс, производный от базового (base class). Производный класс наследует все методы базового, позволяя задействовать результаты прежнего труда.
Чтобы понять, когда и как применять наследование, вернемся к примеру EmployeeApp. Допустим, в компании есть служащие с разными типами оплаты труда: постоянный оклад, почасовая оплата и оплата по договору. Хотя у всех объектов Employee должен быть одинаковый интерфейс, их внутреннее функционирование может различаться. Например, метод CalculatePay для служащего на окладе будет работать не так, как для контрактника. Однако для ваших пользователей важно, чтобы интерфейс CalculatePay не зависел от того, как считается зарплата.
Что такое "правильное" наследование
Важнейшую проблему "правильного" наследования начнем с термина замещаемость (substitutability). Этот термин означает, что поведение производного класса достигается путем замещения поведения, заимствованного у базового класса. Это одно из важнейших правил, которое вам нужно соблюдать при построении работающей иерархии классов.
Имея ссылку на производный класс, программист всегда может обращаться с ним, как с базовым классом. Это называется восходящим преобразованием типа (upcasting). В производный класс можно добавить и другие функции, которые выполняют и более узкие (или более широкие) задачи, чем унаследованные функции. Поэтому данное правило применяется только к унаследованным членам, поскольку существующий код рассчитан на работу только с этими членами.
Полиморфизм
По-моему, самое короткое и выразительное определение полиморфизма таково: это функциональная возможность, позволяющая старому коду вызвать новый. Это свойство ООП, пожалуй, наиболее ценно, поскольку дает вам возможность расширять и совершенствовать свою систему, не затрагивая существующий код.
Предположим, вам нужно написать метод, в котором для каждого объекта из набора Employee вызывается метод CakulatePay. Все просто, если зарплата рассчитывается одним способом: вы можете сразу вставить в набор тип нужного объекта. Проблемы начинаются с появлением других форм оплаты. Допустим, у вас уже есть класс Employee, реализующий расчет зарплаты по фиксированному окладу. А что делать, чтобы рассчитать зарплату контрактников — ведь это уже другой способ расчета! В случае с процедурным языком вам пришлось бы переделать функцию, включив в нее новый тип обработки, так как в прежнем коде такой обработки нет. А объектно-ориентированный язык благодаря полиморфизму позволяет делать различную обработку.
В нашем примере надо описать базовый класс Employee, а затем создать производные от него классы для всех форм оплаты (упомянутых выше). Каждый производный класс будет иметь собственную реализацию метода CakulatePay. Здесь и начинается самое интересное. Возьмите указатель на объект, приведите его к типу-предку и вызовите метод этого объекта, а средства языка времени выполнения обеспечат вам, благодаря полиморфизму, вызов той версии этого метода, которая вам требуется.
Полиморфизм имеет минимум два плюса. Во-первых, он позволяет группировать объекты, имеющие общий базовый класс, и последовательно (например, в цикле) их обрабатывать. В рассмотренном случае у меня три разных типа объектов (SalariedEmployee, ContractorEmployee и Hourly-Employee), но я вправе считать их все объектами Employee, поскольку они произведены от базового класса Employee. Поэтому их можно поместить в массив, описанный как массив объектов Employee. Во время выполнения вызов метода одного из этих объектов будет преобразован, благодаря полиморфизму, в вызов метода соответствующего производного объекта.
Второе достоинство : старый код может использовать новый код. Заметьте: метод PolyApp.Calculate Pay перебирает в цикле элементы массива объектов Employee. Поскольку объекты приводятся неявно к вышестоящему типу Employee, а реализация полиморфизма во время выполнения обеспечивает вызов надлежащего метода, то ничто не мешает нам добавить в систему другие производные формы оплаты, вставить их в массив объектов Employee, и весь существующий код продолжит работу в своем первоначальном виде!
Абстрагирование
Абстрагирование связано с тем, как данная проблема представлена в пространстве программы. Во-первых, абстрагирование заложено в самих языках программирования. Постарайтесь вспомнить, давно ли вам приходилось заботиться о стеке или регистрах процессора. Большинство языков отстраняют вас (абстрагируют) от таких подробностей, позволяя сосредоточиться на решении прикладной задачи.
При объявлении классов в объектно-ориентированных языках вы можете использовать такие имена и интерфейсы, которые отражают смысл и назначение объектов предметной области. "Удаление" элементов, не связанных напрямую с решением задачи, позволит вам полностью сосредоточиться на самой задаче и решить ее более эффективно
Однако язык — это один уровень абстрагирования. Если вы пойдете дальше, то, как разработчику класса, вам нужно придумать такую степень абстрагирования, чтобы клиенты вашего класса могли сразу сосредоточиться на своей задаче, не тратя время на изучение работы класса. На очевидный вопрос — какое отношение интерфейс класса имеет к абстрагированию? — можно ответить так: интерфейс класса и есть реализация абстрагирования.
При определении нужной степени абстрагирования класса важно помнить и о программисте клиентского кода. Представьте, что вы пишете основное ядро базы данных. Возможно, вы прекрасно разбираетесь в таких понятиях БД, как курсоры (cursors), управление фиксацией (commitment control) и кортежи (tuples). Однако многие разработчики, не столь искушенные в программировании БД, не собираются вникать в тонкости этих понятий. Используя терминологию, непонятную клиентам вашего класса, вы не достигнете основной цели абстрагирования — повысить эффективность работы программиста путем представления предметной области в понятных ему и естественных терминах.
Кроме того, решая, какие члены класса сделать открытыми, надо опять вспомнить о клиенте. Это еще раз подтверждает необходимость иметь хотя бы начальное представление о предметной области и клиентах вашего класса. Так, в случае с БД ваши клиенты, наверное, не должны иметь прямого доступа к членам, представляющим внутренние буферы данных. Ведь структура этих буферов может когда-нибудь измениться. Кроме того, от целостности этих буферов зависит вся работа ядра БД, и поэтому операции по их изменению следует выполнять только вашими методами. Только после этого можно сказать, что предприняты все меры предосторожности.
О пользе абстрагирования
Наличие в классах абстрагирования, которое максимально удобно для программистов, работающих с этими классами, имеет первостепенное значение при разработке повторно используемого ПО. Если вы выстроите интерфейс, на который не влияют изменения в реализации, то вашему приложению долгое время не понадобятся никакие модификации. Вспомните пример с расчетом зарплаты. При работе с объектом Employee и функциями, обеспечивающими расчет зарплаты, клиенту нужны лишь несколько методов, таких как CalculatePay, GetAddress и GetEmployeeType. Если вы знакомы с предметной областью задачи, вы без труда определите, какие методы понадобятся пользователям класса. Скажем так: если при проектировании класса вам удается сочетать хорошее знание предметной области с прогнозом относительно дальнейших перспектив использования класса, можно гарантировать, что большая часть интерфейса этого класса останется неизменной, даже в случае возможного совершенствования реализации класса.
Поясним сказанное на примере.
using System;
class Employee
{
public Employee(string firstName, string lastName, int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate; }
protected string firstName;
protected string lastName;
protected int age;
protected double payRate;
public virtual double CalculatePay(int hoursWorked)
{
Console.WriteLine("Employee.CalculatePay"); return 42;
}
class SalariedEmployee : Employee {
public SalariedEmployee(string firstName, string lastName,
int age, double payRate) : base(firstName, lastName, age, payRate) {}
public override double CalculatePay(int hoursWorked) {
Console.WriteLine("SalariedEmployee.CalculatePay"); return 42; // произвольное число } } .
class ContractorEmployee : Employee {
public ContractorEmployee(string firstName, string lastName, int age, double payRate)
: base(firstName, lastName, age, payRate)}
public override double CalculatePay(int hoursWorked) {
Console.WriteLineC'ContractorEmployee.CalculatePay");
return 42; // произвольное число } }
class HourlyEmployee : Employee {
public HourlyEmployee(string firstName, string lastName, int age, double payRate)
: base(firstName, lastName, age, payRate){}
public override double CalculatePay(int hoursWorked)
Console.WriteLine("Hou rlyEmployee.CalculatePay");
return 42; // произвольное число }class PolyApp {
protected Employee[] employees;
protected void LoadEmployeesQ
{
Console.WriteLine("Загрузка информации о сотрудниках...");
// В реальном приложении эти сведения мы // возьмем, наверное, из базы данных, employees = new Employee[3];
employees[0] = new SalariedEmployee ("Amy", "Ariderson", 28, 100);
employees[1] = new ContractorEmployee ("John", "Maffei", 35, 110); employees[2] = new HourlyEmployee ("Lani", "Ota", 2000, 5);
Console. Writel_ine( "\n"); }
protected void CalculatePayO .
{foreach(Employee emp in employees)
emp.CalculatePay(40);
} }
public static void Main()
{PolyApp app = new PolyAppQ;
app.LoadEmployees(); app. CalculatePayO; } }
Использование обобщенных (шаблонных) типов в языках С++иС#. Обзор библиотеки STL.
Шаблоны типа
Зачем программисту может понадобиться определить такой тип, как вектор целых чисел? Как правило, ему нужен вектор из элементов, тип которых неизвестен создателю класса Vector. Следовательно, надо суметь определить тип вектора так, чтобы тип элементов в этом определении участвовал как параметр, обозначающий "реальные" типы элементов:
template < class T > class Vector // вектор элементов типа T
{ T * v;
int sz;
public:
Vector ( int s )
{ if ( s <= 0 )
error ( "недопустимый для Vector размер" );
v = new T [ sz = s ]; // выделить память для массива s типа T
}
T & operator [] ( int i );
int size () { return sz; }};
Таково определение шаблона типа. Он задает способ получения семейства сходных классов. Это описание отличается от обычного описания класса наличием начальной конструкции template<class T>, которая и показывает, что описывается не класс, а шаблон типа с заданным параметром-типом.
void f ()
{ Vector < int > v1 ( 100 ); // вектор из 100 целых
Vector < complex > v2 ( 200 ); // вектор из 200 комплексных чисел
v2 [ i ] = complex ( v1 [ x ], v1 [ y ] );}
Синтаксис описания шаблона
Шаблон функции начинается с ключевого слова template, за которым в угловых скобках следует список параметров. Затем следует объявление функции:
template< typename T >
void sort( T array[], int size ); // шаблон sort объявлен, но не определён
template< typename T >
void sort( T array[], int size ) { /* сортировка */} // объявление и определение
template< int BufferSize > // целочисленный параметр
char* read()
{ char *Buffer = new char[ BufferSize ]; /* считывание данных */
return Buffer;}
Ключевое слово typename появилось сравнительно недавно, поэтому стандарт допускает использование class вместо typename:
template< class T >
Шаблоны классов
В классе, реализующем связный список целых чисел, алгоритмы добавления нового элемента списка, поиска нужного элемента не зависят от того, что элементы списка — целые числа. Те же алгоритмы применялись бы и для списка символов, строк, дат, классов игроков, и так далее.
template< class T >
class List
{public:
void Add( const T& Element );
bool Find( const T& Element );};
Использование шаблонов
Для использования шаблона класса, необходимо указать его параметры:
List<int> li;
List<string> ls;
Различия между шаблонами языка C++ и универсальными шаблонами языка C#
Универсальные шаблоны языка C# и шаблоны языка C++ являются функциями языков программирования, обеспечивающими поддержку параметризированных типов. Но между ними существует много различий. На уровне синтаксиса универсальные шаблоны языка C# являются более простым подходом к параметризованным типам, исключающим сложность шаблонов языка C++. Кроме того, в языке C# не делается попытки обеспечить все функциональные возможности, обеспечиваемые шаблонами языка C++. На уровне реализации основное отличие заключается в том, что замена универсальных типов языка C# выполняется во время выполнения и информация универсальных типов, таким образом, сохраняется для экземпляров объектов.
Ниже приведены основные различия между шаблонами языка C++ и C#:
· Универсальные шаблоны языка C# не обеспечивают такую же гибкость, как шаблоны языка C++.
· Язык C# не позволяет использовать не являющиеся типами параметры шаблона.
· Язык C# не поддерживает явную специализацию, т.е. индивидуальную реализацию шаблона для конкретного типа.
· Язык C# не поддерживает частичную специализацию: индивидуальную реализацию для подмножества аргументов типа.
· Язык C# не позволяет использовать параметр типа в качестве базового класса для универсального типа.
· Язык C# не позволяет параметрам типов иметь типы по умолчанию.
· В языке C# параметр универсального типа не может сам быть универсального типа, хотя сконструированные типы могут использоваться как универсальные. Язык C++ не допускает использование параметров шаблона.
· Язык C++ позволяет использование кода, который может быть допустимым не для всех параметров типа в шаблоне и который затем проверяется для конкретного типа, используемого в качестве параметра типа. В языке C# код класса должен быть написан таким образом, чтобы он работал с любым типом, удовлетворяющим требования существующих ограничений. Допускаются только те языковые конструкции, которые могут быть выведены из ограничений.
STL
Библиотека стандартных шаблонов STL (Standard Template Library) - один из тех немногих программных продуктов, чье появление было встречено единодушно всеми профессиональными программистами.
Четыре основных компонента составляют структуру STL:
· итераторы - специальные указатели, позволяющие алгоритмам перемещаться по данным контейнера;
· алгоритмы - вычислительные процедуры;
· контейнеры - блоки хранения данных, управления ими и размещения;
· функциональные объекты - инкапсулируют функции в объекте для использования другими компонентами.
Итераторы
Итератор - это некий обобщенный указатель. Обычные указатели языка Cи++ являются частным случаем итераторов, позволяющих работать с различными структурами данных и типами универсальным способом. Любой алгоритм (универсальная вычислительная процедура), принимая в качестве параметров итераторы, при их обработке не задумывается о типе данных, на которые передаваемые итераторы ссылаются.
Итераторы бывают пяти видов:
· входные (input);
· выходные (output);
· однонаправленные (forward);
· двунаправленные (bidirectional);
· произвольного доступа (random access).
Входные итераторы служат для чтения адресуемых данных. Выходные, напротив, адресуют объекты, в которые данные должны быть записаны. Однонаправленные итераторы обладают всеми свойствами входных и выходных, а также могут перемещаться от начала последовательности адресуемых данных в конец. Что касается двунаправленных итераторов, то они не только обладают свойствами однонаправленных, но и способны перемещаться в любом направлении по цепочке данных: как вперед, так и назад. Итераторы произвольного доступа самые универсальные. Они обладают функциональностью всех четырех других итераторов. Кстати говоря, указатели языка Си++ также являются итераторами произвольного доступа.
Библиотека STL построена так, что итератор более старшего типа может быть подставлен вместо младшего. Так, итератор произвольного доступа может заменить двунаправленный, двунаправленный может быть подставлен вместо однонаправленного и т. д.
Применяя итераторы, важно учитывать такой элемент, как индикатор конца диапазона (end-of-range), т. е. элемент, идущий непосредственно за концом цепочки адресуемых итератором данных. Весьма похоже на схему адресации строк в языке Си++, когда признаком конца строки является символ '\0'. Но при работе с итераторами индикатором конца диапазона может быть любое число.
Алгоритмы
В библиотеке STL существует группа функций, выполняющих некоторые стандартные действия, например поиск, преобразование, сортировку, копирование и т. д. Они называются алгоритмами. Параметрами для алгоритмов, как правило, служат итераторы. Алгоритму нет никакого дела до типа переданного ему итератора. Главное, чтобы последний подпадал под определенную категорию. К примеру, если параметром алгоритма должен быть однонаправленный итератор, то подставляемый итератор должен быть либо однонаправленным, либо двунаправленным, или же итератором произвольного доступа.
Примером алгоритма может служить equal. Он сравнивает две цепочки данных, адресуемых входными итераторами, и описан следующим образом:
template <class InputIterator1, class InputIterator2>
bool equal(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2);
Первый параметр - входной итератор, указывающий на первую цепочку сравниваемых данных. Второй адресует индикатор конца диапазона данных. Третий параметр - вторая цепочка сравниваемых данных. А вот фрагмент сравнения двух векторов (массивов) v1 и v2:
bool isEqual = equal(v1.begin(), v1.end(), v2.begin());
Здесь использованы стандартные методы векторов: begin() возвращает итератор, настроенный на начало цепочки данных, а end() возвращает индикатор выхода за диапазон. Если все элементы векторов попарно равны друг другу, то equal вернет значение "истина" (true).
Отметим, что все алгоритмы можно разделить на две основных категории: те, которые изменяют данные, и те, которые их не изменяют.
Контейнеры
Библиотека STL имеет в своем арсенале элементы, называемые контейнерами. Контейнеры - это объекты, хранящие в себе другие объекты. В STL таких контейнеров десять:
· vector - массив с произвольным доступом, чаще всего применяемый в тех случаях, когда надо последовательно добавлять данные в конец цепочки;
· list - похож на вектор, но эффективен при добавлении и удалении данных в любое место цепочки;
· deque - контейнер, удобный для вставки данных в начало или конец;
· set - набор уникальных элементов, отсортированных в определенном порядке;
· multiset - то же, что и set, но может содержать повторяющиеся копии;
· map - обеспечивает доступ к значениям по ключам;
· multimap - то же, что и map, но допускающий повторяющиеся ключи;
· stack - данные добавляются в одном порядке, а вынимаются в обратном;
· queue - данные добавляются и вынимаются в том же порядке;
· priority queue- то же, что и queue, но может сортировать данные по приоритету.
Надо отметить, что все алгоритмы работают с методами контейнеров, не вдаваясь в детали их реализации. Так, если алгоритму нужно определить, равен ли один элемент из контейнера другому, он просто вызывает перегруженный оператор сравнения "operator = = ()", реализованный в контейнере.
Функциональные объекты
Функциональные объекты - это объекты, у которых задан перегруженный оператор вызова функции "operator ()()". Они очень важны для эффективного использования. В тех местах, где предполагается передача указателей на функцию, создается интерфейс, принимающий объект с реализованным перегруженным оператором вызова функции. Все операторы обычно пишутся как inline, что дает дополнительный выигрыш в скорости.
Функциональными объектами являются все арифметические операторы. Имеются функциональные объекты для вычисления равенства (equal_to), неравенства (not_equal_to), операции "больше" (greater), операции "меньше" (less), операции "больше или равно" (greater_equal), операции "меньше или равно" (less_equal).
С#. Система типов. Приведение типов. Члены класса: статические члены и члены-экземпляры, константы, неизменяемые поля, свойства, индексаторы. Делегаты и обработчики событий. Интерфейсы.
Система типов
Типы данных принято разделять на простые и сложные. У простых (скалярных) типов возможные значения данных едины и неделимы. Сложные типы характеризуются способом структуризации - одно значение сложного типа состоит из множества значений.Есть и другие классификации типов:
· Так, типы разделяются на встроенные типы и пользовательские типы. Встроенные типы изначально принадлежат языку программирования и составляют его базис. В основе системы типов любого языка программирования всегда лежит базисная система типов, встроенных в язык.
· Типы статические и динамические. Для данных статического типа память отводится в момент объявления, требуемый размер данных известен при их объявлении. Для данных динамического типа размер данных в момент объявления обычно неизвестен и память им выделяется динамически по запросу в процессе выполнения программы.
· Типы значимые и ссылочные. Для значимых типов значение переменной является неотъемлемой собственностью переменной . Для ссылочных типов значением служит ссылка на некоторый объект в памяти, расположенный обычно в динамической памяти - "куче". Несколько ссылочных переменных могут указывать на один и тот же объект и разделять его значения.
Всегда в языке присутствуют арифметический, логический (булев), символьный типы. Арифметический тип всегда разбивается на подтипы. Всегда допускается организация данных в виде массивов и записей (структур). Внутри арифметического типа всегда допускаются преобразования, всегда есть функции, преобразующие строку в число и обратно. Т.к язык C# является потомком языка C++, то системы типов этих двух языков совпадают вплоть до названия типов и областей их определения.
Система типов
Стандарт языка C++ включает следующий набор фундаментальных типов.
· Логический тип (bool).
· Символьный тип (char).
· Целые типы. Целые типы могут быть одного из трех размеров - short, int, long, сопровождаемые описателем signed или unsigned, который указывает, как интерпретируется значение, - со знаком или без оного.
· Типы с плавающей точкой. Эти типы также могут быть одного из трех размеров - float, double, long double.
· void, используемый для указания на отсутствие информации.
Язык позволяет конструировать типы:
· Указатели (например, int* - типизированный указатель на переменную типа int).
· Ссылки (например, double& - типизированная ссылка на переменную типа double).
· Массивы (например, char[] - массив элементов типа char).