C# - 为扩展方法提供类型信息

C# - Provide type information to extension method

我们目前正在对大量使用注释和扩展方法的应用程序进行一些性能调整,例如:

public static TClass TruncateToMaxLength<TClass>(this TClass inClass)
{
   foreach (PropertyInfo classProp in typeof(TClass).GetProperties())
   var maxLengthAttribute = classProp.GetCustomMetaDataAttribute<MaxLengthAttribute>();
   if (null != maxLengthAttribute)
   {
     int maxLength = maxLengthAttribute.MaximumLength;
     var inClassProp = classProp.GetValue(inClass);
     if (null != inClassProp)
     {
       var strProp = inClassProp.ToString();
       classProp.SetValue(inClass, 
         strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
     }
   }
}

做一些事情,比如自动 trim 生成字符串到其关联属性的 [MaxLength()] 属性。在某些情况下,我们正在考虑切换到手动实现以避免反射开销,因此:

public class SimplePerson : IManualMaxLength {
    [MaxLength(10)]
    public IdNumber { get; set; }
    [MaxLength(100)]
    public DisplayName { get; set; }

    public void ManualMaxLength() {
       if (IdNumber?.Length > 10) IdNumber = IdNumber.Substring(0, 10);
       if (DisplayName?.Length > 100) DisplayName = DisplayName.Substring(0, 100);
    }
}

public static TClass OptimizedMaxLength<TClass>(this TClass inClass)
{
   if (inClass is IManualMaxLength manualClass) manualClass.ManualMaxLength();
   else inClass.TruncateToMaxLength();
   return inClass;
}

其中手动操作是作为 class 方法实现的,没有反射。

为了验证所有手动操作是否与反射操作相匹配,我想到了按照以下几行设置测试装置,以自动检查所有优化工作:

foreach (var manualType in (
    from x in Assembly.GetAssembly(typeof(IManualMaxLength)).GetTypes()
    where x.GetInterfaces().Contains(typeof(IManualMaxLength)) 
    select x))
{
    var implManual = (IManualOperation)Activator.CreateInstance(manualType);
    var implAuto = Activator.CreateInstance(manualType);

    //some setup of properties

    implManual.ManualMaxLength();
    implAuto.TruncateToMaxLength();

    //assert rig
}

钻机遍历程序集并获取实现 IManualMaxLength 的所有类型;然后它使用 Activator.CreateInstance() 来设置测试用例,并将手动实现与通过反射获得的预期结果进行比较。

implAutoSimplePerson 时,我希望在 TruncateToMaxLength 中使用它来使测试装置准确。但是正在为 objectTClass 调用。这就是调用 Activator.CreateInstance() 的结果,但它没有任何属性。

现在,要执行设置和断言,我必须调用 manualType.GetProperties(),并且我可以按如下方式创建重载:

public static TClass TruncateToMaxLength<TClass>
   (this TClass inClass, PropertyInfo[] testProperties = null)
{
   var propertyInfo = testProperties ?? typeof(TClass).GetProperties();
   foreach (PropertyInfo classProp in propertyinfo)
   var maxLengthAttribute = classProp.GetCustomMetaDataAttribute<MaxLengthAttribute>();
   if (null != maxLengthAttribute)
   {
     int maxLength = maxLengthAttribute.MaximumLength;
     var inClassProp = classProp.GetValue(inClass);
     if (null != inClassProp)
     {
       var strProp = inClassProp.ToString();
       classProp.SetValue(inClass, 
         strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
     }
   }
}

但我真的不想在正常操作期间从未打算采用的扩展方法上执行此操作。

我是否一直坚持这样做,或者是否有其他方法以扩展方法将拾取它的方式提供已创建类型的元素?

鉴于应用程序的当前状态,您无法避免传递 testProperties 重载。问题是扩展方法采用任何 object,但该方法的实际用途是使用更派生的类型,可以从中反映出属性。 Activator.CreateInstance() 始终 returns 一个 object 需要转换为更派生的类型(在遍历具有这些属性的所有类型时这是未知的)。

您必须找到一种方法来为每个具体实例公开要转换为的类型,这将是大量手动的、相当一次性的工作。

我认为您的 testProperties 过载是最好的解决方法。鉴于扩展方法将在很长一段时间内消失 运行 一旦所有内容都转换为更有效的手动检查,我现在不会担心它存在。

TruncateToMaxLength<TClass> 中,您可以使用 inClass.GetType().GetProperties() 而不是 typeof(TClass).GetProperties

public static void TruncateToMaxLength<TClass>(this TClass inClass)
{
    foreach (PropertyInfo classProp in inClass.GetType().GetProperties()) // This Line
    {
        var maxLengthAttribute = classProp.GetCustomAttribute<MaxLengthAttribute>();
        if (null != maxLengthAttribute)
        {
            int maxLength = maxLengthAttribute.MaximumLength;
            var inClassProp = classProp.GetValue(inClass);
            if (null != inClassProp)
            {
                var strProp = inClassProp.ToString();
                classProp.SetValue(inClass,
                    strProp.Length > maxLength ? strProp.Substring(0, maxLength) : strProp);
            }
        }
    }
}

使用您的 SimplePerson 案例,这似乎对我有用。需要注意的一件事是,如果这样做会导致 TruncateToMaxLengthinClass 类型的属性上执行。所以,如果你有类似...

public interface IHaveAnId
{
   [MaxLength(5)]
   string Id { get; set; }
}

public class SimplePerson : IHaveAnId
{
   [MaxLength(10)]
   public string Id { get; set; }
   [MaxLength(100)]
   public string DisplayName { get; set; }
}

public void Main()
{
   IHaveAnId s = new SimplePerson();
   s.TruncateToMaxLength(); 
}

然后对 s.TruncateToMaxLength 的调用将对 SimplePerson 的所有属性进行操作,并对 class 的属性使用 MaxLength 属性。

顺便说一句,我不知道你的性能要求是什么,但你可以加快速度TruncateToMaxLength<TClass>。它永远不会像你的 ManualMaxLength 那样快(至少我还不够聪明,无法通过反射获得那么快的东西),但是你可以通过缓存 PropertyInfo 实例和 MaxLength 值。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

public static class Extensions
{
   public static void TruncateToMaxLengthCached<TClass>(this TClass input)
   {
      var type = typeof(TClass);
      var props = _cache.GetOrAdd(type, t =>
      {
         return new Lazy<IReadOnlyCollection<MaxLengthData>>(() => BuildData(input));
      }).Value;
      foreach (var data in props)
      {
         var value = data.Property.GetValue(input)?.ToString();
         if (value?.Length > data.MaxLength)
         {
            data.Property.SetValue(input, value.Substring(0, data.MaxLength));
         }
      }
   }

   private static IReadOnlyCollection<MaxLengthData> BuildData<TClass>(TClass input)
   {
      Type type = typeof(TClass);
      var result = new List<MaxLengthData>();
      foreach (var prop in type.GetProperties())
      {
         var maxLengthAttribute = prop.GetCustomAttribute<MaxLengthAttribute>();
         if (null != maxLengthAttribute)
         {
            result.Add(new MaxLengthData
            {
               MaxLength = maxLengthAttribute.Length,
               Property = prop,
               TargetType = type
            });
         }
      }
      return result;
   }

   private static ConcurrentDictionary<Type, Lazy<IReadOnlyCollection<MaxLengthData>>> _cache =
      new ConcurrentDictionary<Type, Lazy<IReadOnlyCollection<MaxLengthData>>>();
   private class MaxLengthData
   {
      public Type TargetType { get; set; }
      public PropertyInfo Property { get; set; }
      public int MaxLength { get; set; }
   }
}

BenchmarkDotNet 结果:

|           Method |             Id |                  Name |          Mean |       Error |      StdDev | Rank |
|----------------- |--------------- |---------------------- |--------------:|------------:|------------:|-----:|
|   ManualTruncate | 09123456789093 |          John Johnson |     28.103 ns |   0.6188 ns |   0.8046 ns |    2 |
| OriginalTruncate | 09123456789093 |          John Johnson | 17,953.005 ns | 356.7870 ns | 534.0220 ns |    8 |
|   CachedTruncate | 09123456789093 |          John Johnson |    697.548 ns |  13.6592 ns |  13.4152 ns |    6 |
|   ManualTruncate | 09123456789093 |  Mr. J(...), Esq [98] |     59.177 ns |   1.2251 ns |   1.5494 ns |    4 |
| OriginalTruncate | 09123456789093 |  Mr. J(...), Esq [98] | 18,333.251 ns | 365.0699 ns | 461.6966 ns |    8 |
|   CachedTruncate | 09123456789093 |  Mr. J(...), Esq [98] |    995.924 ns |  19.9356 ns |  23.7319 ns |    7 |
|   ManualTruncate | 09123456789093 | Mr. J(...)hnson [111] |     58.787 ns |   0.4812 ns |   0.4501 ns |    4 |
| OriginalTruncate | 09123456789093 | Mr. J(...)hnson [111] | 18,032.030 ns | 220.0009 ns | 195.0251 ns |    8 |
|   CachedTruncate | 09123456789093 | Mr. J(...)hnson [111] |    977.168 ns |  19.2770 ns |  27.6465 ns |    7 |
|   ManualTruncate |              1 |          John Johnson |      6.800 ns |   0.2039 ns |   0.2651 ns |    1 |
| OriginalTruncate |              1 |          John Johnson | 18,173.803 ns | 192.1153 ns | 170.3052 ns |    8 |
|   CachedTruncate |              1 |          John Johnson |    410.136 ns |   3.8655 ns |   3.6158 ns |    5 |
|   ManualTruncate |              1 |  Mr. J(...), Esq [98] |     34.886 ns |   0.7203 ns |   0.6385 ns |    3 |
| OriginalTruncate |              1 |  Mr. J(...), Esq [98] | 18,013.630 ns | 327.2057 ns | 306.0684 ns |    8 |
|   CachedTruncate |              1 |  Mr. J(...), Esq [98] |    684.351 ns |  12.0877 ns |  11.3069 ns |    6 |
|   ManualTruncate |              1 | Mr. J(...)hnson [111] |     34.285 ns |   0.6136 ns |   0.5124 ns |    3 |
| OriginalTruncate |              1 | Mr. J(...)hnson [111] | 17,926.434 ns | 184.0216 ns | 172.1340 ns |    8 |
|   CachedTruncate |              1 | Mr. J(...)hnson [111] |    685.590 ns |   9.6743 ns |   9.0493 ns |    6 |