Автор работы: Пользователь скрыл имя, 28 Марта 2012 в 11:49, курс лекций
Лекции по дисциплине "Программирование"
Лекция 1. Состав языка Типы данных Переменные и операции
Лекция 2. Линейные программы
Лекция 3. Простейшие операторы. Операторы ветвления
Лекция 4. Операторы цикла и передачи управления
Лекция 5. Обработка исключительных ситуаций
Лекция 6. Классы: основные понятия Описание класса
Лекция 7. Параметры методов
Лекция 8. Конструкторы и свойства
Лекция 9. . Массивы
Лекция 10. Символы и строки
Лекция 11 Дополнительные возможности методов. Индексаторы
Лекция 12. Операции класса. Деструкторы
Лекция 13. Наследование классов
Лекция 14. Интерфейсы
Лекция 15. Стандартные интерфейсы .NET
Лекция 16. Структуры и перечисления
Лекция 17. Делегаты
Лекция 18. События
При описании операций необходимо соблюдать следующие правила:
операция должна быть описана как открытый статический метод класса (спецификаторы publicstatic);
параметры в операцию должны передаваться по значению (то есть не должны предваряться ключевыми словами ref или out);
сигнатуры всех операций класса должны различаться;
типы, используемые в операции, должны иметь не меньшие права доступа, чем сама операция (то есть должны быть доступны при использовании операции).
В C# существуют три вида операций класса: унарные, бинарные и операции преобразования типа.
Можно определять в классе следующие унарные операции:
+ - ! ~ ++ -- true false
Синтаксис объявителя унарной операции:
тип operator унарная_операция ( параметр )
Примеры заголовков унарных операций:
public static int operator +( MyObject m )
public static MyObject operator --( MyObject m )
public static bool operator true( MyObject m )
Параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция должна возвращать:
для операций +, -, ! и ~ величину любого типа;
для операций ++ и -- величину типа класса, для которого она определяется;
для операций true и false величину типа bool.
Операции не должны изменять значение передаваемого им операнда. Операция, возвращающая величину типа класса, для которого она определяется, должна создать новый объект этого класса, выполнить с ним необходимые действия и передать его в качестве результата.
Можно определять в классе следующие бинарные операции:
+ - * / % & | ^ << >> == != > < >= <=
Синтаксис объявителя бинарной операции:
тип operator бинарная_операция (параметр1, параметр2)
Примеры заголовков бинарных операций:
public static MyObject operator + ( MyObject m1, MyObject m2 )
public static bool operator == ( MyObject m1, MyObject m2 )
Хотя бы один параметр, передаваемый в операцию, должен иметь тип класса, для которого она определяется. Операция может возвращать величину любого типа.
Операции == и !=, > и <, >= и <= определяются только парами и обычно возвращают логическое значение. Чаще всего в классе определяют операции сравнения на равенство и неравенство для того, чтобы обеспечить сравнение объектов, а не их ссылок, как определено по умолчанию для ссылочных типов.
Сложные операции присваивания (например, +=) определять не требуется, да это и невозможно. При выполнении такой операции автоматически вызываются сначала операция сложения, а потом присваивания.
Операции преобразования типа обеспечивают возможность явного и неявного преобразования между пользовательскими типами данных. Синтаксис объявителя операции преобразования типа:
implicit operator тип ( параметр ) // неявное преобразование
explicit operator тип ( параметр ) // явное преобразование
Эти операции выполняют преобразование из типа параметра в тип, указанный в заголовке операции. Одним из этих типов должен быть класс, для которого определяется операция. Таким образом, операции выполняют преобразование либо типа класса к другому типу, либо наоборот. Преобразуемые типы не должны быть связаны отношениями наследования. Примеры операций преобразования типа для класса Monster, описанного ранее:
public static implicit operator int( Monster m )
{
return m.health;
}
public static explicit operator Monster( int h )
{
return new Monster( h, 100, "FromInt" );
}
Ниже приведены примеры использования этих преобразований в программе. Не надо искать в них смысл, они просто иллюстрируют синтаксис:
Monster Masha = new Monster( 200, 200, "Masha" );
int i = Masha; // неявное преобразование
Masha = (Monster) 500; // явное преобразование
Неявное преобразование выполняется автоматически:
при присваивании объекта переменной целевого типа, как в примере;
при использовании объекта в выражении, содержащем переменные целевого типа;
при передаче объекта в метод на место параметра целевого типа;
при явном приведении типа.
Явное преобразование выполняется при использовании операции приведения типа.
Все операции класса должны иметь разные сигнатуры. В отличие от других видов методов, для операций преобразования тип возвращаемого значения включается в сигнатуру, иначе нельзя было бы определять варианты преобразования данного типа в несколько других. Ключевые слова implicit и explicit в сигнатуру не включаются, следовательно, для одного и того же преобразования нельзя определить одновременно явную и неявную версию.
Неявное преобразование следует определять так, чтобы при его выполнении не возникала потеря точности и не генерировались исключения. Если эти ситуации возможны, преобразование следует описать как явное.
В C# существует специальный вид метода, называемый деструктором. Он вызывается сборщиком мусора непосредственно перед удалением объекта из памяти. В деструкторе описываются действия, гарантирующие корректность последующего удаления объекта, например, проверяется, все ли ресурсы, используемые объектом, освобождены (файлы закрыты, удаленное соединение разорвано и т. п.)
Синтаксис деструктора:
[ атрибуты ] [ extern ] ~имя_класса()
тело
Как видно из определения, деструктор не имеет параметров, не возвращает значения и не требует указания спецификаторов доступа. Его имя совпадает с именем класса и предваряется тильдой (~), символизирующей обратные по отношению к конструктору действия. Тело деструктора представляет собой блок или просто точку с запятой, если деструктор определен как внешний (extern).
Сборщик мусора удаляет объекты, на которые нет ссылок. Он работает в соответствии со своей внутренней стратегией в неизвестные для программиста моменты времени. Поскольку деструктор вызывается сборщиком мусора, невозможно гарантировать, что деструктор будет обязательно вызван в процессе работы программы. Следовательно, его лучше использовать только для гарантии освобождения ресурсов, а «штатное» освобождение выполнять в другом месте программы.
В классе можно определять типы данных, внутренние по отношению к классу. Так определяются вспомогательные типы, которые используются только содержащим их классом. Механизм вложенных типов позволяет скрыть ненужные детали и более полно реализовать принцип инкапсуляции. Непосредственный доступ извне к такому классу невозможен (имеется в виду доступ по имени без уточнения). Для вложенных типов можно использовать те же спецификаторы, что и для полей класса.
Например, введем в наш класс Monster вспомогательный класс Gun. Объекты этого класса без «хозяина» бесполезны, поэтому его можно определить как внутренний:
using System;
namespace ConsoleApplication1
{ class Monster
{
class Gun
{
...
}
...
}
}
Помимо классов, вложенными могут быть и другие типы данных: интерфейсы, структуры и перечисления. Мы рассмотрим их позже.
Лекция 13. Наследование классов
Управлять большим количеством разрозненных классов достаточно сложно. С этой проблемой можно справиться путем упорядочивания и ранжирования классов, то есть объединяя общие для нескольких классов свойства в одном классе и используя его в качестве базового.
Эту возможность предоставляет механизм наследования, который является мощнейшим инструментом ООП. Он позволяет строить иерархии, в которых классы-потомки получают свойства классов-предков и могут дополнять их или изменять. Таким образом, наследование обеспечивает важную возможность многократного использования кода.
Классы, расположенные ближе к началу иерархии, объединяют в себе общие черты для всех нижележащих классов. По мере продвижения вниз по иерархии классы приобретают все больше конкретных особенностей.
Итак, наследование применяется для следующих взаимосвязанных целей:
исключения из программы повторяющихся фрагментов кода;
упрощения модификации программы;
упрощения создания новых программ на основе существующих.
Класс в C# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:
[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]
тело класса
Рассмотрим наследование классов на примере. Ранее был описан класс Monster, моделирующий персонаж компьютерной игры. Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта Monster, а кроме того, уметь думать. Будет логично сделать новый объект потомком объекта Monster (листинг 15.1).
Листинг 15.1. Класс Daemon, потомок класса Monster
using System;
namespace ConsoleApplication1
{
class Monster
{
...
}
class Daemon : Monster
{
public Daemon()
{
brain = 1;
}
public Daemon( string name, int brain ) : base( name ) // 1
{
this.brain = brain;
}
public Daemon( int health, int ammo, string name, int brain )
: base( health, ammo, name )
{
this.brain = brain;
}
new public void Passport()
{
Console.WriteLine(
"Daemon {0} \t health = {1} ammo = {2} brain = {3}",
Name, Health, Ammo, brain );
}
public void Think()
{
Console.Write( Name + " is" );
for ( int i = 0; i < brain; ++i ) Console.Write( " thinking" );
Console.WriteLine( "..." );
}
int brain; // закрытое поле
}
class Class1
{ static void Main()
{
Daemon Dima = new Daemon( "Dima", 3 ); // 5
Dima.Passport();
Dima.Think();
Dima.Health -= 10;
Dima.Passport();
}
}
}
В классе Daemon введены закрытое поле brain и метод Think, определены собственные конструкторы, а также переопределен метод Passport. Все поля и свойства класса Monstr наследуются в классе Daemon.
Результат работы программы:
Daemon Dima health = 100 ammo = 100 brain = 3
Dima is thinking thinking thinking...
Daemon Dima health = 90 ammo = 100 brain = 3
Как видите, экземпляр класса Daemon с одинаковой легкостью использует как собственные (операторы 5–7), так и унаследованные (оператор 8) элементы класса. Рассмотрим общие правила наследования.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными ниже правилами.
Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.
Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким образом, каждый конструктор инициализирует свою часть объекта.
Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации. Вызов выполняется с помощью ключевого слова base. Вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.
Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new. В листинге 15.1 таким образом переопределен метод вывода информации об объекте Passport. Метод Passport класса Daemon замещает соответствующий метод базового класса, однако возможность доступа к методу базового класса из метода производного класса сохраняется. Для этого перед вызовом метода указывается все то же волшебное слово base, например:
base.Passport();
Элементы базового класса, определенные как private, в производном классе недоступны. Поэтому в методе Passport для доступа к полям name, health и ammo пришлось использовать соответствующие свойства базового класса. Другое решение заключается в том, чтобы определить эти поля со спецификатором protected, в этом случае они будут доступны методам всех классов, производных от Monster. Оба решения имеют свои достоинства и недостатки.
Во время выполнения программы объекты хранятся в отдельных переменных, массивах или других коллекциях. Во многих случаях удобно оперировать объектами одной иерархии единообразно, то есть использовать один и тот же программный код для работы с экземплярами разных классов. Это возможно благодаря тому, что объекту базового класса можно присвоить объект производного класса.