giallorossy1989@mail.ru
Вибачте, що міш-маш із мовами, не встиг всі лекції перекласти, да і граматичних помилок вистачає (.
ЛЕКЦІЯ
ВИКОРИСТАННЯ ОПЕРАЦІЙ
С# дозволяє перевизначити дії більшості операцій так, щоб при використанні з об’єктами конкретного класу вони виконували задані функції. Визначення власних операцій класу називають перезавантаженням операцій.
Синтаксис:
[aтрибути] спеціфікатори об’явник_операції тіло
В якості спеціфікаторів одночасно використовуються ключові слова public і static. Крім того операцію можливо об’являти як зовнішню [extern].
Об’явник_операції – містить ключове слово operator.
Тіло операції – визначає дії, які виконуються при використанні операцій у виразах. Тіло представляє собою блок який аналогічний тілу інших методів.
Зауваження: нові позначення для власних операцій вводити не можна. Для операцій класу зберігаються кількість аргументів, пріоритети операцій і правила асоціацій, які використовуються у стандартних типах даних.
Правила:
операція повинна бути описана, як відкритий статичний метод класу (public static);
параметри в операцію повинні передаватись за значенням (тобто не можна використовувати ref або out);
сигнатури для всіх операцій класу повинні різними;
типи, які використовуються в операції, повинні мати не менші права доступу ніж сама операція.
Існують три типи операцій класу: унарні, бінарні і операції перетворення типів.
Унарні операції
+ - ! ~ ++ -- true false
Синтаксис:
тип operator унарна_операція (параметр)
Приклади:
public static int operator +(MyObject m)
public static bool operator true(MyObject m)
Параметр, який передається в операцію, повинен мати тип для якого вона визначається.
Операція повинна повертати:
+, - , !, ~ - величину довільного типу;
++, --, - величину, яка має тип класу для якого вона визначена;
true, false – величину типу bool.
Операції не повинні змінювати значення параметрів, які їм передаються. Операція, яка повертає величину типу класу, для якого вона визначена, повинна створювати новий об’єкт цього класу, виконувати з ним необхідні дії і передавати його у якості результату.
Зауваження: Префіксний і постфіксний інкременти не розрізняються (для них може існувати тільки одна реалізація, яка викликається в обох випадках )
Приклад:
class SafeArray
{
int[] a;
int length;
public SafeArray(int size)
{
a = new int[size];
length = size;
}
public SafeArray(params int[] arr)
{
length = arr.Length;
a = new int[length];
for (int i = 0; i < length; ++i)
a[i] = arr[i];
}
public static SafeArray operator ++(SafeArray x)
{
SafeArray temp = new SafeArray(x.length);
for (int i = 0; i < x.length; ++i)
temp[i] = ++x.a[i];
return temp;
}
public int this[int i]
{
get
{
if (i >= 0 && i < length) return a[i];
else throw new IndexOutOfRangeException();
}
set
{
if (i >= 0 && i < length) a[i] = value;
else throw new IndexOutOfRangeException();
}
}
public void Print(string name)
{
Console.WriteLine(name + ":");
for (int i = 0; i < length; ++i)
Console.Write("\t"+a[i]);
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
try
{
SafeArray a1 = new SafeArray(5, 2, -1, 100, -2);
a1.Print("Masiv 1");
a1++;
a1.Print("Increment masiva 1");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
Описание класса:
class Point2D
{
float x,y;
Point2D()
{ x=0; y=0; }
Point2D(Point2D key)
{ x=key.x;
y=key.y;
}
}
Бинарные операции.
Пример объявления и вызова
Семантика перегружаемой операторной функции определяется решаемыми задачами и фантазией разработчика:
// Бинарные операции также обязаны возвращать значения!
public static Point2D operator + (Point2D par1, Point2D par2)
{
return new Point2D(par1.x+par2.x,par1.y+par2.y);
}
// Реализуется алгоритм "сложения" значения типа Point2D
// со значением типа float.
// От перемены мест слагаемых сумма НЕ ИЗМЕНЯЕТСЯ.
// Однако эта особенность нашей операторной функции
//"сложения" (операции "сложения") должна быть
// прописана программистом.
// В результате получаем ПАРУ операторных функций, которые отличаются
// списками параметров.
// Point2D + float
public static Point2D operator + (Point2D par1, float val)
{
return new Point2D(par1.x+val,par1.y+val);
}
// float + Point2D
public static Point2D operator + (float val, Point2D par1)
{
return new Point2D(val+par1.x,val+par1.y);
}
А вот применение этих функций. Внешнее сходство выражений вызова операторных функций с обычными выражениями очевидно.
И при этом иного способа вызова операторных функций нет!
...p1 + p2...
...3.14 + p2...
...p2 + 3.14...
Операции сравнения реализуются по аналогичной схеме. Хотя не существует никаких ограничений на тип возвращаемого значения, в силу специфики применения (обычно в условных выражениях операторов управления) операций сравнения все же имеет смысл определять их как операторные функции, возвращающие значения true и false:
public static bool operator == (myPoint2D par1, myPoint2D par2)
{
if ((par1.x).Equals(par2.x) && (par1.y).Equals(par2.y))
return true;
else
return false;
}
public static bool operator != (myPoint2D par1, myPoint2D par2)
{
if (!(par1.x).Equals(par2.x) || !(par1.y).Equals(par2.y))
return true;
else
return false;
}
operator true и operator false
В классе может быть объявлена операция (операторная функция) true, которая возвращает значение true типа bool для обозначения факта true и возвращает false в противном случае.
Классы, включающие объявления подобных операций (операторных функций), могут быть использованы в структуре операторов if, do, while, for в качестве условных выражений.
При этом, если в классе была определена операция true, в том же классе должна быть объявлена операция false:
// Перегрузка булевских операторов. Это ПАРНЫЕ операторы.
// Объекты типа Point2D приобретают способность судить о правде и лжи!
// А что есть истина? Критерии ИСТИННОСТИ (не путать с истиной)
// могут быть самые разные. В частности, степень удаления от точки
// с координатами (0,0).
public static bool operator true (Point2D par)
{
if (Math.Sqrt(par.x*par.x + par.y*par.y) < 10.0) return true;
else return false;
}
public static bool operator false (Point2D par)
{
double r = (Math.Sqrt(par.x*par.x + par.y*par.y));
if (r > 10.0 || r.Equals(10.0)) return false;
else return true;
}
Операції перетворення типів
забезпечують можливість явного і неявного перетворення між типами користувача.
Синтаксис:
implicit operator тип (параметр) // неявне приведення
explicit operator тип (параметр) // явне приведення
Дані операції виконують перетворення із типу параметра в тип, який вказаний у заголовку операції. Одним із таких типів повинен бути клас, для якого визначається операція.
Типи які приводяться не повинні бути пов’язані відношенням наслідування.
Приклад:
public static implicit operator int (Car m)
{
return m.speed;
}
public static explicit operator Car (int h)
{
return new Car (h, 120);
}
Car BMW = new Car(100, 160);
int i = BMW;
BMW = (Car)300;
Неявне перетворення виконується автоматично:
при присвоєнні об’єкта змінній цільового типу;
при використанні об’єкта у виразі, який містить змінні цільового типу;
при передачі об’єкта у метод на місце параметра цільового типу;
при явному приведенні типу;
Явне перетворення виконується при використанні операції приведення типу.
Всі операції класу повинні мати різні сигнатури. Для одного і того ж перетворення не можна визначити одночасно явну і неявну версію.
Неявне перетворення слід визначати так, щоб при його виконані не виникала втрата точності і не генерувалась випадкова ситуація.
Лекция
ИЕРАРХИЯ КЛАССОВ.
Вопросы:
Использование наследования.
Виртуальные методы.
Абстрактные классы.
Бесплодные классы.
Использование наследования.
Наследование применяется для следующих взаимосвязанных целей:
исключения из программы повторяющихся фрагментов кода;
упрощения модификации программы;
упрощения создания новых программ на основе существующих.
Кроме того, наследование является единственной возможностью использовать объекты, исходный код которых недоступен, но в которые требуется внести изменения.
Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке классапосле двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:
[ атрибуты ] [ спецификаторы ] class имякласса [ : предки ]
тело класса
Обратите внимание на то, что слово предки присутствует в описании класса во множественном числе, хотя класс может иметь только одного предка. Причина в том, что класс наряду с единственным предком может наследовать от интерфейсов — специального вида классов, не имеющих реализации.
Общие правила наследования.
Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется приведенными далее правилами:
Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.
Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким образом, каждый конструктор инициализирует свою часть объекта.
Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации. Вызов выполняется с помощью ключевого слова base. Вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.
Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new.
Итак, присваивать объекту базового класса объект производного класса можно, но вызываются для него только методы и свойства, определенные в базовом классе. Иными словами, возможность доступа к элементам класса определяется типом ссылки, а не типом объекта, на который она указывает. Это и понятно: ведь компилятор должен еще до выполнения программы решить, какой метод вызывать, и вставить в код фрагмент, передающий управление на этот метод (этот процесс называется ранним связыванием).
При этом компилятор может руководствоваться только типом переменной, для которой вызывается метод или свойство. То, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может. Следовательно, если мы хотим, чтобы вызываемые методы соответствовали типу объекта, необходимо отложить процесс связывания до этапа выполнения программы, а точнее — до момента вызова метода, когда уже точно известно, на объект какого типа указывает ссылка.
Такой механизм в С# есть — он называется поздним связыванием и реализуется с помощью так называемых виртуальных методов.
Виртуальные методы.
При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного об'ьекта, для которого выполняется вызов, то заранее жестко связывать эти методы с остальной частью кода нельзя.
Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому.
Для этого в С# существует ключевое слово virtual.
Оно записывается в заголовке метода базового класса, например:
virtual public void Print();
Слово virtual в переводе с английского значит «фактический».
Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения программы. Этот механизм называется поздним связыванием.
Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table, VMT) .
В нее записываются адреса виртуальных методов (в том числе унаследованных) в порядке описания в классе. Для каждого класса создается одна таблица. Каждый объект во время выполнения должен иметь доступ к VMT. Обеспечение этой связи нельзя поручить компилятору, так как она должна устанавливаться во время выполнения программы при создании объекта. Поэтому связь экземпляра объекта с VMT устанавливается с помощью специального кода, автоматически помещаемого компилятором в конструктор объекта.
Если в производном классе требуется переопределить виртуальный метод, используется ключевое слово override, например:
override public void Print();
Переопределенный виртуальный метод должен обладать таким же набором параметров, как и одноименный метод базового кчасса. Это требование вполне естественно, если учесть, что одноименные методы, относящиеся к разным классам, могут вызываться из одной и той же точки программы.
Виртуальные методы базового класса определяют интерфейс всей иерархии. Этот интерфейс может расширяться в потомках за счет добавления новых виртуальных методов. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, метод наследуется.
Вызов виртуального метода выполняется так: из объекта берется адрес его таблицы VMT, из VMT выбирается адрес метода, а затем управление передается этому методу. Таким образом, при использовании виртуальных методов из всех одноименных методов иерархии всегда выбирается тот, который соответствует фактическому типу вызвавшего его объекта.
namespace ShapeVirtual
{
class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
}
public virtual void Draw(int x, int y)
{ }
}
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{ }
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x;
xPos = у;
}
}
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1}),
x, у) ;
xPos = х;
yPos = у;
}
}
class ShapeVirtualApp
{
static void Main(string[] args)
{
Shape pt = new PointA0, 25);
pt.Draw(4, 7);
Shape rect = new Rectangle(1, 4, 10, 20);
rect.Draw(10, 12) ;
Shape[] allShapes = new Shape[4];
allShapes[0] = new Point(1, 5) ;
allShapes[1] = new Rectangle(2, 3, 1, 5);
allShapes[2] = new Rectangle(1, 2, 3, 4);
allShapes[3] = new Point(31, 4);
foreach(Shape currentShape in allShapes)
currentShape.Draw(0, 0);
Console.ReadLine();
}
}
}
ПРИМЕЧАНИЕ: Вызов виртуального метода, в отличие от обычного, выполняется через дополнительный этап получения адреса метода и з таблицы VMT , что несколько замедляет выполнение программы.
С помощью виртуальных методов реализуется один из основных принципов объектно- ориентированного программирования — полиморфизм. Применение виртуальных методов обеспечивает гибкость и возможность расширения функциональности класса. Виртуальные методы незаменимы и при передаче объектов в методы в качестве параметров. В параметрах метода описывается объект базового типа, а при вызове в нее передается объект производного класса. В этом случае виртуальные методы, вызываемые для объекта из метода, будут соответствовать типу аргумента, а не параметра. При описании классов рекомендуется определять в качестве виртуальных те методы, которые в производных классах должны реализовываться по-другому. Если во всех классах иерархии метод будет выполняться одинаково, его лучше определить как обычный метод.
ПРИМЕЧАНИЕ: Все сказанное о виртуальных методах относится также к свойствам и индексаторам.
Абстрактные классы.
При создании иерархии объектов для исключения повторяющегося кода часто бывает логично выделить их общие свойства в один родительский класс. При этом может оказаться, что создавать экземпляры такого класса не имеет смысла, потому что никакие реальные объекты им не соответствуют. Такие классы называют абстрактными.
Абстрактный класс служит только для порождения потомков. Как правило, в нем задается набор методов, которые каждый из потомков будет реализовывать по-своему. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах. Абстрактный класс задает интерфейс для всей иерархии, при этом методам класса может не соответствовать никаких конкретных действий. В этом случае методы имеют пустое тело и объявляются со спецификатором abstract.
ПРИМЕЧАНИЕ Абстрактный класс может содержать и полностью определенные методы, в отличие от сходного с ним по предназначению специального вида класса, называемого интерфейсом.
Если в классе есть хотя бы один абстрактный метод, весь класс также должен быть описан как абстрактный.
Абстрактные классы используются при работе со структурами данных, предназначенными для хранения объектов одной иерархии, и в качестве параметров методов. Если класс, производный от абстрактного, не переопределяет все абстрактные методы, он также должен описываться как абстрактный. Можно создать метод, параметром которого является абстрактный класс. На место этого параметра при выполнении программы может передаваться объект любого производного класса. Это позволяет создавать полиморфные методы, работающие с объектом любого типа в пределах одной иерархии.
Пример:
abstract class Shape
{
protected int xPos;
protected int yPos ;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
}
abstract public void Draw(int x, int y);
}
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у)
xPos = x;
xPos = y;
}
}
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})",
x, у) ;
xPos = х;
yPos = у;
}
}
class ShapeAbstractApp
{
static void Main(string[] args)
{
Shape[] allShapes = new Shape[4];
allShapes[0] = new Point(1, 5);
allShapes[1] = new Rectangle(2, 3, 1, 5);
allShapes[2] = new Rectangle(1, 2, 3, 4);
allShapes[3] = new Point(1, 4) ;
foreach(Shape currentshape in allShapes)
currentShape.Draw(0, 0) ;
Console.ReadLine();
}
}
Обратите внимание, что объявление абстрактного метода заканчивается точкой с запятой и не имеет тела, ограниченного фигурными скобками. Использование ключевого слова abstract при объявлении метода исключает не- необходимость (а также возможность) указания ключевых слов override и virtual. Кроме того, это ключевое слово несовместимо с ключевым словом static.
Бесплодные классы.
В С# есть ключевое слово sealed, позволяющее описать класс, от которого, в противоположность абстрактному, наследовать запрещается.
Большинство встроенных типов данных описано как sealed. Если необходимо использовать функциональность бесплодного класса, применяется не наследование, а вложение, или включение: в классе описывается поле соответствующего типа.
Вложение классов, когда один класс включает в себя поля, являющиеся классами, является альтернативой наследованию при проектировании. Например, если есть объект «двигатель», а требуется описать объект «самолет», логично сделать двигатель полем этого объекта, а не его предком.
Поскольку поля класса обычно закрыты, возникает вопрос, как же пользоваться методами включенного объекта. Общепринятый способ состоит в том, чтобы описать метод объемлющего класса, из которого вызвать метод включенного класса. Такой способ взаимоотношений классов известен как модель включения- делегирования.
При этом закрываться для наследования может как класс целиком, так и отдельные его члены:
sealed class X
{ }
class Y:X // Наследование от класса X невозможно
{ }
А так закрывается для переопределения функция – член класса:
class X
{
sealed public void f0()
{ }
}
class Y:X
{
public void f0(){}
// Наследование (переопределение) f0, объявленной в X,
//запрещено!
}
ЛЕКЦИЯ
ИНТЕРФЕЙСЫ
Работа с объектами через интерфейсы.
Операции is и as
При работе с объектом через объект типа интерфейса бывает необходимо убедиться, что объект поддерживает данный интерфейс. Проверка выполняется с помощью бинарной операции is.
Эта операция определяет, совместим ли текущий тип объекта, находящегося слева от ключевого слова is, с типом, заданным справа.
Результат операции равен true, если объект можно преобразовать к заданному типу, и false в противном случае.
Операция обычно используется в следующем контексте:
if ( обьект is тип )
{
// выполнить преобразование "объекта" к "типу"
//выполнить действия с преобразованным объектом
}
Допустим, мы оформили какие-то действия с объектами в виде метода с параметром типа object. Прежде чем использовать этот параметр внутри метода для обращения к методам, описанным в производных классах, требуется выполнить преобразование к производному классу.
Для безопасного преобразования следует проверить, возможно ли оно, например так:
static void Act( object A )
{
if ( A is IAction )
{
IAction Actor =(IAction) A;
Actor.Draw();
}
}
В метод Act можно передавать любые объекты, но на экран будут выведены только те, которые поддерживают интерфейс IAction.
Недостатком использования операции is является то, что преобразование фактически выполняется дважды: при проверке и при собственно преобразовании. Более эффективной является другая операция - as.
Она выполняет преобразование к заданному типу, а если это невозможно, формирует результат null, например:
static void Act( object A )
{
IAction Actor = A as IAction;
if ( Actor != null ) Actor.Draw();
}
Обе рассмотренные операции применяются как к интерфейсам, так и к классам.
Интерфейс может не иметь или иметь сколько угодно интерфейсов-предков, в последнем случае он наследует все элементы всех своих базовых интерфейсов, начиная с самого верхнего уровня.
Базовые интерфейсы должны быть доступны в не меньшей, степени, чем их потомки.
Например, нельзя использовать интерфейс, описанный со спецификатором private или internal, в качестве базового для открытого (public) интерфейса.
Базовые интерфейсы определяют общее поведение, а их потомки конкретизируют и дополняют его.
В интерфейсе-потомке можно также указать элементы, переопределяющие унаследованные элементы с такой же сигнатурой.
В этом случае перед элементом указывается ключевое слово new, как и в аналогичной ситуации в классах. С помощью этого слова соответствующий элемент базового интерфейса скрывается.
Пример:
interface IBase
{
void F( int i );
}
interface ILeft : IBase
{
new void F( int i ); // переопределение метода F
}
interface IRight : IBase
{
void G( );
}
interface Iderived : ILeft, IRight {}
class A
{
void Test( IDerived d )
{
d.F( 1 ); // Вызывается ILeft.F
((IBase)d).F( 1 ); // Вызывается IBase.F
((ILeft)d).F( 1 ); // Вызывается ILeft.F
((IRight)d).F( 1 ); // Вызывается IBase.F
}
}
Метод F из интерфейса IBase скрыт интерфейсом ILeft, несмотря на то что в цепочке IDerived - IRight - IBase он не переопределялся.
Класс, реализующий интерфейс, должен определять все его элементы, в том числе унаследованные. Если при этом явно указывается имя интерфейса, оно должно ссылаться на тот интерфейс, в котором был описан соответствующий элемент.
Класс наследует все методы своего предка, в том числе те, которые реализовывали интерфейсы. Он может переопределить эти методы с помощью спецификатора new, но обращаться к ним можно будет только через объект класса.
Если использовать для обращения ссылку на интерфейс, вызывается не переопределенная версия:
interface IBase
{
void А():
}
class Base : IBase
{
public void А() { ... }
}
class Derived: Base
{
new public void А() { ... }
}
…
Derived d = new Derived ();
d.A(); // вызывается Derived.А()
IBase id = d;
id.А(); // вызывается Base.А()
Однако если интерфейс реализуется с помощью виртуального метода класса, после его переопределения в потомке любой вариант обращения (через класс или через интерфейс) приведет к одному и тому же результату.
Метод интерфейса, реализованный явным указанием имени, объявлять виртуальным запрещается.
При необходимости переопределить в потомках его поведение пользуются следующим приемом: из этого метода вызывается другой, защищенный метод, который объявляется виртуальным.
В приведенном далее примере метод А интерфейса IBase реализуется посредством защищенного виртуального метода А_, который можно переопределять в потомках класса Base:
interface IBase
{
void А();
}
class Base : IBase
{
void IBase.А() { A_( ); }
protected virtual void A_( ) { ... }
class Derived: Base
{
protected override void A_() { ... }
}
Существует возможность повторно реализовать интерфейс, указав его имя в списке предков класса наряду с классом-предком, уже реализовавшим этот интерфейс. При, этом реализация переопределенных методов базового класса во внимание не принимается:
interface IBase
{
void А();
}
class Base : IBase
{
void IBase.А() { ... } //не используется в Derived
}
class Derived : Base, IBase
{
public void А() { ... }
}
Если класс наследует от класса и интерфейса, которые содержат методы с одинаковыми сигнатурами, унаследованный метод класса воспринимается как реализация интерфейса, например:
interface Interface1
{
void F();
}
class Classl
{
public void F() { ... }
public void G() { ... }
}
class Class2 : Class1, Interface1
{
new public void G(){ ... }
}
Здесь класс Class2 наследует от класса Class1 метод F. Интерфейс Interface1 также содержит метод F. Компилятор не выдает ошибку, потому что класс Class2 содержит метод, подходящий для реализации интерфейса.
ЛЕКЦИЯ
ДЕЛЕГАТЫ
Делегат — это вид класса, предназначенный для хранения ссылок на методы. Делегат, как и любой другой класс, можно передать в качестве параметра, а затем вызвать инкапсулированный в нем метод.
Делегаты используются для
поддержки событий;
также как самостоятельная конструкция языка.
Рассмотрим сначала второй случай.
Описание делегатов:
[ атрибуты ] [ спецификаторы ] delegate тип имя_делегата ( [ параметры ] )
Спецификаторы делегата имеют тот же смысл, что и для класса, причем допускаются только спецификаторы new, public, protected, internal и private.
Тип описывает возвращаемое значение методов, вызываемых с помощью делегата, а необязательными параметрами делегата являются параметры этих методов.
Делегат может хранить ссылки на несколько методов и вызывать их поочередно; естественно, что сигнатуры всех методов должны совпадать.
Пример описания делегата:
public delegate void D ( int i );
Здесь описан тип делегата, который может хранить ссылки на методы, возвращающие void и принимающие один параметр целого типа.
Делегат, как и всякий класс, представляет собой тип данных. Его базовым классом является класс System.Delegate, снабжающий своего «отпрыска» некоторыми полезными элементами, которые мы рассмотрим позже.
Наследовать от делегата нельзя, да и нет смысла.
Объявление делегата можно размещать непосредственно в пространстве имен или внутри класса.
Использование делегатов
Для того чтобы воспользоваться делегатом, необходимо создать его экземпляр и задать имена методов, на которые он будет ссылаться. При вызове экземпляра делегата вызываются все заданные в нем методы.
Делегаты применяются в основном для следующих целей:
получения возможности определять вызываемый метод не при компиляции, а динамически во время выполнения программы;
обеспечения связи между объектами по типу «источник — наблюдатель»;
создания универсальных методов, в которые можно передавать другие методы;
поддержки механизма обратных вызовов.
Рассмотрим сначала пример реализации первой из этих целей. В листинге объявляется делегат, с помощью которого один и тот же оператор используется для вызова двух разных методов (Cool и Hack).
delegate void Del ( ref string s ); // объявление делегата
class Classl
{
public static void Cool ( ref string s ) // метод 1
{
string temp = "";
for( int i = 0; i < s.Length; ++i )
{
if ( s[i] == 'о' || s[i] == '0') temp += '0';
else if ( s[i] == 'l' ) temp += '1';
else temp += s[i];
}
s = temp;
}
public static void Hack ( ref string s ) // метод 2
{
string temp = "";
for ( int i = 0; i < s.Length; ++i )
if ( i / 2 * 2 == i ) temp += char.ToUpper( s[i] );
else temp += s[i];
s = temp;
}
static void Main()
{
string s = "cool hackers";
Del d; // экземпляр делегата
for ( int i = 0: i < 2; ++i )
{
d = new Del ( Cool); // инициализация методом 1
if ( I == 1 ) d = new Del (Hack); // инициализация методом 2
d( ref s ); // использование делегата для вызова методов
Console.WriteLine(s);
}
}
}
Результат работы программы:
c001 hackers
С001 hAcKeRs
Использование делегата имеет тот же синтаксис, что и вызов метода.
Если делегат хранит ссылки на несколько методов, они вызываются последовательно в том порядке, в котором были добавлены в делегат.
Добавление метода в список выполняется либо с помощью метода Combine, унаследованного от класса System.Delegate, либо, что удобнее, с помощью перегруженной операции сложения.
Вот как выглядит измененный метод Main из предыдущего листинга, в котором одним вызовом делегата выполняется преобразование исходной строки сразу двумя методами:
string s = "cool hackers":
Del d = new Del ( Cool );
d += new DeK Hack ); // добавление метода в делегат
d( ref s );
Console.WriteLine( s ): // результат: C001 hAcKeRs
При вызове последовательности методов с помощью делегата необходимо учитывать следующее:
сигнатура методов должна в точности соответствовать делегату;
методы могут быть как статическими, так и обычными методами класса;каждому методу в списке передается один и тот же набор параметров;
если параметр передается по ссылке, изменения параметра в одном методе отразятся на его значении при вызове следующего метода;
если параметр передается с ключевым словом out или метод возвращает значение, результатом выполнения делегата является значение, сформированное последним из методов списка (в связи с этим рекомендуется формировать списки только из делегатов, имеющих возвращаемое значение типа void);
если в процессе работы метода возникло исключение, не обработанное в том же методе,