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

本文关键字:lambda 表达式 构造函数 属性 | 更新日期: 2023-09-27 18:15:55

我创建了一个名为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)
{ ... }

错误:

非泛型类型"相关属性属性"不能与 类型参数

我怎样才能做到这一点?

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

以传统方式无法实现泛型属性。但是,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

现在,您可以将相关属性的类型从字符串更改为泛型类型。

例如:

.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限制的方法。

围绕这个主题有一个有趣的阅读,看看这本书。

希望对您有所帮助。

你不能

  • 不再为真:您无法创建通用属性,耶!您不能创建泛型属性类型(根本不允许(;同样,没有定义使用泛型属性([Foo<SomeType>](的语法
  • 您不能在属性初始值设定项中使用 lambda - 可传递给属性的值非常有限,并且根本不包括表达式(表达式非常复杂,并且是运行时对象,而不是编译时文本(

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

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

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

public class MyClass
{
    public int EmployeeID { get; set; }
    [RelatedProperty(nameof(EmployeeID))]
    public int EmployeeNumber { get; set; }
}

可能的解决方法之一是为每个属性关系定义类,并通过以下方式
引用它属性构造函数中的 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)
        {
        }
    }
}
你不能

。属性类型受到限制,如此处所述。我的建议是,尝试在外部评估您的 lambda 表达式,然后使用以下类型之一:

  • 简单类型(布尔值、字节、字符、短整型、整型、长整型、浮点型和双精度型(
  • 字符串
  • 系统类型。
  • 枚举
  • 对象
  • (对象类型的属性参数的参数必须是上述类型之一的常量值。
  • 上述任何类型的一维数组

为了扩展我的评论,这是一种用不同的方法完成任务的方法。您说您想"指示类中的相关属性",并且您"希望使用 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成为两个Expression Tuple的别名,每个捕获从MyClass到对象的函数定义。例如,MyClass上的属性获取器就是这样的函数。

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

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

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

定义而言,基本上就是这样 - 那些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这里的代码我在调试时明智地使用"监视"窗口 - 这通常是我计算Expression树实际包含的内容的方式。

运行这将产生

EmployeeId
EmployeeNumber
True
True

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

提示。 使用名称。我有一个日期范围属性,用于验证两个属性并确保它们是有效的日期范围。

[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; }
}