Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
-C#_Lec14_OOP-1.doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
315.39 Кб
Скачать

14.5. Статические классы и их члены

Статический член класса – это поле, свойство или метод, которое совместно используется всеми экземплярами этого класса.

Определение статического члена:

static class SampleClass

{ // Определение статического поля

public static string SampleString = "Sample String";

...

}

Чтобы получить доступ к статическому члену, можно использовать имя класса без создания объекта этого класса:

Console.WriteLine(SampleClass.SampleString);

Статические (совместно используемые) классы в C# и модули в Visual Basic могут содержать только статические члены. Нельзя создать экземпляр статического класса. Статические члены также не могут обращаться к нестатическим полям, свойствам или методам.

Если таким членом является поле или свойство, оно имеет одинаковое значение для всех экземпляров этого класса. Чтобы убедиться в этом, рассмотрим простой пример [2 – Фаронов].

Пример 14.2. Иллюстрация статических членов класса. (Проверено)

using System;

class StaticDemo

{ public class A // Объявление класса

{ private static int N = 0; // Объявление статического поля

public A() {N++;} // Объявление конструктора

public int Get_N() {return N;} // Объявление метода

}

public static void Main()

{ A a1 = new A(), a2 = new A(); // Создание объектов

Console.WriteLine("a1.N = {0}, a2.N = {1}",

a1.Get_N(), a2.Get_N()); // Выполнение метода

Console.ReadLine();

}

}

// Вывод: a1.N = 2, a2.N = 2

В этой программе перекрытый конструктор A при каждом срабатывании увеличивает на 1 значение закрытого поля N.

Метод Get_N() не является статическим. Однако и для объекта a1, и для а2 он возвращает одно и то же значение 2. Происходит это потому, что метод обращается к одному и тому же статическому полю. В этом отношении было бы правильнее объявить метод с модификатором static – такие методы нельзя вызывать как члены объекта, их следует вызывать как члены класса:

Console.WriteLine("a1.N = {0}, a2.N - {1}",

A.Get_N(), A.Get_N());

В этом варианте оператора явно указывается, что речь идет об одном и том же статическом поле.

Делегаты обеспечивают встроенный, поддерживаемый языком механизм определения и выполнения обратных вызовов. Их гибкость позволяет определять точную сигнатуру обратного вызова, и эта информация становится частью самого типа делегата. Анонимные функции – форма делегатов, позволяющая сократить некоторую часть синтаксиса делегатов, который во многих случаях бывает громоздким и утомительным. На делегатах построена поддержка механизма событий в С# и платформе .NET. События представляют собой унифицированный шаблон привязки реализаций обратных вызовов – даже по нескольку экземпляров сразу – к коду, инициирующему обратный вызов.

Обзор делегатов

CLR предоставляет исполняющую систему, которая явно поддерживает гибкий механизм обратных вызовов. С самого начала времен – по крайней мере, со времен появления Windows – всегда существовала необходимость в функциях обратного вызова, которые система либо какая-то другая сущность вызовет в определенный момент времени, чтобы известить о чем-нибудь интересном. В конце концов, обратные вызовы являются удобным механизмом, посредством которого пользователи могут расширить функциональность компонента. Даже наиболее базовые компоненты приложения Win32 GUI – оконные процедуры – представляют собой функции обратного вызова, регистрируемые в системе. Система вызывает такую функцию всякий раз, когда нужно известить о поступлении некоторого события в окно. Этот механизм работает вполне хорошо в программной среде на базе языка С.

Все стало несколько сложнее с распространением объектно-ориентированных языков вроде С++. Разработчики немедленно захотели, чтобы система могла вызывать методы экземпляров на объектах, а не только глобальные функции или статические методы. Существует много решений этой проблемы. Но независимо от того, какое из них используется, в итоге все сводится к тому, что где-то кто-то должен хранить указатель на экземпляр объекта и вызывать метод экземпляра через этот указатель на него. Реализации обычно состоят из переходника (thunk), который представляет собой не что иное адаптер, такой как промежуточный блок данных или кода, вызывающий метод экземпляра через указатель на него. Переходник – в действительности функция, зарегистрированная в системе. За многие годы в С++ было разработано немало изобретательных реализаций переходников. Ваш доверчивый автор с сентиментальной нежностью может вспомнить множество примеров такого дизайна.

В настоящее время делегаты являются предпочтительным способом реализации обратных вызовов в CLR. Делегат имеет смысл представлять как хорошо известный указатель на функцию, в качестве которой может выступать статический метод или метод экземпляра. Экземпляр делегата – это в точности то же самое, что переходник, но в то же время он является полноправным гражданином в стране CLR. Фактически, при объявлении делегата в своем коде компилятор С# генерирует класс-наследник MulticastDelegate, и CLR реализует все интересные методы делегата динамически, во время выполнения. Вот почему вы не увидите никакого IL-кода, стоящего за методами делегата, если заглянете в скомпилированный модуль с помощью ILDASM.

Делегат содержит два полезных поля. Первое хранит ссылку на объект, а второе – указатель на метод. При вызове делегата вызывается метод экземпляра на содержащейся в нем объектной ссылке. Однако если ссылка на объект равна null, то исполняющая система понимает это так, что метод является статическим. Более того, вызов делегата – синтаксически то же самое, что вызов обычной функции. Поэтому делегаты – блестящее средство реализации обратных вызовов.

Как видите, делегаты представляет собой отличный механизм отделения вызываемого метода от вызывающего кода. Фактически вызывающий делегат код не имеет понятия, да и не нуждается в знании того, вызывается метод экземпляра или статический метод, либо на каком именно экземпляре производится вызов. Для вызывающего кода это просто вызов произвольного кода. Он может получить экземпляр делегата любым подходящим способом, при этом полностью абстрагируясь от сущности, которую он на самом деле вызывает.

Задумайтесь на минутку об элементах пользовательского интерфейса в диалоговом окне, таких как кнопка отправки, и о том, насколько много внешних сторон могут быть заинтересованы в знании факта выбора этой кнопки. Если класс, представляющий кнопку, должен напрямую вызывать эти заинтересованные стороны, ему нужно обладать подробными знаниями о компоновке этих заинтересованных сторон, или объектов, и знать, какие именно их методы должны быть вызваны. Ясно, что такое требование приводит к чрезмерной зависимости между кнопкой и заинтересованными сторонами, причем зависимость эта чрезвычайно сложна и превращает сопровождение кода в кошмар. Здесь на помощь приходят делегаты и разрывают эту связь. Теперь заинтересованные стороны должны только зарегистрировать делегат с кнопкой, и этот делегат предварительно сконфигурирован так, что может вызывать любые методы, которые им нужны. Этот механизм отделения описывает события, как поддерживаемые CLR. Дополнительные сведения о событиях CLR можно найти в разделе "События" далее в главе. А пока давайте посмотрим, как создаются и используются делегаты в С#.

Создание и использование делегатов

Объявления делегатов выглядят почти так же, как объявления абстрактных методов, за исключением того, что снабжаются одним дополнительным ключевым словом – delegate. Вот как выглядит допустимое объявление делегата:

public delegate double ProcessResults(double x, double у);

Когда компилятор С# встречает эту строку, он определяет тип-наследник MulticastDelegate, который также реализует метод по имени Invoke, имеющий в точности ту же сигнатуру, что и метод, описанный в объявлении делегата. Для практических нужд этот класс выглядит следующим образом:

public class ProcessResults : System.MulticastDelegate

{ public double Invoke(double x, double у);

// Остальное опущено для ясности

}

Несмотря на то, что компилятор создает тип, подобный приведенному, он также абстрагирует использование делегатов за синтаксическими сокращениями. Обычно для вызова делегата применяется синтаксис, подобный вызову функции, а не прямое обращение к методу Invoke. Все это будет показано ниже.

При создании экземпляр делегата потребуется связать с вызовом метода. Привязываемый метод может быть как статическим, так и методом экземпляра, обладающим сигнатурой, которая совместима с делегатом. Таким образом, типы параметров и тип возврата должны либо совпадать с объявлением делегата, либо быть неявно преобразуемыми в типы, указанные в объявлении делегата.

Одиночный делегат

В следующем примере показан базовый синтаксис создания делегата.

using System;

public delegate double ProcessResults(double x, double у);

public class Processor

{ public Processor(double factor)

{ this.factor = factor;

}

public double Compute(double x, double у)

{ double result = (x + y) * factor;

Console.WriteLine("InstanceResults: {0}", result);

return result;

}

public static double StaticCompute(double x, double у)

{ double result = (x + y) * 0.5;

Console.WriteLine("StaticResult: {0}", result);

return result;

}

private double factor;

}

public class EntryPoint

{ static void Main()

{ Processor proc1 = new Processor(0.75);

Processor proc2 = new Processor(0.83);

ProcessResults delegate1 = new ProcessResults(proc1.Compute);

ProcessResults delegate2 = new ProcessResults(proc2.Compute);

ProcessResults delegate3 = Processor.StaticCompute;

double combined = delegate1(4, 5) + delegate2(6,2) +

delegate3(5, 2);

Console.WriteLine("Вывод: {0}", combined);

}

}

В приведенном выше примере создаются три делегата. Два из них указывают на методы экземпляра, а один – на статический метод. Обратите внимание, что делегаты создаются посредством создания экземпляров типа ProcessResults, который является типом, созданным объявлением делегата и передачей целевого метода в списке аргументов конструктора. Однако экземпляр delegate3 использует сокращенный синтаксис, когда экземпляру делегата просто присваивается метод. Хотя выглядит так, будто Processor.StaticCompute является именем метода, на самом деле это группа методов, потому что метод может быть перегружен, и это имя может ссылаться на несколько методов. В данном случае группа методов Processor.StaticCompute состоит из одного метода. И чтобы облегчить жизнь, С# позволяет непосредственно присваивать делегату группу методов. При создании экземпляров делегатов с помощью new группа методов передается в конструкторе. Обратите внимание на формат групп методов. В первых двух случаях передается метод экземпляра на экземплярах proc1 и ргос2. Однако в третьем случае передается группа методов типа, а не экземпляра. Подобным образом создается делегат, указывающий на статический метод вместо метода экземпляра. Делегату delegate3 можно было бы также присвоить экземпляр группы методов. В точке вызова делегата синтаксис идентичен и не зависит от того, указывает делегат на метод экземпляра или на статический метод. Конечно, этот пример довольно надуманный, но он дает представление об основах применения делегатов в С#.

Во всех случаях предыдущего кода при вызове делегата происходит единственное действие. Но делегаты можно связывать в цепочки, выполняющие по нескольку действий сразу, и этому посвящен следующий раздел.

Цепочки делегатов

Цепочка делегатов позволяет создавать связный список делегатов, так что когда вызывается делегат, находящийся в начале списка, вслед за ним выполняются все делегаты цепочки. Класс System.Delegate предоставляет несколько статических методов для управления списком делегатов. При создании списков делегатов применяются следующие методы, объявленные в типе System.Delegate:

public class Delegate : ICloneable, ISerializable

{ public static Delegate Combine(Delegate []);

public static Delegate Combine(Delegate first, Delegate second);

}

Обратите внимание, что методы Combine принимают делегаты для связи в цепочку и возвращают другой Delegate. Возвращенный Delegate представляет собой новый экземпляр MulticastDelegate, унаследованный от Delegate, потому что экземпляры Delegate являются неизменяемыми.

Как видите, первая версия Combine, приведенная выше, принимает массив делегатов, чтобы сформировать непрерывную цепочку делегатов, а вторая форма принимает

. . . (с.287)

5. Делегаты (MSDN_ООП_VB+C#_2010)

Делегат – это тип, который определяет сигнатуру метода и может обеспечивать связь с любым методом с совместимой сигнатурой. Метод можно запустить (или вызвать) с помощью делегата. Делегаты используются для передачи методов в качестве аргументов к другим методам.

Примечание. Обработчики событий – это ничто иное, как методы, вызываемые с помощью делегатов. Дополнительные сведения об использовании делегатов при обработке событий см. в разделе События и делегаты.

Создание делегата:

public delegate void SampleDelegate(string str);

Создание ссылки на метод, сигнатура которого соответствует сигнатуре, указанной делегатом:

class SampleClass

{ // Метод, который соответствует сигнатуре SampleDelegate.

public static void sampleMethod(string message)

{ // Код метода.

}

// Метод, который иллюстрирует делегат.

void SampleDelegate()

{ SampleDelegate sd = sampleMethod;

sd("Простая строка");

}

}

События. (MSDN_ООП_VB+C#_2010)

События позволяют классу или объекту уведомлять другие классы или объекты о возникновении каких-либо ситуаций. Класс, отправляющий (или порождающий) событие, называется издателем, а классы, принимающие (или обрабатывающие) событие, называются подписчиками.

События

Во многих случаях использования делегатов в качестве механизма обратного вызова может понадобиться просто известить кого-то о наступлении некоторого события вроде щелчка на кнопке в пользовательском интерфейсе. Предположим, что проектируется приложение медиа-проигрывателя. Где-то в пользовательском интерфейсе имеется кнопка "Play" (Воспроизведение). В хорошо спроектированной системе пользовательский интерфейс отделен от логики управления с помощью четко определенной абстракции, обычно реализуемой через шаблон Bridge (Мост). Абстракция облегчает последующее изменение пользовательского интерфейса или, что еще лучше, поскольку этот интерфейс зависит от платформы – облегчает перенос приложения на другую платформу. Например, шаблон Bridge хорошо работает в ситуациях, когда требуется отделить логику управления от пользовательского интерфейса.

На заметку! Смысл шаблона Bridge, согласно книге Эриха Гаммы (Erich Gamma), Ричарда Хелма (Richard Helm), Ральфа Джонсона (Ralph Johnson) и Джона Влиссидеса (John Vlissides) Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Professional,1995), заключается в отделении абстракции от реализации, чтобы то и другое можно было менять независимо.

Используя шаблон Bridge, можно облегчить сценарий, при котором изменения, происходящие в ядре системы, не влекут за собой изменений в пользовательском интерфейсе и, что более важно, при котором изменения в пользовательском интерфейсе не требуют изменений в ядре системы. Один распространенный способ реализации этого шаблона предусматривает создание в ядре системы четко определенных интерфейсов, через которые пользовательский интерфейс взаимодействует с системой, и наоборот. Однако в таких ситуациях определение интерфейсных типов является громоздким и далеким от идеала. С другой стороны, делегаты представляют собой блестящий механизм для использования в сценариях подобного рода. С помощью делегата можно сформулировать абстрактные вещи следующим образом: "Когда пользователь пожелает включить воспроизведение, необходимо, чтобы были вызваны зарегистрированные методы, принимающие всю необходимую информацию для выполнения данного действия". Прелесть заключается в том, что ядро системы не волнует, как именно пользователь укажет пользовательскому интерфейсу о том, что он желает включить воспроизведение. Это может быть щелчок на кнопке или срабатывание какого-то устройства, улавливающего мозговые волны и угадывающего мысли пользователя. Для системы это не имеет значения, и в любой момент можно изменить любой компонент взаимодействия, не затрагивая остальное. Обе стороны выполняют требования согласованного контракта, который в данном случае включает специально сформированный делегат и средства его регистрации в генерирующей события сущности.

Этот шаблон использования, также известный под названием "издатель/подписчик" (publish/subscribe), настолько распространен, даже за пределами мира разработки пользовательских интерфейсов, что проектировщики исполняющей системы .NET позаботились об определении формализованного встроенного механизма событий. При объявлении события внутри класса компилятор реализует скрытые методы, позволяющие регистрировать и отменять регистрацию делегатов, которые вызываются при наступлении определенных событий. По сути, событие – это сокращение, позволяющее сэкономить время, которое понадобилось бы для написания методов регистрации и отмены регистрации, управляющих цепочкой делегатов. Давайте взглянем на простой пример события, основанный на вышесказанном.

using System;

// Аргументы, переданные от пользовательского интерфейса

// при возникновении события включения воспроизведения.

public class PlayEventArgs : EventArgs

{ public PlayEventArgs(string filename)

{ this.filename = filename;

}

private string filename;

public string Filename

{ get { return filename; }

}

}

public class PlayerUI

{ // Определить событие для уведомления о воспроизведении.

public event EventHandler<PlayEventArgs> PlayEvent;

public void UserPressedPlay()

{ OnPlay();

}

protected virtual void OnPlay()

{ // Инициировать событие.

EventHandler<PlayEventArgs> localHandler = PlayEvent;

if (localHandler != null)

{ localHandler(this, new PlayEventArgs("somefile.wav"));

}

}

}

public class CorePlayer

{ public CorePlayer()

{ ui = new PlayerUI();

// Регистрация обработчика события.

ui.PlayEvent += this.PlaySomething;

}

private void PlaySomething(object source, PlayEventArgs args)

{ // Воспроизведение файла.

}

private PlayerUI ui;

}

public class EntryPoint

{ static void Main()

{ CorePlayer player = new CorePlayer();

}

}

Несмотря на то, что синтаксис этого простого события может показаться усложненным, общая идея состоит в создании четко определенного контракта, через который все заинтересованные стороны уведомляются о том, что пользователь желает воспроизвести файл. Этот контракт инкапсулирован внутри класса PlayEventArgs, который унаследован от System.EventArgs (как описано ниже). События накладывают определенные правила на использование делегатов. Делегат должен что-нибудь возвращать и должен принимать два аргумента, как показано в методе PlaySomething из предыдущего примера. Первый аргумент – это ссылка на объект, представляющий сторону, которая генерирует сообщение, а второй аргумент – тип, унаследованный от System.EventArgs. В этом производном классе определяются все специфичные для события аргументы.

На заметку! В .NET 1.1 приходилось явно определять тип делегата, стоящего за событием. Начиная с .NET 2.0, можно использовать новый обобщенный делегат EventHandler<T>, защищающий от этой рутинной работы.

Обратите внимание на способ определения события внутри класса PlayerUI с применением ключевого слова event. За этим ключевым словом сначала следует определенный делегат события, а за ним – имя события, в данном случае PlayEvent. Также отметьте, что член-событие объявлено с использованием обобщенного делегата EventHandler<T>.

При регистрации обработчиков с применением операции += в качестве сокращения, можно предоставлять только имя метода для вызова, а компилятор создаст экземпляр EventHandler<T>, используя группу методов для делегирования правил присваивания, которые упоминались в предыдущем разделе. После операции += можно дополнительно указать выражение, создающее новый экземпляр EventHandler<T>, как это делалось при создании экземпляров делегатов, но если компилятор предлагает показанное сокращение, то зачем применять громоздкий синтаксис, затрудняющий чтение кода?

Идентификатор PlayEvent означает две совершенно разные вещи, в зависимости от того, с какой точки зрения его рассматривать. С точки зрения генератора события – в данном случае, PlayerUI – событие PlayEvent используется в точности как делегат. Такое его применение можно видеть внутри метода OnPlay. Обычно метод, названный OnPlay, вызывается в ответ на щелчок на кнопке пользовательского интерфейса. Он уведомляет всех зарегистрированных слушателей о вызове через событие (делегат) PlayEvent.

На заметку! При генерации событий существует популярный подход – инициировать их внутри метода protected virtual по имени On<событие>, где <событие> заменяется именем события, в данном случае – OnPlay. Подобным образом производные классы могут легко модифицировать действия, предпринимаемые, когда должно быть инициировано событие. В С# необходимо проверить событие на равенство null, прежде чем вызывать его, иначе будет сгенерировано исключение NullReferenceException. Перед проверкой на null метод OnPlay создает локальную копию события. Это позволяет избежать условия состязаний, когда событие устанавливается в null из другого потока после выполнения проверки на null и перед генерацией события.

Как видно в конструкторе CorePlayer, со стороны потребителя события идентификатор PlayEvent используется совершенно иначе.

Такова базовая структура событий. Как упоминалось ранее, события .NET – это сокращения для создания делегатов и контрактов, с которыми нужно регистрировать эти делегаты. В доказательство этого можно просмотреть код IL, полученный в результате компиляции предыдущего примера. "За кулисами" компилятор генерирует два метода addOnPlay и removeOnPlay, которые вызываются, когда используются перегруженные операции += и -=. Эти методы управляют добавлением и удалением делегатов в цепочке делегатов событий. В действительности компилятор С# не позволяет вызывать эти методы явно, так что должны использоваться перегруженные операции. Может возникнуть вопрос, а есть ли какой-нибудь способ контролировать тело этих функций-членов, как это делается со свойствами? Ответ – да, и используемый для этого синтаксис подобен синтаксису свойств.

Ниже показан измененный код класса PlayerUI, в котором демонстрируется явный способ обработки добавления и удаления операций событий.

public class PlayerUI

{ // Определить событие для уведомления о воспроизведении.

private EventHandler<PlayEventArgs> playEvent;

public event EventHandler<PlayEventArgs> PlayEvent

{ add

{ playEvent = (EventHandler<PlayEventArgs>)

Delegate.Combine(playEvent, value);

}

Remove

{ playEvent = (EventHandler<PlayEventArgs>)

Delegate.Remove( playEvent, value );

}

}

public void UserPressedPlay()

{ OnPlay();

}

protected virtual void OnPlay()

{ // Инициировать событие.

EventHandler<PlayEventArgs> localHandler = playEvent;

if (localHandler != null)

{ localHandler(this, new PlayEventArgs("somefile.wav"));

}

}

}

Внутри разделов add и remove объявления события ссылка на добавляемый или удаляемый делегат осуществляется по ключевому слову value, что идентично тому, как работает метод set свойства. Данный пример использует Delegate.Combine и Delegate.Remove для управления внутренней цепочкой делегатов по имени playEvent. Пример выглядит несколько надуманным, поскольку механизм событий по умолчанию делает, по сути, то же самое, однако он предназначен только для целей демонстрации.

На заметку! Явное определение пользовательских средств доступа к событиям может потребоваться для построения специализированного механизма хранения событий или для выполнения специальной обработки при регистрации и отмене регистрации событий.

А теперь заключительный комментарий относительно шаблонов проектирования. На основе сказанного можно сделать вывод, что события идеальны для реализации шаблона проектирования "издатель/подписчик", когда множество слушателей регистрируются для получения уведомления о событии (о его публикации). Точно так же события .NET удобно использовать для реализации формы шаблона Observer (Обозреватель), когда некоторые сущности регистрируются для получения уведомлений об изменении другой сущности. Это лишь два шаблона проектирования, реализацию которых облегчают события.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]