当向类中添加新属性时,如何强制更改某些方法

本文关键字:何强制 方法 添加 新属性 属性 | 更新日期: 2023-09-27 17:57:32

我有以下问题
这是我们使用的第三方类(所以我不能更改它)

public class ThirdPartyEmployee
{
    public string F_Name { get; set; }
    public string L_Name { get; set; }
    public DateTime Date_of_birth { get; set; }
    public string Telephone1 { get; set; }
    public string Telephone2 { get; set; }
    public string Position { get; set; }
    //..... and so on
}

此外,我们还有自己的更小更好的员工级

public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MobileTelephone { get; set; }
}

有时我们需要将第三方阶级转变为我们自己的阶级。有扩展方法

public static class ThirdPartyExtensions
{
    public static Employee ConvertTo(this ThirdPartyEmployee thirdPartyEmployee)
    {
        var result = new Employee();
        result.FirstName = thirdPartyEmployee.F_Name;
        result.LastName = thirdPartyEmployee.L_Name;
        result.MobileTelephone = thirdPartyEmployee.Telephone1;
        return result;
    }
}

现在谈谈这个问题如果有人考虑向Employee类添加一些其他属性,他/她可能会忘记更改ConvertTo方法。我们怎样才能避免它理想情况下,我希望出现一些编译错误
有什么建议吗?

当向类中添加新属性时,如何强制更改某些方法

如果Employee类只是一个容器,那么有一种简单的方法:

public class Employee
{
  private readonly string firstName;
  public Employee(string firstName)
  {
    this.firstName = firstName;
  }
}

现在,您的转换方法别无选择,只能传递所有参数,因此在转换方法未更新时会出现编译器错误。

当然,这仍然不是万无一失的——如果你也关心改变论点,这也没有多大帮助。

现在我们有了Roslyn,在Visual Studio中进行了很好的集成,您实际上可以使用Roslyn分析器来制造自己的编译器错误。如果你不怕弄脏自己的手,这将是一个很好的机会来展示这样的东西有多有用。遗憾的是,它现在不太容易使用,需要"正确的思维"才能很好地使用。它将允许您制定规则,例如"类转换扩展方法必须分配结果类中的所有属性"。

不能用标准方法创建编译错误。可能有一些VisualStudio插件允许您这样做。

但这可能不是必要的:可以更改CopyTo方法,以便它不硬编码所有要复制的属性,而是使用反射来获得所有要复制公共属性的列表。

开始的示例代码:

FieldInfo[] myObjectFields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo fi in myObjectFields)
{
    i.SetValue(destination, fi.GetValue(source));
}

处理不同的属性名称:您可以引入一个属性,该属性允许您指定ThirdPartyEmployee的哪个属性转换为Employee的哪个属性。这也可以使用反射进行评估。

示例:

public class Employee
{
    [CopyFromThirdPartyEmployee("F_Name")]
    public string FirstName { get; set; }
    [CopyFromThirdPartyEmployee("L_Name")]
    public string LastName { get; set; }
    [CopyFromThirdPartyEmployee("Telephone1")]
    public string MobileTelephone { get; set; }
}

CopyTo方法发现一个没有所需映射属性的公共属性时,可以让它抛出异常。通过这种方式,您可以确保每个属性都有该属性,但这将是运行时错误,而不是编译时错误。


另一种方法是简单地将Employee作为ThirdPartyEmployee:的包装器

public class Employee
{
    private ThirdPartyEmployee _baseEmployee;
    public Employee() { _baseEmployee = new ThirdPartyEmployee(); }
    public Employee(ThirdPartyEmployee e) { _baseEmployee = e; }
    public string FirstName 
    { 
        get { return _baseEmployee.F_Name; }
        set { _baseEmployee.F_Name = value; }
    }
    ...
}

这样你就会注意到,如果你不能访问一个属性,你就没有实现它。缺点是,每个员工都会基于ThirdPartyEmployee

您可以在反射的帮助下完成此操作,但需要用于名称映射的字典:

public static class ThirdPartyExtensions
{
    static Dictionary<string, string> map;
    static ThirdPartyExtensions()
    {
        map = new Dictionary<string, string>{ {"F_Name", "FirstName"} /*and others*/};
    }  
    public static Employee ConvertTo(this ThirdPartyEmployee thirdPartyEmployee)
    {
        var result = new Employee();
        if(map.Count < typeof(Employee).GetProperties().Count())
            throw new Exception("Forget to add mapping for new field!");
        foreach(var prop in typeof(ThirdPartyEmployee).GetProperties())
            if(map.ContainsKey(prop.Name))
            {
                var temp = typeof(Employee).GetProperty(map[prop.Name]);
                temp.SetValue(result, prop.GetValue(thirdPartyEmployee));
            }
        return result;
    }
}

使用Roslyn分析器可以产生超出编译器范围的编译时(和IntelliSense)错误。下面是一个非常简单的分析器实现,用于检查方法返回的类型的所有属性是否已分配。它没有考虑控制流(例如if)。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AssignAllPropertiesAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor("AssignAllPropertiesAnalyzer",
        "All properties must be assigned.", "All properties of the return type must be assigned.", "Correctness", 
        DiagnosticSeverity.Warning, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);
    }
    private static void AnalyzeMethod(SyntaxNodeAnalysisContext context)
    {
        var methodNode = (MethodDeclarationSyntax)context.Node;
        var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode);
        if (methodSymbol.GetReturnTypeAttributes().Any(x => x.AttributeClass.Name == "AssignAllPropertiesAttribute"))
        {
            var properties = methodSymbol.ReturnType.GetMembers().OfType<IPropertySymbol>().Where(x => !x.IsReadOnly).ToList();
            foreach (var assignmentNode in methodNode.DescendantNodes().OfType<AssignmentExpressionSyntax>())
            {
                var propertySymbol = context.SemanticModel.GetSymbolInfo(assignmentNode.Left).Symbol as IPropertySymbol;
                if (propertySymbol != null)
                {
                    properties.Remove(propertySymbol);
                }
            }
            if (properties.Count > 0)
            {
                var diagnostic = Diagnostic.Create(Rule, methodSymbol.Locations[0]);
                context.ReportDiagnostic(diagnostic);
            }
        }
    }

分析器假定一个名为AssignAllProperties的属性被应用于方法的返回类型。在以下示例中,~~~~~~标记分析仪将产生诊断的位置。

class A
{
    public string S { get; set; }
}
[return: AssignAllProperties]
public static A Create()
                ~~~~~~
{
    return new A();
}

分析器既可以作为VSIX安装,也可以作为NuGet包安装。我建议始终使用NuGet方法——它会将分析器应用于使用代码的每个人,并允许您更改严重性(例如错误),从而导致编译失败。要开始构建分析器库,请安装Roslyn SDK并创建一个带有代码修复的analyzerC#项目。

您不能为此生成编译错误,但是。。。我会将转换方法移到Employee类。我建议避免依赖数据的扩展方法(类似于其他类的属性)

public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MobileTelephone { get; set; }
    public static Employee From(ThirdPartyEmployee employee)
    {
        var result = new Employee();
        result.FirstName = thirdPartyEmployee.F_Name;
        result.LastName = thirdPartyEmployee.L_Name;
        result.MobileTelephone = thirdPartyEmployee.Telephone1;
        return result;
    }
}

通过这种方式,您可以将所有功能保留在正确的类/文件中,其他人可以清楚地看到他们是否添加了属性。