为什么"属性"可以执行方法——委托类型的属性
适用场景:第一次看到
DoExecute?.Invoke()、someProperty()、callback.Invoke(...)这类写法时产生的疑惑。
本文可独立阅读,也可作为以后遇到”属性怎么能当方法用”问题的速查手册。
一、问题的起点
在 MVVM 的 Command 实现里,经常看到这样的代码:
1 | public class Command : ICommand |
直觉上,属性是”存数据”的,比如 int Age { get; set; },你不可能写 Age()。那么 DoExecute?.Invoke() 为什么合法?
根因:混淆了”属性的语法形式”和”属性里装的东西的类型”。
属性只是访问方式(getter / setter 语法),它内部可以装任意类型。当属性类型是委托(delegate) 时,属性里装的就是”一个方法”,而委托天生支持被调用。
二、核心概念:委托 = 方法的引用
2.1 委托是什么
委托(delegate)是 C# 中一种特殊类型,它的变量不存数据,而是存指向某个方法的引用。
1 | public delegate void MyDelegate(); // 自定义委托:无参无返回 |
可以把它想象成”方法的容器”或者”方法的遥控器”。
2.2 内置委托:Action 与 Func
.NET 已经预置了常用委托,日常几乎不需要自己定义:
| 类型 | 签名含义 | 示例 |
|---|---|---|
Action |
无参、无返回值 | Action a = () => { }; |
Action<T> |
一个参数、无返回值 | Action<int> a = x => { }; |
Action<T1,T2> |
两个参数、无返回值 | Action<int,string> a = (x,y) => { }; |
Func<TResult> |
无参、有返回值 | Func<int> f = () => 1; |
Func<T,TResult> |
一个参数、有返回值 | Func<int,int> f = x => x * 2; |
Action 是最简单的: “一个没有参数、没有返回值的方法”的类型。
2.3 委托的三种调用写法(完全等价)
1 | Action a = () => Console.WriteLine("hi"); |
三者在编译后基本等价(a() 就是 a.Invoke() 的简写)。使用 .Invoke() 的好处是意图明确——读代码的人一眼就知道这是在”执行方法”,而不是在访问某个数据。
三、属性 vs 字段 vs 委托:三者关系
3.1 属性的本质
属性是字段的包装,本质是 get / set 方法对。
1 | // 语法上的属性 |
所以 DoExecute 的 getter 返回的是一个 Action 委托,拿到的就是”那个方法引用”。
3.2 关键对比表
| 属性类型 | 存的什么 | 能否”执行” | 典型写法 |
|---|---|---|---|
int |
数字 | 否 | Age = 18; |
string |
字符串 | 否 | Name = "abc"; |
List<int> |
集合对象 | 否(但可调其方法) | Items.Add(1); |
Action |
方法引用 | 是 | DoExecute?.Invoke(); |
Func<int> |
有返回值的方法引用 | 是 | var v = Calc.Invoke(); |
结论:能不能”执行”,取决于属性里装的东西的类型,而不是”它是属性”这件事本身。
四、完整链路:从赋值到执行
以 Command 为例,把整条链路串起来:
1 | // 1. 外部创建命令,把一个 lambda 传进去 |
1 | // 2. 构造函数里,把 lambda 存进 DoExecute 属性 |
1 | // 3. 触发时(比如按钮被点击),WPF 调用 ICommand.Execute |
执行流程图:
1 | 用户点击按钮 |
五、为什么用 ?.Invoke() 而不是直接 () 或 .Invoke()
| 写法 | 说明 | 风险 |
|---|---|---|
DoExecute.Invoke(); |
直接调用 | 若为 null,抛 NullReferenceException |
DoExecute(); |
直接调用(语法糖) | 同上 |
DoExecute?.Invoke(); |
判空后调用 | 安全,null 时静默跳过 |
?. 是 null 条件运算符(C# 6 引入),等价于:
1 | if (DoExecute != null) |
建议:调用委托属性时统一用 ?.Invoke() ,这是 C# 社区的主流写法。
六、延伸:这套模式在哪些地方出现
理解了”委托属性可以执行”,就能看懂大量框架代码:
6.1 事件(event)本质也是委托
1 | public event EventHandler Click; |
event 只是委托类型字段的特殊封装(限制外部只能 += / -=),调用时同样是 ?.Invoke()。
6.2 回调(callback)
1 | public Func<int, bool> Filter { get; set; } |
6.3 依赖注入里的工厂方法
1 | public Func<IService> ServiceFactory { get; set; } |
6.4 LINQ 里的委托参数
1 | list.Where(x => x > 10); // 传入的 lambda 就是 Func<int,bool> 委托 |
6.5 Command / RelayCommand 模式(本文起点)
- 把”要执行的方法”存为委托属性;
- 通过
ICommand.Execute统一调用; - 从而实现 MVVM 中 View 与逻辑解耦。
七、常见误区排查清单
以后看到”属性/变量被当作方法调用”而疑惑时,按顺序检查:
- 看类型:这个标识符的声明类型是
Action / Func / 自定义 delegate吗?- 是 → 它是委托,可以被调用。
- 否 → 那它不该能被
()调用,检查是否看错。
- 看调用形式:
x.Invoke(...)→ 显式调用委托。x(...)→ 语法糖,等价于上面。x?.Invoke(...)→ 带判空的显式调用。
- 看事件:
x 是 event类型吗?- 是 → 事件就是委托,
+= 订阅、Invoke触发。
- 是 → 事件就是委托,
- 看是否返回委托:有些方法返回
Func / Action,例如:1
2Action GetHandler() => () => { };
GetHandler().Invoke(); // 返回值直接调用 - 看是否是多播委托:一个委托可以挂多个方法(
+=),调用时按顺序全部执行。
八、最小可运行示例
把上面所有概念压缩成一段可以独立运行、验证的代码:
1 | using System; |
输出:
1 | 执行了绑定的方法 |
九、一句话总结
属性只是”访问方式”,委托才是”存的东西”。当属性的类型是
Action / Func / delegate 时,属性里装的就是方法引用, DoExecute?.Invoke() 就是在执行那个方法。这与 int Age 不能 Age() 并不矛盾——区别只在装的东西能不能被调用。
十、术语速查
| 术语 | 含义 |
|---|---|
| delegate | 委托类型,方法引用 |
| Action | 无返回值的内置委托 |
| Func<T, TResult> | 有返回值的内置委托 |
| Invoke | 委托的显式调用方法 |
?. |
null 条件运算符,判空后调用 |
| 多播委托 | 可 += 挂多个方法,调用时依次执行 |
| event | 委托的特殊封装,限制外部仅能订阅/退订 |
| lambda | 匿名方法,常作为委托的值传入 |

