callermembername 是编译器在编译期静态注入调用处成员名的特性,需配合可选参数使用,类型必须为 string,仅在参数省略时生效,不依赖运行时反射,无性能损耗。

直接说结论:CallerMemberName 不是运行时反射,也不是魔法,它由编译器在编译阶段静态注入调用处的成员名(如方法名、属性名、构造函数名),必须配合可选参数使用,否则编译报错。
为什么CallerMemberName传空字符串却能拿到方法名?
因为它的值不是“运行时读出来的”,而是编译器看到这个特性后,在 IL 层直接把字面量写死进去。比如你在 DoWork() 里调用 Log("start"),而 Log 的参数带 [CallerMemberName],那编译器就自动把 "DoWork" 这个字符串塞进参数默认值里——和手写 Log("start", "DoWork") 效果一样,只是省了你打字。
所以它不依赖 StackTrace 或 MethodBase.GetCurrentMethod(),没有性能损耗,也不受 JIT 内联影响。
- 构造函数调用时注入
".ctor" - 属性
set访问器中调用时注入该属性名(不是set) - lambda 或本地函数中调用时,注入的是**外层方法名**,不是 lambda 自身(lambda 没有成员名)
- 如果调用链跨了
await或异步重入,依然只取**直接调用者**的成员名,不会跳到上层 async 方法
CallerMemberName 必须配可选参数,否则编译失败
这是最容易卡住新手的地方:你不能这么写:
public void Log(string message, [CallerMemberName] string memberName) // ❌ 编译错误:参数没有默认值
必须显式提供默认值:
public void Log(string message, [CallerMemberName] string memberName = "") // ✅ 正确
这个默认值本身没意义,只是告诉编译器“这个参数允许不传”,从而触发特性注入逻辑。类型必须是 string,其他类型(如 object 或自定义类)会编译失败。
- 即使你传了值,比如
Log("x", "forced"),CallerMemberName也不会覆盖它——特性只在参数被省略时生效 - 多个同名参数都加该特性?不行,编译器只认第一个匹配的
- 在泛型方法里用没问题,但注入的是**调用点所在泛型实例的方法名**,不是泛型定义处的名称
常见误用场景:INotifyPropertyChanged 中的典型写法
很多人用 CallerMemberName 简化属性变更通知,但容易忽略边界情况:
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(); // 这里会注入 "Name"
}
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
这看起来很干净,但要注意:
- 如果在构造函数里调用
OnPropertyChanged()且没传参,注入的是".ctor",不是属性名——可能让绑定引擎找不到对应属性 - 手动触发某属性变更(如
OnPropertyChanged("Age"))时,CallerMemberName不起作用,得靠你手写字符串,此时重构风险仍在 - 如果属性名拼错(比如写成
OnPropertyChanged("Nmae")),编译器不报错,运行时绑定失效,且CallerMemberName帮不上忙
和 CallerFilePath/CallerLineNumber 搭配使用的注意事项
三者常一起用于日志或诊断输出,但行为差异明显:
-
CallerFilePath返回的是编译时的绝对路径(如"C:\src\App\ViewModel.cs"),CI 构建环境路径不同会导致日志路径不一致,建议在发布版本中关闭或截取文件名 -
CallerLineNumber是调用语句所在的行号,不是方法定义行——这点容易误解;如果调用语句换行或带注释,行号可能偏移 - 三个特性必须各自独立声明,不能合并成一个参数;也不能用在
ref、out或params参数上 - 在 .NET Framework 4.5+、.NET Core 1.0+ 和所有现代 .NET(6/7/8/9)中都可用,但旧版 Mono 或某些 AOT 编译目标可能不支持
最常被忽略的一点:这些信息在 Release 模式下依然有效,因为它们是编译期行为,和调试符号(PDB)无关;但如果代码被混淆器处理过,成员名可能被重命名,CallerMemberName 注入的就是混淆后的名字。











