Тверской государственный университет
Опубликован: 22.11.2005 | Доступ: свободный | Студентов: 30256 / 1759 | Оценка: 4.31 / 3.69 | Длительность: 28:26:00
ISBN: 978-5-9556-0050-5
Лекция 20:

Функциональный тип в C#. Делегаты

< Лекция 19 || Лекция 20: 12345 || Лекция 21 >
Аннотация: Новое слово для старого понятия. Функциональный тип. Функции высших порядков. Вычисление интеграла и сортировка. Два способа взаимодействия частей при построении сложных систем. Функции обратного вызова. Наследование и функциональные типы. Сравнение двух подходов. Класс Delegate. Методы и свойства класса. Операции над делегатами. Комбинирование делегатов. Список вызовов.

Как определяется функциональный тип и как появляются его экземпляры

Слово делегат (delegate) используется в C# для обозначения хорошо известного понятия. Делегат задает определение функционального типа (класса) данных. Экземплярами класса являются функции. Описание делегата в языке C# представляет собой описание еще одного частного случая класса. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция (метод), сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом. Синтаксис объявления делегата имеет следующий вид:

[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);

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

Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:

  • непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
  • внутри другого класса, наряду с объявлениями методов и свойств. Такое объявление рассматривается как объявление вложенного класса.

Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, должны объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Если контракт выполняется, то можно создать экземпляры делегата, присвоив им в качестве значений функции, удовлетворяющие контракту. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у экземпляра соответствующий параметр имеет тип, согласованный с object, например, int.

Начнем примеры этой лекции с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс:

namespace Delegates
{
//объявление классов - делегатов
delegate void Proc(ref int x);
delegate void MesToPers(string s);
class OwnDel
{
	public delegate int Fun1(int x);
	int Plus1( int x){return(x+100);}//Plus1
	int Minus1(int x){return(x-100);}//Minus1
	void Plus(ref int x){x+= 100;}
	void Minus(ref int x){x-=100;}
	//поля класса
	public Proc p1;
	public Fun1 f1;
	char sign;
	//конструктор
	public OwnDel(char sign)
	{
		this.sign = sign;
		if (sign == '+')
		{p1 = new Proc(Plus);f1 = new Fun1(Plus1);}
		else
		{p1 = new Proc(Minus);f1 = new Fun1(Minus1);}
	}
}//class OwnDel

Прокомментирую этот текст.

  • Первым делом объявлены три функциональных класса - три делегата: Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.
  • В классе OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1, сигнатуры которых соответствуют сигнатурам, задаваемых классами Proc и Fun1.
  • Поля p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1.
  • В конструкторе класса поля p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.

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

Приведу теперь процедуру, тестирующую работу созданного класса:

public void TestOwnDel()
{			
	int account = 1000, account1=0;
	OwnDel oda = new OwnDel('+');
	Console.WriteLine("account = {0}, account1 = {1}",
		account, account1);
	oda.p1(ref account); account1=oda.f1(account);
	Console.WriteLine("account = {0}, account1 = {1}",
		account, account1);
}

Клиент класса OwnDel создает экземпляр класса, передавая конструктору знак той операции, которую он хотел бы выполнить над своими счетами - account и account1. Вызов p1 и f1, связанных к моменту вызова с закрытыми методами класса, приводит к выполнению нужных функций.

В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим многократно встречавшийся класс Person, слегка изменив его определение:

class Person
{
	//конструкторы
	public Person(){name =""; id=0; salary=0.0;}
	public Person(string name){this.name = name;}
	public Person (string name, int id, double salary)
	{this.name = name; this.id=id; this.salary = salary;}
	public Person (Person pers)
	{this.name = pers.name; this.id = pers.id;
			this.salary = pers.salary;}
	//методы
	public void ToPerson(string mes)
	{
		this.message = mes;
		Console.WriteLine("{0}, {1}",name, message);
	}
	//свойства
	private string name;
	private int id;
	private double salary;
	private string message;
	//доступ к свойствам
	public string Name
	{get {return(name);}	set {name = value;}}
	public double Salary
	{get {return(salary);}	set {salary = value;}}
	public int Id
	{get {return(id);}		set {id = value;}}
}//class Person

Класс Person устроен обычным способом: у него несколько перегруженных конструкторов, закрытые поля и процедуры-свойства для доступа к ним. Особо обратить внимание прошу на метод класса ToPerson, сигнатура которого совпадает с сигнатурой класса, определенной введенным ранее делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:

Person man1 = new Person("Владимир");
			MesToPers mestopers = new MesToPers(man1.ToPerson);
			mestopers("пора работать!");

Обратите внимание, что поскольку метод ToPerson не является статическим методом, то при связывании необходимо передать и объект, вызывающий метод. Более того, переданный объект становится доступным экземпляру делегата. Отсюда сразу же становится ясным, что экземпляры делегата - это не просто указатели на функцию, а более сложно организованные структуры. Они, по крайней мере, содержат пару указателей на метод и на объект, вызвавший метод. Вызываемый метод в своей работе использует как информацию, передаваемую ему через аргументы метода, так и информацию, хранящуюся в полях объекта. В данном примере переданное сообщение "пора работать" присоединится к имени объекта, и результирующая строка будет выдана на печать. В тех случаях, когда метод, связываемый с экземпляром делегата, не использует информацию объекта, этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими, так и динамическими методами, связанными с конкретными объектами.

Последние три строки были добавлены в вышеприведенную тестирующую процедуру. Взгляните на результаты ее работы.

Объявление делегатов и создание их экземпляров

Рис. 20.1. Объявление делегатов и создание их экземпляров
< Лекция 19 || Лекция 20: 12345 || Лекция 21 >
Александр Галабудник
Александр Галабудник

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

Александра Гусева
Александра Гусева