如何用C#实现重复属性访问器简化?优先C# 8.0
问题背景
我们有一个包含数十个访问逻辑高度重复属性的大型类,代码结构如下:
public struct ObservableField<T> { internal T Val; public ObservableField(T val) => this.Val = val; // 其他逻辑 } public class DataConfiguration { private ObservableField<bool> isEnabled; public bool IsEnabled { get => GetValue( ref isEnabled, nameof(IsEnabled)); set { if(SetValue( ref isEnabled, nameof(IsEnabled), value)) OnDataChanged(nameof(IsEnabled)); } } private ObservableField<WrapMode> dataWrapMode; public WrapMode DataWrapMode { get => GetValue( ref dataWrapMode, nameof(DataWrapMode)); set { if(SetValue( ref dataWrapMode, nameof(DataWrapMode), value)) OnDataChanged(nameof(DataWrapMode)); } } // 更多类似属性 }
早期代码库基于C# 4之前的版本,当时没有nameof语法,曾出现复制粘贴错误(比如属性Dirty的OnDataChanged错误传入了"IsEnabled"):
private ObservableField<bool> dirty; public bool Dirty { get => GetValue( ref dirty, "Dirty"); set { if(SetValue( ref dirty, "Dirty", value)) OnDataChanged("IsEnabled"); // 此处错误 } }
我们需要精简代码、降低出错概率,要求如下:
- 优先采用T4代码生成方案
- 无法修改
ObservableField结构(已在大型代码库广泛使用,且部分属性需要特殊处理) - 不能使用包装器(存在大量基于特性和反射的逻辑用于GUI属性发现)
如果用C风格宏可以简单实现,但不符合C#惯例:
#define OBSERVABLE_WITH_ACCESSORS(name,type) \ private ObservableField<type> _##name; \ public type name \ { \ get => GetValue( ref _##name, nameof(name)); \ set \ { \ if(SetValue( ref _##name, nameof(name), value)) \ OnDataChanged(nameof(name)); \ } \ }
需要解决的问题:如何在C# 8.0中实现需求?若C# 8.0难以实现,也可提供更高版本C#的方案(我们计划后续迁移版本)。
解决方案
一、T4代码生成方案(兼容C# 8.0)
T4是Visual Studio原生支持的代码生成工具,可以完美实现类似宏的代码复用,同时保留手动编写特殊属性的灵活性。
1. 创建T4模板文件(比如ObservableProperties.tt)
<#@ template language="C#" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Collections.Generic" #> // 自动生成的可观察属性代码,请勿手动修改 namespace YourNamespace { public partial class DataConfiguration { <# // 定义需要生成的属性列表:键是属性名,值是属性类型 var properties = new Dictionary<string, string> { {"IsEnabled", "bool"}, {"DataWrapMode", "WrapMode"}, {"Dirty", "bool"}, // 添加更多属性... }; foreach(var prop in properties) { string propName = prop.Key; string propType = prop.Value; string backingFieldName = "_" + char.ToLowerInvariant(propName[0]) + propName.Substring(1); #> private ObservableField<<#= propType #>> <#= backingFieldName #>; public <#= propType #> <#= propName #> { get => GetValue(ref <#= backingFieldName #>, nameof(<#= propName #>)); set { if(SetValue(ref <#= backingFieldName #>, nameof(<#= propName #>), value)) OnDataChanged(nameof(<#= propName #>)); } } <# } #> } }
2. 使用方式
- 将
DataConfiguration改为部分类(partial class DataConfiguration),这样自动生成的代码可以和手动编写的特殊属性代码合并 - 手动维护T4模板中的属性列表,运行模板即可自动生成所有重复的属性代码
- 特殊属性可以直接在手动编写的
DataConfiguration部分类中实现,不受自动生成代码影响
优势
- 完全兼容C# 8.0,无需版本迁移
- 彻底消除复制粘贴错误:所有
nameof引用由模板统一生成 - 保留灵活性:特殊属性可以手动编写,不依赖模板
二、C# 9+ 源代码生成器方案(推荐长期迁移方案)
如果后续迁移到C# 9及以上版本,可以使用.NET的源代码生成器(Source Generator),这是更现代化、更符合C#惯例的解决方案。
1. 定义标记特性
首先创建一个特性来标记需要自动生成可观察属性的字段:
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public sealed class ObservablePropertyAttribute : Attribute { // 可选:允许指定属性名,默认使用字段名(去掉下划线开头) public string? PropertyName { get; set; } }
2. 实现源代码生成器
创建一个源代码生成器项目,实现ISourceGenerator接口:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; [Generator] public class ObservablePropertyGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { // 注册语法接收器,监听带ObservablePropertyAttribute的字段 context.RegisterForSyntaxNotifications(() => new ObservablePropertySyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is not ObservablePropertySyntaxReceiver receiver) return; foreach (var field in receiver.Fields) { // 获取字段的类型和名称 var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(field) as IFieldSymbol; if (fieldSymbol == null) continue; // 获取特性参数,确定属性名 var attr = fieldSymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == nameof(ObservablePropertyAttribute)); string propName = attr?.ConstructorArguments.FirstOrDefault().Value as string ?? fieldSymbol.Name.TrimStart('_'); // 确保属性名首字母大写 propName = char.ToUpperInvariant(propName[0]) + propName.Substring(1); var propType = fieldSymbol.Type.Name; var backingFieldName = fieldSymbol.Name; var className = fieldSymbol.ContainingType.Name; var namespaceName = fieldSymbol.ContainingNamespace.Name; // 生成属性代码 var source = $@" namespace {namespaceName} {{ public partial class {className} {{ public {propType} {propName} {{ get => GetValue(ref {backingFieldName}, nameof({propName})); set {{ if(SetValue(ref {backingFieldName}, nameof({propName}), value)) OnDataChanged(nameof({propName})); }} }} }} }}"; // 添加生成的代码到编译上下文 context.AddSource($"{className}_{propName}.g.cs", source); } } // 语法接收器:收集带ObservablePropertyAttribute的字段 private class ObservablePropertySyntaxReceiver : ISyntaxReceiver { public List<FieldDeclarationSyntax> Fields { get; } = new(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is FieldDeclarationSyntax fieldDecl && fieldDecl.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == nameof(ObservablePropertyAttribute)))) { Fields.Add(fieldDecl); } } } }
3. 使用方式
- 在
DataConfiguration类中,只需要声明带特性的字段,无需手动编写属性:
public partial class DataConfiguration { [ObservableProperty] private ObservableField<bool> _isEnabled; [ObservableProperty] private ObservableField<WrapMode> _dataWrapMode; // 特殊属性手动编写 private ObservableField<int> _specialValue; public int SpecialValue { get => GetValue(ref _specialValue, nameof(SpecialValue)); set { // 自定义特殊逻辑 if(value < 0) value = 0; if(SetValue(ref _specialValue, nameof(SpecialValue), value)) OnDataChanged(nameof(SpecialValue)); } } }
优势
- 完全符合C#惯例,无需维护模板列表,代码更简洁
- 实时生成代码,开发体验更流畅
- 强类型检查,编译期即可发现错误
三、C# 8.0 替代方案(无代码生成)
如果暂时无法使用代码生成工具,也可以利用C# 8.0的扩展方法来简化代码,但无法完全消除重复:
// 定义扩展方法封装重复逻辑 public static class ObservableFieldExtensions { public static bool SetAndNotify<T>(this ref ObservableField<T> field, string propertyName, T value, Action<string> onChanged) { if(EqualityComparer<T>.Default.Equals(field.Val, value)) return false; field.Val = value; onChanged(propertyName); return true; } public static T GetValue<T>(this ref ObservableField<T> field) { return field.Val; } } // 使用扩展方法简化属性 public class DataConfiguration { private ObservableField<bool> _isEnabled; public bool IsEnabled { get => _isEnabled.GetValue(); set => _isEnabled.SetAndNotify(nameof(IsEnabled), value, OnDataChanged); } // 其他属性类似 }
该方案只能减少重复代码量,但仍需手动编写每个属性,无法彻底避免复制粘贴错误。
内容的提问来源于stack exchange,提问作者ABaumstumpf
相关产品推荐
相关产品推荐

