适用场景:第一次看到 ​DoExecute?.Invoke()​、​someProperty()​、​callback.Invoke(...) 这类写法时产生的疑惑。
本文可独立阅读,也可作为以后遇到”属性怎么能当方法用”问题的速查手册。


一、问题的起点

在 MVVM 的 ​Command 实现里,经常看到这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Command : ICommand
{
public Action DoExecute { get; set; }

public Command(Action action)
{
DoExecute = action;
}

public void Execute(object parameter)
{
DoExecute?.Invoke(); // 疑问点:DoExecute 是属性,为什么能执行?
}
}

直觉上,属性是”存数据”的,比如 ​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
2
3
4
5
Action a = () => Console.WriteLine("hi");

a(); // 语法糖
a.Invoke(); // 显式调用,语义最清楚
a?.Invoke(); // 带判空的显式调用

三者在编译后基本等价(​a()​ 就是 ​a.Invoke()​ 的简写)。使用 ​.Invoke() 的好处是意图明确——读代码的人一眼就知道这是在”执行方法”,而不是在访问某个数据。


三、属性 vs 字段 vs 委托:三者关系

3.1 属性的本质

属性是字段的包装,本质是 ​get​ / ​set 方法对。

1
2
3
4
5
6
7
// 语法上的属性
public Action DoExecute { get; set; }

// 编译器实际生成的等价物(简化)
private Action _doExecute;
public Action get_DoExecute() { return _doExecute; }
public void set_DoExecute(Action value) { _doExecute = value; }

所以 ​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
2
// 1. 外部创建命令,把一个 lambda 传进去
var cmd = new Command(() => MessageBox.Show("clicked"));
1
2
3
4
5
// 2. 构造函数里,把 lambda 存进 DoExecute 属性
public Command(Action action)
{
DoExecute = action; // DoExecute 现在"指向"那个 lambda
}
1
2
3
4
5
// 3. 触发时(比如按钮被点击),WPF 调用 ICommand.Execute
public void Execute(object parameter)
{
DoExecute?.Invoke(); // 取出属性里的委托,执行它指向的方法
}

执行流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户点击按钮
|
v
WPF 框架调用 ICommand.Execute(parameter)
|
v
Execute() 内执行 DoExecute?.Invoke()
|
v
DoExecute 属性返回的委托被调用
|
v
实际执行的是构造时传入的 lambda 方法体
|
v
MessageBox.Show("clicked")

五、为什么用 ​?.Invoke()​ 而不是直接 ​()​ 或 ​.Invoke()

写法 说明 风险
DoExecute.Invoke(); 直接调用 若为 null,抛 ​NullReferenceException
DoExecute(); 直接调用(语法糖) 同上
DoExecute?.Invoke(); 判空后调用 安全,null 时静默跳过

?. 是 null 条件运算符(C# 6 引入),等价于:

1
2
3
4
if (DoExecute != null)
{
DoExecute.Invoke();
}

建议:调用委托属性时统一用 ​ ​?.Invoke()​ ​ ,这是 C# 社区的主流写法。


六、延伸:这套模式在哪些地方出现

理解了”委托属性可以执行”,就能看懂大量框架代码:

6.1 事件(event)本质也是委托

1
2
3
4
5
6
public event EventHandler Click;

void OnClick()
{
Click?.Invoke(this, EventArgs.Empty);
}

event​ 只是委托类型字段的特殊封装(限制外部只能 ​+=​ / ​-=​),调用时同样是 ​?.Invoke()。

6.2 回调(callback)

1
2
3
4
public Func<int, bool> Filter { get; set; }

// 使用
bool pass = Filter?.Invoke(42) ?? true;

6.3 依赖注入里的工厂方法

1
2
public Func<IService> ServiceFactory { get; set; }
var svc = ServiceFactory.Invoke();

6.4 LINQ 里的委托参数

1
list.Where(x => x > 10);   // 传入的 lambda 就是 Func<int,bool> 委托

6.5 ​Command​ / ​RelayCommand 模式(本文起点)

  • 把”要执行的方法”存为委托属性;
  • 通过 ​ICommand.Execute 统一调用;
  • 从而实现 MVVM 中 View 与逻辑解耦。

七、常见误区排查清单

以后看到”属性/变量被当作方法调用”而疑惑时,按顺序检查:

  1. 看类型:这个标识符的声明类型是 ​Action​ / ​Func​ / 自定义 ​delegate 吗?
    • 是 → 它是委托,可以被调用。
    • 否 → 那它不该能被 ​() 调用,检查是否看错。
  2. 看调用形式:
    • x.Invoke(...) → 显式调用委托。
    • x(...) → 语法糖,等价于上面。
    • x?.Invoke(...) → 带判空的显式调用。
  3. 看事件:​x​ 是 ​event 类型吗?
    • 是 → 事件就是委托,​+=​ 订阅、​Invoke 触发。
  4. 看是否返回委托:有些方法返回 ​Func​ / ​Action,例如:
    1
    2
    Action GetHandler() => () => { };
    GetHandler().Invoke(); // 返回值直接调用
  5. 看是否是多播委托:一个委托可以挂多个方法(​+=),调用时按顺序全部执行。

八、最小可运行示例

把上面所有概念压缩成一段可以独立运行、验证的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;

public class Demo
{
// 委托类型的属性
public Action DoExecute { get; set; }

public void Run()
{
DoExecute?.Invoke();
}
}

public class Program
{
public static void Main()
{
var demo = new Demo();

// 未赋值时调用:安全,不抛异常
demo.Run();

// 赋值一个 lambda
demo.DoExecute = () => Console.WriteLine("执行了绑定的方法");

// 触发执行
demo.Run();

// 多播委托:再挂一个
demo.DoExecute += () => Console.WriteLine("第二个方法也执行了");
demo.Run();
}
}

输出:

1
2
3
执行了绑定的方法
执行了绑定的方法
第二个方法也执行了

九、一句话总结

属性只是”访问方式”,委托才是”存的东西”。当属性的类型是 ​​Action​​ / ​​Func​​ / ​​delegate​​ 时,属性里装的就是方法引用, ​​DoExecute?.Invoke()​ ​ 就是在执行那个方法。这与 ​​int Age​​ 不能 ​​Age()​ ​ 并不矛盾——区别只在装的东西能不能被调用。


十、术语速查

术语 含义
delegate 委托类型,方法引用
Action 无返回值的内置委托
Func<T, TResult> 有返回值的内置委托
Invoke 委托的显式调用方法
?. null 条件运算符,判空后调用
多播委托 可 ​+= 挂多个方法,调用时依次执行
event 委托的特殊封装,限制外部仅能订阅/退订
lambda 匿名方法,常作为委托的值传入