Action作为参数传递后无法订阅,如何实现正确订阅?
你说得完全没错!在C#里,委托(包括Action)虽然是引用类型,但作为参数传递时是按值传递引用——也就是说,你传递的是原委托引用的一个副本。在Subscribe方法里对action做+=操作,本质是创建了一个新的委托实例并赋值给这个副本,完全不会影响到原类里的_action字段,这就是为什么运行时没打印"2"的原因。
下面给你几种可行的解决方案,以及关于Action是否适合这类场景的分析:
方案1:用ref关键字传递委托
直接通过ref传递委托引用,这样方法内的操作会直接作用于原委托实例:
public class TestClass { private Action _action; // 注意:readonly修饰符要去掉,因为ref传递会修改引用 public TestClass() { _action += () => { Console.WriteLine("1"); }; Subscribe(ref _action); // 传递原委托的引用 _action?.Invoke(); } private static void Subscribe(ref Action action) { // 用ref修饰参数 action += () => { Console.WriteLine("2"); }; } }
这样修改后,Subscribe里的+=会直接修改原_action的引用,调用_action.Invoke()时就会同时执行两个lambda了。注意要去掉_action的readonly修饰符,因为ref传递会改变它的引用指向。
方案2:用包装类封装委托
如果不想用ref(比如方法需要异步执行,或者需要更灵活的封装),可以把Action放在一个引用类型的包装类里,传递包装类实例:
// 定义委托包装类 public class ActionWrapper { public Action Action { get; set; } } public class TestClass { private readonly ActionWrapper _actionWrapper = new ActionWrapper(); public TestClass() { _actionWrapper.Action += () => { Console.WriteLine("1"); }; Subscribe(_actionWrapper); _actionWrapper.Action?.Invoke(); } private static void Subscribe(ActionWrapper wrapper) { wrapper.Action += () => { Console.WriteLine("2"); }; } }
因为ActionWrapper是引用类型,传递的是它的引用,所以修改wrapper.Action会直接影响原实例的委托,而且可以保留readonly修饰符。
方案3:改用事件(推荐)
其实这种订阅-触发的场景,C#专门设计了**事件(event)**来处理,它本质是对委托的封装,不仅能避免直接修改委托引用的问题,还提供了更好的安全性(外部只能订阅/取消订阅,不能直接触发或覆盖委托):
public class TestClass { // 声明事件 private event Action MyEvent; public TestClass() { MyEvent += () => { Console.WriteLine("1"); }; SubscribeToEvent(); MyEvent?.Invoke(); } // 订阅事件的方法 private void SubscribeToEvent() { MyEvent += () => { Console.WriteLine("2"); }; } }
如果需要外部类也能订阅这个事件,只需要把MyEvent的访问修饰符改成public即可。事件是这类场景的最佳实践,比直接使用Action更规范、更安全。
关于Action是否适用于此类场景
Action本身可以用来实现简单的订阅逻辑,但因为它是普通委托,没有事件的封装性,容易出现意外的委托覆盖(比如不小心用=代替+=),或者传递时的引用副本问题。如果只是内部简单逻辑,用ref或包装类可以解决,但如果是需要对外暴露订阅能力的场景,强烈推荐使用事件,它更符合C#的设计规范。
内容的提问来源于stack exchange,提问作者Vahagn

