You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 17:06:02