C#委托与事件


一、委托(Delegate)基础

1. 委托的概念

委托是C#中的一种类型,它定义了方法的签名,可以持有对方法的引用。委托本质上是类型安全的函数指针,允许将方法作为参数传递或存储。

// 声明委托类型
public delegate void MessageHandler(string message);

// 兼容方法
public void DisplayMessage(string msg)
{
    Console.WriteLine($"消息:{msg}");
}

public void LogMessage(string log)
{
    File.AppendAllText("log.txt", $"{DateTime.Now}: {log}\n");
}

// 使用委托
MessageHandler handler = DisplayMessage;
handler += LogMessage;  // 多播委托

handler("系统启动");  // 同时调用两个方法

2. 委托的特点

  • 类型安全:编译时检查方法签名匹配
  • 多播能力:一个委托实例可以引用多个方法
  • 异步支持:可用于异步方法调用
  • 灵活性:能够动态改变引用的方法

二、内置委托类型

.NET框架提供了几种常用的通用委托类型:

1. Action委托

表示无返回值的方法(支持0-16个参数):

Action<string, int> action = (s, i) => 
{
    Console.WriteLine($"{s} {i}");
};
action("计数", 5);

2. Func委托

表示有返回值的方法(最后一个类型参数为返回值类型):

Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 4);  // 返回7

3. Predicate委托

表示返回bool的方法(通常用于条件判断):

Predicate<int> isEven = num => num % 2 == 0;
bool even = isEven(4);  // 返回true

三、事件(Event)机制

1. 事件的基本概念

事件是基于委托的特殊机制,提供了一种发布-订阅模型,允许对象通知其他对象发生特定情况。

// 发布者类
public class TemperatureMonitor
{
    // 1. 定义委托类型
    public delegate void TemperatureChangedHandler(decimal newTemp);

    // 2. 声明事件
    public event TemperatureChangedHandler TemperatureChanged;

    private decimal _currentTemp;

    public decimal CurrentTemperature
    {
        get => _currentTemp;
        set
        {
            if (_currentTemp != value)
            {
                _currentTemp = value;
                // 3. 触发事件
                OnTemperatureChanged(_currentTemp);
            }
        }
    }

    protected virtual void OnTemperatureChanged(decimal newTemp)
    {
        TemperatureChanged?.Invoke(newTemp);
    }
}

// 订阅者类
public class TemperatureDisplay
{
    public void Subscribe(TemperatureMonitor monitor)
    {
        monitor.TemperatureChanged += ShowTemperature;
    }

    private void ShowTemperature(decimal temp)
    {
        Console.WriteLine($"当前温度:{temp}°C");
    }
}

// 使用
var monitor = new TemperatureMonitor();
var display = new TemperatureDisplay();
display.Subscribe(monitor);

monitor.CurrentTemperature = 23.5m;  // 触发事件

2. 事件与委托的区别

特性委托事件
调用权限类外部可调用仅声明类内部可触发
订阅控制可赋值(=)只能使用+=和-=操作
设计目的通用方法引用实现观察者模式
封装性

四、标准事件模式

.NET框架定义的标准事件模式:

// 事件参数类
public class ProcessCompletedEventArgs : EventArgs
{
    public bool Success { get; }
    public DateTime CompletionTime { get; }

    public ProcessCompletedEventArgs(bool success, DateTime completionTime)
    {
        Success = success;
        CompletionTime = completionTime;
    }
}

// 发布者
public class Processor
{
    // 使用EventHandler<T>定义事件
    public event EventHandler<ProcessCompletedEventArgs> ProcessCompleted;

    public void StartProcess()
    {
        try
        {
            // 模拟处理过程
            Thread.Sleep(2000);

            // 触发事件
            OnProcessCompleted(true, DateTime.Now);
        }
        catch
        {
            OnProcessCompleted(false, DateTime.Now);
        }
    }

    protected virtual void OnProcessCompleted(bool success, DateTime time)
    {
        ProcessCompleted?.Invoke(this, new ProcessCompletedEventArgs(success, time));
    }
}

// 订阅者
public class Logger
{
    public void Subscribe(Processor processor)
    {
        processor.ProcessCompleted += LogCompletion;
    }

    private void LogCompletion(object sender, ProcessCompletedEventArgs e)
    {
        string status = e.Success ? "成功" : "失败";
        Console.WriteLine($"处理{status},完成时间:{e.CompletionTime}");
    }
}

五、匿名方法与Lambda表达式

1. 匿名方法

Button.Click += delegate(object sender, EventArgs e)
{
    Console.WriteLine("按钮被点击");
};

2. Lambda表达式

Func<int, int> square = x => x * x;
Action<string> greet = name => 
{
    string message = $"你好,{name}";
    Console.WriteLine(message);
};

六、委托与事件的高级应用

1. 多播委托的异常处理

public delegate void WorkHandler();

public class Worker
{
    public event WorkHandler WorkDone;

    public void DoWork()
    {
        try
        {
            // 模拟工作
            Console.WriteLine("工作中...");

            // 安全调用多播委托
            Delegate[] handlers = WorkDone?.GetInvocationList();
            if (handlers != null)
            {
                foreach (WorkHandler handler in handlers)
                {
                    try
                    {
                        handler();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"处理器异常:{ex.Message}");
                    }
                }
            }
        }
        finally
        {
            Console.WriteLine("工作完成");
        }
    }
}

2. 弱事件模式

解决事件导致的内存泄漏问题:

using System.Windows;  // 需要引用WindowsBase.dll

public class WeakEventSample
{
    public class EventSource
    {
        public event EventHandler Event;

        public void RaiseEvent()
        {
            Event?.Invoke(this, EventArgs.Empty);
        }
    }

    public class EventListener
    {
        public void Subscribe(EventSource source)
        {
            WeakEventManager<EventSource, EventArgs>.AddHandler(
                source, 
                "Event", 
                OnEvent);
        }

        private void OnEvent(object sender, EventArgs e)
        {
            Console.WriteLine("收到事件(弱引用)");
        }
    }
}

七、综合示例:股票价格监控系统

using System;
using System.Timers;

class Program
{
    static void Main()
    {
        // 创建股票监控器
        var stockMonitor = new StockMonitor("AAPL");

        // 创建显示服务
        var display = new StockDisplay();
        var logger = new StockLogger();

        // 订阅事件
        stockMonitor.PriceChanged += display.ShowPrice;
        stockMonitor.PriceChanged += logger.LogPrice;
        stockMonitor.PriceThresholdReached += display.NotifyThreshold;

        // 启动监控
        stockMonitor.StartMonitoring();

        Console.ReadLine();  // 保持程序运行
    }
}

// 股票价格事件参数
public class StockPriceEventArgs : EventArgs
{
    public string Symbol { get; }
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public DateTime Time { get; }

    public StockPriceEventArgs(string symbol, decimal oldPrice, decimal newPrice)
    {
        Symbol = symbol;
        OldPrice = oldPrice;
        NewPrice = newPrice;
        Time = DateTime.Now;
    }
}

// 股票监控器
public class StockMonitor
{
    private readonly string _symbol;
    private decimal _currentPrice;
    private readonly Timer _timer;
    private readonly Random _random = new Random();

    public event EventHandler<StockPriceEventArgs> PriceChanged;
    public event EventHandler<StockPriceEventArgs> PriceThresholdReached;

    public StockMonitor(string symbol)
    {
        _symbol = symbol;
        _currentPrice = 150.00m;  // 初始价格

        _timer = new Timer(1000);  // 1秒间隔
        _timer.Elapsed += CheckPrice;
    }

    public void StartMonitoring()
    {
        _timer.Start();
    }

    private void CheckPrice(object sender, ElapsedEventArgs e)
    {
        // 模拟价格变化
        decimal change = (_random.NextDouble() > 0.5 ? 1 : -1) * 
                         (decimal)_random.NextDouble() * 2m;
        decimal newPrice = _currentPrice + change;

        // 触发价格变化事件
        OnPriceChanged(newPrice);

        // 检查价格阈值(±5%)
        if (Math.Abs(newPrice - 150m) / 150m >= 0.05m)
        {
            OnPriceThresholdReached(newPrice);
        }

        _currentPrice = newPrice;
    }

    protected virtual void OnPriceChanged(decimal newPrice)
    {
        var args = new StockPriceEventArgs(_symbol, _currentPrice, newPrice);
        PriceChanged?.Invoke(this, args);
    }

    protected virtual void OnPriceThresholdReached(decimal newPrice)
    {
        var args = new StockPriceEventArgs(_symbol, _currentPrice, newPrice);
        PriceThresholdReached?.Invoke(this, args);
    }
}

// 股票显示服务
public class StockDisplay
{
    public void ShowPrice(object sender, StockPriceEventArgs e)
    {
        Console.ForegroundColor = e.NewPrice > e.OldPrice ? ConsoleColor.Green : 
                                 e.NewPrice < e.OldPrice ? ConsoleColor.Red : 
                                 ConsoleColor.Gray;

        Console.WriteLine($"{e.Time:T} {e.Symbol} 价格: {e.OldPrice:C} → {e.NewPrice:C}");
        Console.ResetColor();
    }

    public void NotifyThreshold(object sender, StockPriceEventArgs e)
    {
        Console.BackgroundColor = ConsoleColor.Yellow;
        Console.ForegroundColor = ConsoleColor.Black;
        Console.WriteLine($"警告!{e.Symbol} 价格波动超过5%:{e.NewPrice:C}");
        Console.ResetColor();
    }
}

// 股票日志服务
public class StockLogger
{
    public void LogPrice(object sender, StockPriceEventArgs e)
    {
        string log = $"{e.Time:yyyy-MM-dd HH:mm:ss} {e.Symbol} " +
                    $"价格变化: {e.OldPrice:C} → {e.NewPrice:C} " +
                    $"变化: {(e.NewPrice - e.OldPrice):C} " +
                    $"({(e.NewPrice - e.OldPrice)/e.OldPrice:P2})";

        File.AppendAllText("stock_log.txt", log + Environment.NewLine);
    }
}

八、最佳实践

  1. 命名规范
  • 委托类型名称以Handler结尾
  • 事件名称使用现在时或过去时动词(如Click/Clicked
  1. 事件设计
  • 使用EventHandler<T>标准模式
  • 事件参数继承自EventArgs
  • 提供受保护的OnEventName方法触发事件
  1. 内存管理
  • 及时取消订阅事件(-=)
  • 考虑使用弱事件处理可能的内存泄漏
  1. 线程安全
  • 多线程环境下应同步事件调用
  • 使用?.Invoke()避免null检查
  1. 性能考虑
  • 避免在频繁触发的事件中执行耗时操作
  • 考虑使用事件聚合器处理大量事件

委托与事件是C#中实现松耦合、响应式编程的强大工具,熟练掌握它们对于开发高质量的.NET应用程序至关重要。通过合理运用这些机制,可以构建出灵活、可扩展且易于维护的事件驱动架构。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注