本篇定位
Source Generators 是 8.0 最重磅的特性——在编译时生成代码,而不是在运行时通过反射。读完你会理解:
[ObservableProperty] 在编译时到底生成了什么?
[RelayCommand] 如何把方法变成命令属性?
- 生成器如何从 C# 语法树中提取信息?
1. 示例一:对比有/无 Source Generators
没有生成器(纯手写)
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 34 35 36 37 38 39 40
| public class LoginViewModel : ObservableObject { private string _userName; private string _password; private bool _isLoggingIn;
public string UserName { get => _userName; set { if (SetProperty(ref _userName, value)) LoginCommand.NotifyCanExecuteChanged(); } }
public string Password { get => _password; set { if (SetProperty(ref _password, value)) LoginCommand.NotifyCanExecuteChanged(); } }
public bool IsLoggingIn { get => _isLoggingIn; set => SetProperty(ref _isLoggingIn, value); }
private RelayCommand? _loginCommand; public RelayCommand LoginCommand => _loginCommand ??= new RelayCommand(Login, CanLogin);
private void Login() { } private bool CanLogin() => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password); }
|
共 48 行,重复代码多。
有生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public partial class LoginViewModel : ObservableObject { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoginCommand))] private string _userName;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoginCommand))] private string _password;
[ObservableProperty] private bool _isLoggingIn;
[RelayCommand(CanExecute = nameof(CanLogin))] private void Login() { }
private bool CanLogin() => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password); }
|
共 18 行,减少 60% 样板代码。 两者编译结果完全相同。
2. 示例二:看生成的代码
在 .csproj 中开启:
1 2 3
| <PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> </PropertyGroup>
|
编译后在 obj/Debug/net8.0/generated/ 下能看到生成的 .g.cs:
[ObservableProperty] 生成的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| partial class LoginViewModel { public string UserName { get => _userName; set { if (!EqualityComparer<string>.Default.Equals(_userName, value)) { OnPropertyChanging("UserName"); _userName = value; OnPropertyChanged("UserName");
LoginCommand.NotifyCanExecuteChanged(); } } } }
|
[RelayCommand] 生成的代码
1 2 3 4 5 6 7 8
| partial class LoginViewModel { private RelayCommand? _loginCommandField;
public RelayCommand LoginCommand => _loginCommandField ??= new RelayCommand(Login, CanLogin); }
|
3. 示例三:常用注解组合
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
| public partial class ProfileViewModel : ObservableObject { [ObservableProperty] private string _firstName;
[ObservableProperty] private string _lastName;
public string FullName => $"{FirstName} {LastName}";
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] private string _nickName; }
|
partial 方法钩子
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
| public partial class FormViewModel : ObservableObject { [ObservableProperty] private string _email;
partial void OnEmailChanged(string value) { ValidateEmail(value); }
partial void OnEmailChanging(string value) { if (string.IsNullOrEmpty(value)) Console.WriteLine("警告:邮箱为空"); }
private void ValidateEmail(string email) { if (!email.Contains('@')) Console.WriteLine("邮箱格式不正确"); } }
|
4. 生成器工作原理
1 2 3 4 5 6 7 8 9 10
| 你的代码(有注解的 partial class) ↓ Roslyn 解析为 SyntaxTree ↓ Source Generator 分析语法树 ↓ 找到 [ObservableProperty] 字段 / [RelayCommand] 方法 ↓ 生成 .g.cs 文件加入编译 ↓ 与你手写的代码一起编译为程序集
|
生成器入口(简化版)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [Generator] public class ObservablePropertyGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); }
public void Execute(GeneratorExecutionContext context) { foreach (var field in GetObservablePropertyFields(context)) { var source = GenerateProperty(field); context.AddSource( $"{field.ContainingType.Name}_{field.Name}.g.cs", source); } } }
|
字段名 → 属性名转换规则
5. 常见编译错误
| 错误 |
原因 |
解决 |
MVVMTK0001 |
class 不是 partial |
加 partial |
MVVMTK0002 |
没继承 ObservableObject |
加上继承 |
MVVMTK0003 |
字段是 static |
去掉 static |
MVVMTK0004 |
字段是 readonly |
去掉 readonly |
MVVMTK0032 |
[RelayCommand] 方法不在 partial class 中 |
加 partial |
6. 字段初始化陷阱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public partial class MyVM : ObservableObject { [ObservableProperty] private string _name = "默认值";
public MyVM() { Name = "另一个值"; } }
|
7. 调试生成代码的技巧
1 2 3 4 5 6 7 8 9 10
|
partial void OnNameChanged(string value) { }
|
8. 手动实现 vs 生成器实现
| 对比项 |
手写 |
Source Generator |
| 代码量 |
多 |
少 60%+ |
| 运行时性能 |
相同 |
相同(生成代码就是手写代码) |
| 调试体验 |
直接看到 |
可在 生成的文件 中查看 |
| 约束 |
无 |
需要 partial class + 正确基类 |
下一篇预告:ObservableRecipient 源码分析 — 带消息能力的 ViewModel 基类