web-dev-qa-db-ja.com

属性コンストラクターのラムダ式

AttributeというRelatedPropertyAttributeクラスを作成しました:

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

ラムダ式を使用して、「マジックストリング」ではなく、属性のコンストラクターに強い型を渡すことができます。このようにして、コンパイラの型チェックを活用できます。例えば:

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'は、型引数と一緒に使用できません

どうすればこれを達成できますか?

39
Dave New

できません

  • ジェネリック属性タイプを作成することはできません(単に許可されていません)。同様に、singジェネリック属性([Foo<SomeType>])が定義されています
  • 属性初期化子でラムダを使用することはできません-属性に渡すことができる値は非常に限られており、単純に式(非常に複雑で、ランタイムオブジェクトであり、コンパイル時リテラルではありません)を含みません
33
Marc Gravell

一般的な属性を持つことは、従来の方法では不可能です。ただし、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の限界を打ち破る別の方法を指摘したかっただけです。

このトピックに関する興味深い読み物があります チェックアウト この本。

それが役に立てば幸い。

53

C#6.0を使用している場合は、 nameof を使用できます

変数、型、またはメンバーの単純な(修飾されていない)文字列名を取得するために使用されます。コードのエラーを報告するとき、モデルビューコントローラー(MVC)リンクを接続するとき、プロパティ変更イベントを起動するときなどに、メソッドの文字列名をキャプチャしたいことがよくあります。 nameofを使用すると、定義の名前を変更するときにコードを有効に保つことができます。文字列リテラルを使用して定義を参照する必要があった前は、コード要素の名前を変更する場合、ツールはこれらの文字列リテラルをチェックすることを知らないため、脆弱です。

これを使用すると、次のように属性を使用できます。

public class MyClass
{
    public int EmployeeID { get; set; }

    [RelatedProperty(nameof(EmployeeID))]
    public int EmployeeNumber { get; set; }
}
13
Ayman

可能な回避策の1つは、各プロパティリレーションシップのクラスを定義し、それを参照することです。
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)
        {

        }
    }
}

できません。属性タイプは、書かれているように制限されます here 。私の提案では、ラムダ式を外部で評価してから、次のいずれかのタイプを使用してください。

  • 単純型(bool、byte、char、short、int、long、float、およびdouble)
  • ストリング
  • システムタイプ
  • 列挙型
  • オブジェクト(オブジェクト型の属性パラメーターの引数は、上記のいずれかの型の定数値でなければなりません。)
  • 上記のいずれかの1次元配列
5

my comment を展開すると、これは異なるアプローチでタスクを達成する方法です。 「クラスに関連するプロパティを示す」こと、「ラムダ式を使用して、属性のコンストラクタではなく強い型を渡すことができるようにしたい」と言います。魔法の文字列」。この方法でコンパイラの型チェックを活用できます "。

次に、compile-time typedであり、マジックストリングを持たない関連プロパティを示す方法を示します。

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が2つのTupleExpressionのエイリアスになり、それぞれがMyClassからオブジェクトへの関数の定義をキャプチャします。たとえば、MyClassのプロパティゲッターはそのような関数です。

次に、関係をキャプチャしましょう。ここではMyClassに静的プロパティを作成しましたが、このリストはどこでも定義できます。

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

C#コンパイラは、TuplesのExpressionを構築していることを知っているので、これらのラムダ式の前に明示的なキャストは必要ありません-それらは自動的に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);
        }
    }
}

ここでpropertyInfo1FromExpressionおよびpropertyInfo2FromExpressionのコードは、デバッグ中に[ウォッチ]ウィンドウを慎重に使用することで解決しました。これは通常、Expressionツリーに実際に含まれるものを解決する方法です.

これを実行すると

EmployeeId
EmployeeNumber
True
True

関連プロパティの詳細を正常に抽出できることを示し、(重要な)これらは他の手段で取得されたPropertyInfosと参照同一です。実行時に目的のプロパティを指定するために実際に使用しているアプローチと組み合わせてこれを使用できることを願っています。

4
AakashM

ヒント。 nameof を使用します。 2つのプロパティを検証し、それらが有効なDateRangeであることを確認するDateRangeAttributeがあります。

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