属性构造函数中的 Lambda 表达式

Posted

技术标签:

【中文标题】属性构造函数中的 Lambda 表达式【英文标题】:Lambda expression in attribute constructor 【发布时间】:2013-05-24 10:36:47 【问题描述】:

我创建了一个名为RelatedPropertyAttributeAttribute 类:

[AttributeUsage(AttributeTargets.Property)]
public class RelatedPropertyAttribute: Attribute

    public string RelatedProperty  get; private set; 

    public RelatedPropertyAttribute(string relatedProperty)
    
        RelatedProperty = relatedProperty;
    

我用它来表示类中的相关属性。我将如何使用它的示例:

public class MyClass

    public int EmployeeID  get; set; 

    [RelatedProperty("EmployeeID")]
    public int EmployeeNumber  get; set; 

我想使用 lambda 表达式,以便将强类型传递给属性的构造函数,而不是“魔术字符串”。这样我可以利用编译器类型检查。例如:

public class MyClass

    public int EmployeeID  get; set; 

    [RelatedProperty(x => x.EmployeeID)]
    public int EmployeeNumber  get; set; 

我以为我可以用以下方法做到这一点,但编译器不允许这样做:

public RelatedPropertyAttribute<TProperty>(Expression<Func<MyClass, TProperty>> propertyExpression)
 ... 

错误:

非泛型类型“RelatedPropertyAttribute”不能与 类型参数

我怎样才能做到这一点?

【问题讨论】:

尝试使类成为通用类..而不是单独的构造函数。 我认为属性参数仅限于compile-time constants, typeof expression or array creation expression of an attribute parameter type。您甚至不能将小数作为参数传递。看看***.com/questions/11004909/… 泛型类型不能从“属性”派生,因为它是一个属性类 可能像[PostSharp][postsharp.net/]这样的工具可以帮你解决。它可以在.NET编译器执行之前集成到构建过程中生成代码。 没有时间给出完整的答案,但实现最终目标的一种方法是:让班级拥有Tuple&lt;,&gt;s 和Expression&lt;Func&lt;class,object&gt;&gt;s 的静态列表;此列表中的每个条目都是相关属性的 getter 元组。这在编译时是强大的,并且在执行时也是可查询的。 【参考方案1】:

以传统方式不可能拥有通用属性。但是 C# 和 VB 不支持它,但 CLR 支持。如果你想写一些 IL 代码是可能的。

让我们来看看你的代码:

[AttributeUsage(AttributeTargets.Property)]
public class RelatedPropertyAttribute: Attribute

    public string RelatedProperty  get; private set; 

    public RelatedPropertyAttribute(string relatedProperty)
    
       RelatedProperty = relatedProperty;
    

编译代码,使用ILSpy 或ILDasm 打开程序集,然后将内容转储到 文本文件。您的属性类声明的 IL 将如下所示:

.class public auto ansi beforefieldinit RelatedPropertyAttribute
extends [mscorlib]System.Attribute

在文本文件中,您可以将属性设为通用。有几件事需要改变。

这可以简单地通过更改 IL 来完成,CLR 不会抱怨:

.class public abstract auto ansi beforefieldinit
      RelatedPropertyAttribute`1<class T>
      extends [mscorlib]System.Attribute

现在您可以将 relatedProperty 的类型从字符串更改为您的泛型类型。

例如:

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
        string relatedProperty
    ) cil managed

改成:

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
        !T relatedProperty
    ) cil managed

有很多框架可以做这样的“肮脏”工作:Mono.Cecil 或 CCI。

正如我已经说过的,它不是一个干净的面向对象的解决方案,只是想指出另一种打破 C# 和 VB 限制的方法。

关于这个主题有一个有趣的阅读,check it out 这本书。

希望对你有帮助。

【讨论】:

@MatthiasMüller 感谢您的支持!很抱歉问你,但你是发表论文 Real time dynamic broken with VACD 的同一个人吗? 嗨,不,我只是一个初级开发人员,我喜欢这种疯狂的方法:D GitHub上有一个功能请求到support lambdas in attributes in C#。【参考方案2】:

你不能

您不能创建通用属性类型(这是不允许的);同样,没有定义 使用 通用属性 ([Foo&lt;SomeType&gt;]) 的语法 您不能在属性初始值设定项中使用 lambda - 可传递给属性的值非常有限,而且根本不包括表达式(非常复杂,是运行时对象,而不是编译时文字)

【讨论】:

马克,顺便问一下,你知道为什么我们不能将小数作为参数传递给属性吗?我从未遇到过这种限制背后的解释。 编辑:发现于use decimal values as attribute params in c#? @Ilya 因为 IL 不知道小数,即使 C# 知道。根本没有直接的 IL 机制可以在元数据中表示它们。 @IlyaIvanov:确实这是一个单独的问题,但幸运的是它已经被问到了:***.com/questions/3192833/…【参考方案3】:

如果你使用的是 C# 6.0,你可以使用nameof

用于获取变量的简单(非限定)字符串名称, 类型或成员。在代码中报告错误时,连接 模型-视图-控制器 (MVC) 链接,触发属性更改事件, 等等,您经常想要捕获方法的字符串名称。使用 nameof 有助于在重命名定义时保持代码有效。前 您必须使用字符串文字来引用定义,即 重命名代码元素时很脆弱,因为工具不知道要检查 这些字符串文字。

有了它,你可以像这样使用你的属性:

public class MyClass

    public int EmployeeID  get; set; 

    [RelatedProperty(nameof(EmployeeID))]
    public int EmployeeNumber  get; set; 

【讨论】:

【参考方案4】:

一种可能的解决方法是为每个属性关系定义类并通过 属性构造函数中的 typeof() 运算符。

更新:

例如:

[AttributeUsage(AttributeTargets.Property)]
public class RelatedPropertyAttribute : Attribute

    public Type RelatedProperty  get; private set; 

    public RelatedPropertyAttribute(Type relatedProperty)
    
        RelatedProperty = relatedProperty;
    


public class PropertyRelation<TOwner, TProperty>

    private readonly Func<TOwner, TProperty> _propGetter;

    public PropertyRelation(Func<TOwner, TProperty> propGetter)
    
        _propGetter = propGetter;
    

    public TProperty GetProperty(TOwner owner)
    
        return _propGetter(owner);
    


public class MyClass

    public int EmployeeId  get; set; 

    [RelatedProperty(typeof(EmployeeIdRelation))]
    public int EmployeeNumber  get; set; 

    public class EmployeeIdRelation : PropertyRelation<MyClass, int>
    
        public EmployeeIdRelation()
            : base(@class => @class.EmployeeId)
        

        
    

【讨论】:

真正让事情变得容易理解的代码。 ...base(@class=> @class.EmployeeId)... ? 非常有趣的方法。也许您可以通过添加有关如何使用 GetProperty 方法的内容来扩展代码示例。【参考方案5】:

你不能。属性类型限制为写入here。我的建议是,尝试在外部评估您的 lambda 表达式,然后使用以下类型之一:

简单类型(bool、byte、char、short、int、long、float 和 double) 字符串 System.Type 枚举 object(object类型的属性参数的参数必须是上述类型之一的常量值。) 上述任何类型的一维数组

【讨论】:

【参考方案6】:

扩展my comment,这是一种用不同方法完成任务的方法。你说你想“在一个类中表示相关的属性”,并且你“想使用 lambda 表达式 这样我就可以将一个强类型传递给我的属性的构造函数,而不是一个“魔术字符串”。这样我可以利用编译器类型检查”。

这是一种指示相关属性的方法,它是编译时类型并且没有任何魔术字符串:

public class MyClass

    public int EmployeeId  get; set; 
    public int EmployeeNumber  get; set; 

这是正在考虑的课程。我们想指出EmployeeIdEmployeeNumber 是相关的。为了代码简洁,让我们把这个类型别名放在代码文件的顶部。完全没有必要,但它确实使代码不那么令人生畏:

using MyClassPropertyTuple = 
    System.Tuple<
            System.Linq.Expressions.Expression<System.Func<MyClass, object>>,
            System.Linq.Expressions.Expression<System.Func<MyClass, object>>
        >;

这使得MyClassPropertyTuple 成为两个Expressions 中Tuple 的别名,每个Expressions 都将函数的定义从MyClass 捕获到对象。例如,MyClass 上的属性 getter 就是这样的函数。

现在让我们捕捉这种关系。这里我在MyClass 上创建了一个静态属性,但是这个列表可以在任何地方定义:

public class MyClass

    public static List<MyClassPropertyTuple> Relationships
        = new List<MyClassPropertyTuple>
            
                new MyClassPropertyTuple(c => c.EmployeeId, c => c.EmployeeNumber)
            ;

C# 编译器知道我们正在构造 TupleExpressions,因此我们不需要在这些 lambda 表达式前面进行任何显式转换 - 它们会自动转换为 Expressions。

就定义而言基本上就是这样 - 那些EmployeeIdEmployeeNumber 提及在编译时是强类型和强制执行的,并且进行属性重命名的重构工具应该能够在重命名期间找到这些用法( ReSharper 绝对可以)。这里没有魔术字符串。


当然,我们也希望能够在运行时询问关系(我假设!)。我不知道你想怎么做,所以这段代码只是说明性的。

class Program

    static void Main(string[] args)
    
        var propertyInfo1FromReflection = typeof(MyClass).GetProperty("EmployeeId");
        var propertyInfo2FromReflection = typeof(MyClass).GetProperty("EmployeeNumber");

        var e1 = MyClass.Relationships[0].Item1;

        foreach (var relationship in MyClass.Relationships)
        
            var body1 = (UnaryExpression)relationship.Item1.Body;
            var operand1 = (MemberExpression)body1.Operand;
            var propertyInfo1FromExpression = operand1.Member;

            var body2 = (UnaryExpression)relationship.Item2.Body;
            var operand2 = (MemberExpression)body2.Operand;
            var propertyInfo2FromExpression = operand2.Member;

            Console.WriteLine(propertyInfo1FromExpression.Name);
            Console.WriteLine(propertyInfo2FromExpression.Name);

            Console.WriteLine(propertyInfo1FromExpression == propertyInfo1FromReflection);
            Console.WriteLine(propertyInfo2FromExpression == propertyInfo2FromReflection);
        
    

propertyInfo1FromExpressionpropertyInfo2FromExpression 的代码在这里我在调试时明智地使用了 Watch 窗口 - 这通常是我计算 Expression 树实际包含的内容的方法。

运行这个会产生

EmployeeId
EmployeeNumber
True
True

表明我们可以成功提取相关属性的详细信息,并且(至关重要)它们与通过其他方式获得的PropertyInfos 的引用相同。希望您可以将它与您实际使用的任何方法结合使用,以在运行时指定感兴趣的属性。

【讨论】:

【参考方案7】:

提示。使用nameof。我有一个 DateRangeAttribute,它验证两个属性并确保它们是有效的 DateRange。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
 public class DateRangeAttribute : ValidationAttribute
 
      private readonly string _endDateProperty;
      private readonly string _startDateProperty;

      public DateRangeAttribute(string startDateProperty, string endDateProperty) : base()
      
            _startDateProperty = startDateProperty;
            _endDateProperty = endDateProperty;
      

      protected override ValidationResult IsValid(object value, ValidationContext validationContext)
      
            var stP = validationContext.ObjectType.GetProperty(_startDateProperty);
            var enP = validationContext.ObjectType.GetProperty(_endDateProperty);
            if (stP == null || enP == null || stP.GetType() != typeof(DateTime) || enP.GetType() != typeof(DateTime))
            
                 return new ValidationResult($"startDateProperty and endDateProperty must be valid DateTime properties of nameof(value).");
            
            DateTime start = (DateTime)stP.GetValue(validationContext.ObjectInstance, null);
            DateTime end = (DateTime)enP.GetValue(validationContext.ObjectInstance, null);

            if (start <= end)
            
                 return ValidationResult.Success;
            
            else
            
                 return new ValidationResult($"_endDateProperty must be equal to or after _startDateProperty.");
            
      
 


class Tester

    public DateTime ReportEndDate  get; set; 
    [DateRange(nameof(ReportStartDate), nameof(ReportEndDate))]
    public DateTime ReportStartDate  get; set; 

【讨论】:

以上是关于属性构造函数中的 Lambda 表达式的主要内容,如果未能解决你的问题,请参考以下文章

了解 Lambda 闭包类型如何删除默认构造函数

Java8新特性——Lambda表达式之四大核心函数式接口 & 方法/构造器/数组引用

Java8新特性——Lambda表达式之四大核心函数式接口 & 方法/构造器/数组引用

将Lambda表达式作为参数传递并解析-在构造函数参数列表中使用Lambda表达式

java8新特性→方法和构造函数引用:替代Lambda表达式

Kotlin学习