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# 官方给出的建议, 以下情况请使用委托:(后面 事件模式/委托模式 举例说明)
- 当使用事件设计模式时
- 当封装静态方法可取时
- 当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时
- 需要方便的组合
- 当类可能需要该方法的多个实现时
以下情况请使用接口:
- 当存在一组可能被调用的相关方法时
- 当类只需要方法的单个实现时
- 当使用接口的类想要将该接口强制转换为其它接口或类类型时
- 当正在实现的方法链接到类的类型或标识时,例如比较方法
事件模式 / 委托模式
也可能翻译有偏差,并没有搜索到事件设计模式(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 中异常处理
在业务实现的时候,很多场景需要保证数据的原子性,例如带有数据库操作的、一系列动作组成的场景,一旦中间某个动作失败,我们需要将所有状态恢复。
假如这种场景选择使用委托模式,并且在委托方法中抛出异常,由于委托者并不知道被委托者实际的代码流程,即使出现异常,委托者也无法确定要回滚的内容。
如一个简单下单场景:
- 用户下订单
- 库存有效并预约库存
- 创建订单
- 调用支付进行支付
- 实际扣减库存
- 订单生效状态更新
假使抛出了一个非常精确的异常,例如订单在扣减库存阶段失败,并需要委托者处理,那么委托者就必然需要了解被委托者内部实现细节,也就违背了我们使用委托模式的初衷。
所以我们应避免在 Func 和 Action 中抛出异常,或者说对于有原子性要求的业务,应避免使用委托模式。如果场景贴合委托模式,却没有办法保证相关的表达式绝对不抛出异常,那么必须采用其他开销更大的防护措施,即把异常情况页考虑进来。例如:
- 拷贝源数据,并在源数据上进行操作,整个委托执行成功时,才会真正覆盖源数据。
- 添加异常处理逻辑和额外的数据恢复流程,例如可以设计整个流程能多次重复执行,每个步骤的中间结果都保存下来,通过不同的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);