C# 委托

1. Delegate 委托

在C#中,委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用,即委托就是一种用来指向一个方法的类型变量,可以通过委托实例调用方法,关键字是 delegate。

// 声明一个委托
public delegate void MyDelegate(string message);
 
public class Program
{
    static void Main(string[] args)
    {
        // 创建委托实例,绑定到具体的方法
        MyDelegate myDelegate = new MyDelegate(DisplayMessage);
 
        // 使用委托
        myDelegate("Hello, World!");
    }
 
    // 与委托具有相同签名的方法
    static void DisplayMessage(string message)
    {
        Console.WriteLine(message);
    }
}

现在我们可以更简单的方式使用委托:

public class Program
{
    static void Main(string[] args)
    {
        MyDelegate myDelegate = message => Console.WriteLine(message);
 
        myDelegate("Hello, World!");
    }
}

委托 vs 接口

委托和接口是不是感觉很类似?都是分离类型的声明和实现,且都可以由不了解实现该接口或委托的类对象使用,那么这两个使用场景有什么区别呢?

C# 官方给出的建议, 以下情况请使用委托:(后面 事件模式/委托模式 举例说明)

  1. 当使用事件设计模式时
  2. 当封装静态方法可取时
  3. 当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时
  4. 需要方便的组合
  5. 当类可能需要该方法的多个实现时

以下情况请使用接口:

  1. 当存在一组可能被调用的相关方法时
  2. 当类只需要方法的单个实现时
  3. 当使用接口的类想要将该接口强制转换为其它接口或类类型时
  4. 当正在实现的方法链接到类的类型或标识时,例如比较方法

事件模式 / 委托模式

也可能翻译有偏差,并没有搜索到事件设计模式(eventing design pattern)。我们说的委托模式,是否可以认为是这里指的事件设计模式?
委托模式解耦了委托者与被委托者,同时又需要委托者与被委托者协同完成同一个事件。

如下代码示例,#1 事件模式时,委托者即事件发布类(publisher),被委托者即订阅类(subscriber),同时由于#5 订阅类的多样性,对于同一事件会有不同的订阅处理,且#3 委托者只需要发布事件,而不需要关心订阅者任何其他细节:

interface ISubscriber
{
    // 事件处理程序
    public void HandleSimpleEvent(object sender, EventArgs e);
}

public abstract class BaseSubscriber : ISubscriber
{
    public void Subscribe(SimplePublisher publisher)
    {
        publisher.SimpleEvent += HandleSimpleEvent;
    }

    public abstract void HandleSimpleEvent(object sender, EventArgs e);
}

// 订阅事件类1
public class SimpleSubscriber : BaseSubscriber
{
    public override void HandleSimpleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("SimpleEvent is raised.");
    }
}

// 订阅事件类2
public class ComplexSubscriber : BaseSubscriber
{
    public override void HandleSimpleEvent(object sender, EventArgs e)
    {
        Console.WriteLine("Do complex work in ComplexSubscriber.");
        Console.WriteLine("ComplexEvent is raised.");
    }
}

// 事件委托者
public class SimplePublisher
{
    // 定义委托类型
    public delegate void EventHandler(object sender, EventArgs e);

    // 定义事件
    public event EventHandler SimpleEvent;

    // 触发事件的方法
    public void RaiseEvent()
    {
        // 事件可能为null,所以先检查
        EventHandler handler = SimpleEvent;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

// 使用示例
public class Program
{
    public static void Main()
    {
        var subscribers = GetAllSubscribers();
            if (!subscribers.Any())
            {
                Console.WriteLine("No subscriber found!");
                return;
            }

            SimplePublisher publisher = new SimplePublisher();
            foreach (var subscriber in subscribers)
            {
                subscriber.Subscribe(publisher);
                // 触发事件
                publisher.RaiseEvent();
            }
        }

        // 通过反射获取所有订阅者
        private static List<BaseSubscriber> GetAllSubscribers()
        {
            var pluginTypes = Assembly.GetExecutingAssembly().GetTypes()
                .Where(t => t.IsClass && !t.IsAbstract && typeof(BaseSubscriber).IsAssignableFrom(t));
 
            List<BaseSubscriber> subscribers = new List<BaseSubscriber>();
            foreach (var pluginType in pluginTypes)
            {
                var instance = Activator.CreateInstance(pluginType) as BaseSubscriber;
                if (instance != null)
                {
                    subscribers.Add(instance);
                }
            }

            return subscribers;
        }
    }
}

------------------Output-----------------------
Do complex work in ComplexSubscriber.
ComplexEvent is raised.
SimpleEvent is raised.

委托组合

c# 允许我们用 + 将多个委托组合成一个新的委托,组合的委托可以调用组成它的所有委托,但是注意,只有相同类型的委托才可以组合。运算符 - 可以从组合的委托中移除某个委托。

delegate void Del(string s);

class TestClass
{
    static void Hello(string s)
    {
        System.Console.WriteLine("  Hello, {0}!", s);
    }

    static void Goodbye(string s)
    {
        System.Console.WriteLine("  Goodbye, {0}!", s);
    }

    static void Main()
    {
        Del a, b, c, d;

        // Create the delegate object a that references 
        // the method Hello:
        a = Hello;

        // Create the delegate object b that references 
        // the method Goodbye:
        b = Goodbye;

        // The two delegates, a and b, are composed to form c: 
        c = a + b;

        // Remove a from the composed delegate, leaving d, 
        // which calls only the method Goodbye:
        d = c - a;

        System.Console.WriteLine("Invoking delegate a:");
        a("A");
        System.Console.WriteLine("Invoking delegate b:");
        b("B");
        System.Console.WriteLine("Invoking delegate c:");
        c("C");
        System.Console.WriteLine("Invoking delegate d:");
        d("D");
    }
}
/* Output:
Invoking delegate a:
  Hello, A!
Invoking delegate b:
  Goodbye, B!
Invoking delegate c:
  Hello, C!
  Goodbye, C!
Invoking delegate d:
  Goodbye, D!
*/

委托中的协变和逆变

协变:下面官方示例种 Dogs 集成 Mammals,所以 DogsHandler 可以分配给 HandlerMethod 委托。

class Mammals {}  
class Dogs : Mammals {}  
  
class Program  
{  
    // Define the delegate.  
    public delegate Mammals HandlerMethod();  
  
    public static Mammals MammalsHandler()  
    {  
        return null;  
    }  
  
    public static Dogs DogsHandler()  
    {  
        return null;  
    }  
  
    static void Test()  
    {  
        HandlerMethod handlerMammals = MammalsHandler;  
  
        // Covariance enables this assignment.  
        HandlerMethod handlerDogs = DogsHandler;  
    }  
}

逆变:逆变可以使用一个事件处理程序而不是多个单独的处理程序,如下 MultiHandler 中的第二个参数 System.EventArgs 是 KeyEventhandler 和 MouseEventHandler 的基类,因此这两个委托都可以作为MultiHandler 入参。

public delegate void KeyEventHandler(object sender, KeyEventArgs e);
public delegate void MouseEventHandler(object sender, MouseEventArgs e);

// Event handler that accepts a parameter of the EventArgs type.  
private void MultiHandler(object sender, System.EventArgs e)  
{  
    label1.Text = System.DateTime.Now.ToString();  
}  
  
public Form1()  
{  
    InitializeComponent();  
  
    // You can use a method that has an EventArgs parameter,  
    // although the event expects the KeyEventArgs parameter.  
    this.button1.KeyDown += this.MultiHandler;  
  
    // You can use the same method
    // for an event that expects the MouseEventArgs parameter.  
    this.button1.MouseClick += this.MultiHandler;  
}

2. Func & Action 委托

Func 是接受任意数量的参数并且有返回值的委托.

public delegate TResult Func<out TResult>();

// Func 的 TResult前可以定义任意多个入参 T1, T2 ... ...
public delegate TResult Func<in T,out TResult>(T arg);

Action 是接受任意数量的参数且没有返回值的委托。

public delegate void Action<in T>(T obj);

对Func和Action泛型委托使用变体

这里给出官方示例,使用具有协变类型参数的委托:当一个方法的返回值类型 TResult 继承了某个类A,那么就可以将这个方法分配给 Func<in T, out A> / Func 委托。

// Simple hierarchy of classes.  
public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static Employee FindByTitle(String title)  
    {  
        return new Employee();  
    }  
  
    static void Test()  
    {  
        Func<String, Employee> findEmployee = FindByTitle;  
        Func<String, Person> findPerson = FindByTitle;  
 
        findPerson = findEmployee;  
  
    }  
}

使用具有逆变类型参数的委托

public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static void AddToContacts(Person person)  
    {  
        // do something
    }  
  
    static void Test()  
    {  
        Action<Person> addPersonToContacts = AddToContacts;  
        Action<Employee> addEmployeeToContacts = AddToContacts;  
        addEmployeeToContacts = addPersonToContacts;  
    }  
}

Func 与 Action 中异常处理

在业务实现的时候,很多场景需要保证数据的原子性,例如带有数据库操作的、一系列动作组成的场景,一旦中间某个动作失败,我们需要将所有状态恢复。

假如这种场景选择使用委托模式,并且在委托方法中抛出异常,由于委托者并不知道被委托者实际的代码流程,即使出现异常,委托者也无法确定要回滚的内容。

如一个简单下单场景:

  1. 用户下订单
  2. 库存有效并预约库存
  3. 创建订单
  4. 调用支付进行支付
  5. 实际扣减库存
  6. 订单生效状态更新

假使抛出了一个非常精确的异常,例如订单在扣减库存阶段失败,并需要委托者处理,那么委托者就必然需要了解被委托者内部实现细节,也就违背了我们使用委托模式的初衷。

所以我们应避免在 Func 和 Action 中抛出异常,或者说对于有原子性要求的业务,应避免使用委托模式。如果场景贴合委托模式,却没有办法保证相关的表达式绝对不抛出异常,那么必须采用其他开销更大的防护措施,即把异常情况页考虑进来。例如:

  1. 拷贝源数据,并在源数据上进行操作,整个委托执行成功时,才会真正覆盖源数据。
  2. 添加异常处理逻辑和额外的数据恢复流程,例如可以设计整个流程能多次重复执行,每个步骤的中间结果都保存下来,通过不同的flag来判断当前流程是否执行成功,如果成功则跳过直接执行下一个步骤,如果不成功则尝试重试。对于始终无法成功的场景,采用其他流程用来恢复数据(可能就需要人工参与)。

3. 其他委托:Predicate & Comparison & Converter

Predicate 是用来判断某个条件是否成立的布尔委托。

public delegate bool Predicate<in T>(T obj);

Comparison 是比较两个同类型对象的委托。

public delegate int Comparison<in T>(T x, T y);

Converter<TInput, TOutput> 是将对象从一种类型转换为另一种类型的委托。

public delegate TOutput Converter<in TInput,out TOutput>(TInput input);
posted @ 2024-06-04 21:07  文心1225  阅读(5)  评论(0编辑  收藏  举报