Конспект лекций (C#)-unlocked. 1 Основные сведения о C# Особенности языка
![]()
|
6.3 События Событие, по существу, представляет собой автоматическое уведомление о том, что произошло некоторое действие. События действуют по следующему прин- ципу: объект, проявляющий интерес к событию, регистрирует обработчик этого со- бытия. Когда же событие происходит, вызываются все зарегистрированные обра- ботчики этого события. Обработчики событий обычно представлены делегатами, для чего использует- ся следующая формальная запись: event <имя делегата> <имя события>; Кроме того, обычно в классе имеется метод, вызываемый при необходимости активизации события (обычно название метода начинается с On ). Пример: создание события и вызов его для различных экземпляров класса. delegate void MyEventHandler(); // Делегат для события class RunEvent // Класс события { public event MyEventHandler MyEvent; // Само событие public void OnMyEvent() // Метод активизации события { if (MyEvent != null) MyEvent(); } 97 } class MyClass // Класс, заинтересованный в событии { public int i = 5; public void Add2() // Метод, выполняемый при возникновении события { i += 2; } } RunEvent clRun = new RunEvent(); MyClass cl1 = new MyClass(); MyClass cl2 = new MyClass(); clRun.OnMyEvent(); // cl1: 5, cl2: 5 clRun.MyEvent += cl1.Add2; clRun.OnMyEvent(); // cl1: 7, cl2: 5 clRun.MyEvent += cl2.Add2; clRun.OnMyEvent(); // cl1: 9, cl2: 7 clRun.MyEvent -= cl1.Add2; clRun.OnMyEvent(); // cl1: 9, cl2: 9 События получаются объектами в порядке регистрации интереса к ним. До- бавление обработчиков событий в цепочку обработчиков событий, а также удаление из неё выполняется, по умолчанию, автоматически. Однако, при необходимости, можно реализовать действия, выполняемые при добавлении или удалении обработ- чика событий. Формальная запись реализации такого события имеет вид: event <имя делегата> <имя события> { add { ... } remove { ... } } В блоках add и remove полученный обработчик события доступен через ав- томатически созданное свойство value При реализации события с обработкой добавления и удаления становится не- возможным использовать его нигде, кроме левой части операторов += и -= . Поэтому запоминание всех обработчиков и работа с ними должна осуществляться вручную. Модификация предыдущего примера с пользовательской обработкой добавле- ния и удаления обработчиков и подсчётом количества обработчиков будет иметь вид: delegate void MyEventHandler(); class RunEvent { public int Count = 0; // Количество обработчиков private MyEventHandler ListHandler = null;// Цепочка обработчиков public event MyEventHandler MyEvent { add 98 { Count++; ListHandler += value; } remove { Count--; ListHandler -= value; } } public void OnMyEvent() { if (ListHandler != null) // Теперь здесь MyEvent ListHandler(); // использовать нельзя } } class MyClass { public int i = 5; public void Add2() { i += 2; } } RunEvent clRun = new RunEvent(); MyClass cl1 = new MyClass(); MyClass cl2 = new MyClass(); clRun.OnMyEvent(); // Count: 0, cl1: 5, cl2: 5 clRun.MyEvent += cl1.Add2; clRun.OnMyEvent(); // Count: 1, cl1: 7, cl2: 5 clRun.MyEvent += cl2.Add2; clRun.OnMyEvent(); // Count: 2, cl1: 9, cl2: 7 clRun.MyEvent -= cl1.Add2; clRun.OnMyEvent(); // Count: 1, cl1: 9, cl2: 9 В качестве обработчика события может также использоваться анонимный ме- тод или лямбда-выражение. В среде .NET Framework обработчик события (делегат) имеет вид: delegate void <имя делегата>(object sender, EventArgs e); где sender – отправитель события, а e – дополнительная информация о событии. Модифицируем предыдущий пример таким образом, чтобы он был совместим с .NET Framework, объект класса события подсчитывал, сколько раз он активизиро- вал событие, и передавал это значение в качестве параметра. Переданное значение будет добавляться вместо двойки в методе Add2 delegate void MyEventHandler(object sender, EventArgs e); class MyEventArgs : EventArgs { public int num; } class RunEvent 99 { public int Count = 0; public int CountEvt = 0; // Количество активаций события у объекта private MyEventHandler ListHandler = null; public event MyEventHandler MyEvent { add { Count++; ListHandler += value; } remove { Count--; ListHandler -= value; } } public void OnMyEvent() { CountEvt++; if (ListHandler != null) { MyEventArgs e = new MyEventArgs(); e.num = CountEvt; ListHandler(this, e); } } } class MyClass { public int i = 5; public void Add2(object sender, EventArgs e) { i += (e as MyEventArgs).num; } } RunEvent clRun = new RunEvent(); MyClass cl1 = new MyClass(); MyClass cl2 = new MyClass(); clRun.OnMyEvent(); // Count: 0, CountEvt:1, cl1: 5, cl2: 5 clRun.MyEvent += cl1.Add2; clRun.OnMyEvent(); // Count: 1, CountEvt:2, cl1: 7, cl2: 5 clRun.MyEvent += cl2.Add2; clRun.OnMyEvent(); // Count: 2, CountEvt:3, cl1: 10, cl2: 8 clRun.MyEvent -= cl1.Add2; clRun.OnMyEvent(); // Count: 1, CountEvt:4, cl1: 10, cl2: 12 Также в среде .NET Framework имеется делегат для обработки событий EventHandler 1 , который позволяет не создавать собственных деле- гатов для обработки событий. Кроме того, если событию не требуются дополни- 1 Формат и смысл такой записи будет рассмотрен позже в разделе универсальных типов 100 тельные параметры, то оно может быть описано с использованием делегата EventHandler . С применением этих делегатов событие могло бы быть описано следующим образом: public event EventHandler 101 7 Универсальные типы 1 Как известно, многие алгоритмы очень похожи по своей логике независимо от типа данных, к которым они применяются. Например, механизм, поддерживающий очередь, остаётся одинаковым независимо от того, предназначена ли очередь для хранения элементов типа int , string , object или для класса, определяемого пользователем. До появления универсальных типов для обработки данных разных типов приходилось создавать различные варианты одного и того же алгоритма. А благодаря универсальным типам можно сначала выработать единое решение неза- висимо от конкретного типа данных, а затем применить его к обработке данных са- мых разных типов без каких-либо дополнительных усилий. При описании универсального типа указывается параметр типа (или пара- метры, если их несколько), который далее используется во всех членах данного типа (чаще всего называется T ). Фактически, параметр типа является местозаполните- лем для фактического типа, указываемого при создании экземпляра типа. 7.1 Общая схема Общая форма описания универсального типа имеет вид 2 : class <имя класса><<список параметров типа>> { <члены класса> } а форма объявления переменной такого типа и создания объекта: <имя класса><<список аргументов типа>> <идентификатор переменной>= new <имя класса><<список аргументов типа>> ([<список параметров конструктора>]); Пример: создать универсальный класс, имеющий одно поле и методы для до- ступа к нему. class MyClass { T Value; public T Get() { return Value; } public void Set(T NewValue) { Value = NewValue; } } MyClass MyClass 1 Также такие типы называют обобщениями, параметризованными типами 2 Универсальными могут быть не только классы, но и другие элементы языка, например, интерфейсы 102 sc.Set("Пример"); string s = sc.Get(); // s = "Пример" ic = sc; // Ошибка при компиляции i = sc.Get(); // Ошибка при компиляции Так как универсальные классы поддерживают контроль типов параметров, то это позволяет создавать более защищённые программы (по сравнению с программа- ми, использующими в качестве параметра тип object ). Универсальный класс может иметь более одного параметра типа. В этом слу- чае, при описании класса она перечисляются через запятую. Дополним приведённый выше класс методом, «обнуляющим» значение поля Value . При реализации такого метода возникает вопрос: какое значение присвоить в качестве «нулевого», ведь для разных типов это может быть 0 , null , "" и др. Для решения этой проблемы служит оператор default(<тип>) : class MyClass { T Value; public T Get() { return Value; } public void Set(T NewValue) { Value = NewValue; } public void Clear() { Value = default(T); } } 7.2 Ограничения по параметрам типа В простейшем случае, в качестве аргументов типа может выступать любой тип данных. Однако возможна ситуация, когда требуется ввести ограничения на список типов, которые могут быть использованы в качестве аргументов типа. Общая форма описания класса с ограничениями на параметры типа имеет вид: class <имя класса><<список параметров типа>> where <параметр 1> : <ограничение 1_1>[,<ограничение 1_2>...] [where <параметр 2> : <ограничение 2_1>[,<ограничение 2_2>...]...] { <члены класса> } На параметры могут быть наложения ограничения следующих типов: ограничение на базовый класс, требующее наличия определённого базового клас- са в аргументе типа. Это ограничение накладывается указанием имени требуемого базового класса; ограничение на интерфейс, требующее реализации одного или нескольких интер- фейсов аргументом типа. Это ограничение накладывается указанием имени тре- буемого интерфейса; ограничение на конструктор, требующее предоставить конструктор без парамет- ров в аргументе типа. Это ограничение накладывается с помощью оператора new() ; 103 ограничение ссылочного типа, требующее указывать аргумент ссылочного типа с помощью оператора class ; ограничение типа значения, требующее указывать аргумент типа значения с по- мощью оператора struct При наличии на один параметр нескольких ограничений первым должно быть указано ограничение class либо struct , если оно присутствует, или же ограниче- ние на базовый класс, если оно накладывается. Указывать ограничения class или struct одновременно с ограничением на базовый класс не разрешается. Далее по списку должно следовать ограничение на интерфейс, а последним по порядку – ограничение new() 7.2.1 Ограничение на базовый класс Ограничение на базовый класс позволяет указывать базовый класс, который должен наследоваться аргументом типа. Ограничение на базовый класс служит двум главным целям: во-первых, оно позволяет использовать в универсальном типе доступные члены того базового класса, на который указывает данное ограничение. В отсутствие ограничения на базовый класс компилятору ничего не известно о типе членов, ко- торые может иметь аргумент типа; во-вторых, ограничение на базовый класс гарантирует использование только тех аргументов типа, которые поддерживают указанный базовый класс. Это означает, что для любого ограничения, накладываемого на базовый класс, аргумент типа должен обозначать сам базовый класс или производный от него класс. Если же попытаться использовать аргумент типа, не соответствующий указанному базо- вому классу или не наследующий его, то в результате возникнет ошибка во время компиляции. Пример: создать универсальный класс, хранящий наследников класса Figure и позволяющий рассчитать их общую площадь. class MasFigures { T[] Mas = new T[10]; int Count = 0; public void Add(T Fig) { if (Count < 10) Mas[Count++] = Fig; } public T MaxFigure() { if (Count > 0) { T Max = Mas[0]; for (int i = 1; i < Count; i++) if (Max.size < Mas[i].size) 104 Max = Mas[i]; return Max; } else return null; } } MasFigures Square Fig = new Square(); Fig.size = 3; MasSquare.Add(Fig); Fig = new Square(); Fig.size = 5; MasSquare.Add(Fig); Fig = new Square(); Fig.size = 4; MasSquare.Add(Fig); Fig = MasSquare.MaxFigure(); // Вторая фигура, у которой size=5 Rectangle Fig2 = new Rectangle(); MasSquare.Add(Fig2); // Такой действие невозможно Если в классе MasFigures заменить ограничение на тип Figure , то найти самую большую фигуру через размер будет невозможно, т.к. у класса Figure нет размера. Однако, будет возможно реализовать поиск через площадь, и в этом случае добавление в массив прямоугольника будет вполне допустимым. 7.2.2 Ограничение на интерфейс Ограничение на интерфейс служит тем же целям, что и ограничение на базо- вый класс. Пример: создать универсальный класс, хранящий классы с реализацией ин- терфейса I2D и позволяющий рассчитать их общий периметр. class MasI2D { T[] Mas = new T[10]; int Count = 0; public void Add(T Fig) { if (Count < 10) Mas[Count++] = Fig; } public double SumPerimeter() { double Sum = 0; for (int i = 0; i < Count; i++) Sum += Mas[i].Perimeter(); return Sum; 105 } } Square A = new Square(); A.size = 5; Rectangle B = new Rectangle(); B.size1 = 4; B.size2 = 7; Cube C = new Cube(); C.size = 13; MasI2D Mas.Add(A); Mas.Add(B); Mas.Add(C); // Эта строка не допустима double SumPerimeter = Mas.SumPerimeter(); // SumPerimeter = 42 7.2.3 Ограничение на конструктор Ограничение на конструктор необходимо, чтобы в универсальном классе можно было создавать объекты класса «аргумент типа». Если такое ограничение не наложить, то не гарантируется, что в классе «аргумент типа» есть конструктор без параметров. Например: class MyClass1 { int A; public MyClass1() { A = 5; } public MyClass1(int NewA) { A = NewA; } } class MyClass2 { int A; public MyClass2(int NewA) { A = NewA; } } class MyClass3 { T B; public MyClass3() { // Без ограничения «new()» в строке заголовка класса // нижеприведенная строка будет выдавать ошибку при компиляции B = new T(); } } MyClass3 // Следующая строка выдаст ошибку компиляции, т.к. в классе // MyClass2 нет конструктора без параметров MyClass3 106 7.2.4 Ограничения ссылочного типа и типа значения Данные ограничения требуют, чтобы «аргумент типа» принадлежал заданной группе типов. Наложение ограничения ссылочного типа « class »: позволяет внутри класса работать с параметром типа, как с переменной, допуска- ющей значение null , например: class MyClass { T A; public MyClass() { // Без ограничения «class» в строке заголовка класса // нижеприведенная строка будет выдавать ошибку при компиляции A = null; } } блокирует использование в качестве аргумента типа типов значения, например: MyClass MyClass Наложение ограничения типа значения « struct » приводит прямо к противо- положным результатам, например: class MyClass { T A; public MyClass(T NewA) { A = null; // Теперь эта строка вызывает ошибку компиляции A = NewA; // А эта строка вполне допустима } } MyClass MyClass 7.2.5 Установление связи между двумя параметрами с помощью ограничения Ещё одной особенностью ограничений является возможность требования наличия связи «родитель-потомок» между параметрами типа. Например, если тре- буется, чтобы параметр типа V был таким же, или являлся наследником параметра типа T, то запись такого ограничения будет иметь вид: class MyClass 107 Например для двух классов A и B Class A { ... } Class B : A { ... } и указанного выше универсального типа MyClass , рассмотрим следующие описа- ния переменных и создания объектов: MyClass cl1 = new MyClass(); // Допустимо MyClass cl2 = new MyClass(); // Допустимо MyClass cl3 = new MyClass(); // Не допустимо MyClass cl3 = new MyClass(); // Допустимо 7.3 Параметры типы в методах Универсальность может применяться не только в универсальных типах, но и в отдельных методах, в т.ч. в обычных классов. В этом случае после имени метода указывается параметр типа (или параметры), который может быть использован в па- раметрах метода и в теле реализации метода. Пример: создать класс, метод которого позволяет заменить все вхождения ис- комого значения на заданное. class MyClass { public static void Replace { for (int i = 0; i < Mas.Length; i++) if (Mas[i].Equals(Old)) Mas[i] = New; } } int[] IntMas = { 1, 2, 1, 3, 4, 1 }; MyClass.Replace(IntMas, 1, 5); // IntMas = {5, 2, 5, 3, 4, 5} string[] StrMas = { "один", "два", "один" }; MyClass.Replace(StrMas, "один", "три"); // StrMas = {"три", "два", "три"} Как видно из примера, при вызове такого метода указывать фактический тип данных не требуется, компилятор его распознает автоматически. Если компилятор не может распознать тип данных, то формируется ошибка компиляции. Например, в следующей строке невозможно распознать тип , т.к. на основе массива он дол- жен быть int , а на основе параметра Old – double : MyClass.Replace(IntMas, 1.0, 5.0); // Ошибка компиляции При вызове метода возможно явное указание используемого типа. для чего после имени метода указывается требуемый тип, например: 108 MyClass.Replace MyClass.Replace На параметр типа, используемый в методе, также могут накладываться огра- ничения. Они имеют ту же конструкцию, что и ограничения на параметр типа уни- версального типа. Расположение ограничений осуществляется после закрывающей- ся круглой скобки метода. Например, если наложить ограничение struct на пара- метр из приведённого выше примера, то работа с массивом строк станет недопусти- мой: class MyClass { public static void Replace { for (int i = 0; i < Mas.Length; i++) if (Mas[i].Equals(Old)) Mas[i] = New; } } int[] IntMas = { 1, 2, 1, 3, 4, 1 }; MyClass.Replace(IntMas, 1, 5); // IntMas = {5, 2, 5, 3, 4, 5} string[] StrMas = { "один", "два", "один" }; MyClass.Replace(StrMas, "один", "три"); // Ошибка компиляции |