通过字符串在对象图中查找属性

9

我将尝试使用任意字符串访问嵌套类结构的各个部分。

考虑以下(人为制造的)类:

public class Person
{
   public Address PersonsAddress { get; set; }
}

public class Adddress
{
   public PhoneNumber HousePhone { get; set; }
}

public class PhoneNumber
{
   public string Number { get; set; }
}

我希望能够从一个Person对象实例中获取"PersonsAddress.HousePhone.Number"处的对象。

目前,我正在使用反射进行一些奇怪的递归查找,但我希望有些高手能提供更好的想法。

以下是我开发的(糟糕的)方法:

private static object ObjectFromString(object basePoint, IEnumerable<string> pathToSearch)
{
   var numberOfPaths = pathToSearch.Count();

   if (numberOfPaths == 0)
     return null;

   var type = basePoint.GetType();
   var properties = type.GetProperties();

   var currentPath = pathToSearch.First();

   var propertyInfo = properties.FirstOrDefault(prop => prop.Name == currentPath);

   if (propertyInfo == null)
     return null;

   var property = propertyInfo.GetValue(basePoint, null);

   if (numberOfPaths == 1)
     return property;

   return ObjectFromString(property, pathToSearch.Skip(1));
}

你为什么认为你需要这样做? - Steve Wellens
@Steve - 因为我需要控制任意类型的投影,而配置是最好的地方。 - Khanzor
这对于实现通用的数据绑定机制也非常有用 - BindingSource的DataMember属性接受类似的导航路径字符串。 - ducu
5个回答

15

你可以直接使用标准的.NET DataBinder.Eval方法,如下所示:

object result = DataBinder.Eval(myPerson, "PersonsAddress.HousePhone.Number");

这可能更接近我寻找的无代码方法! - Khanzor
@Khanzor,你的方法和我的完全一样,我还是不明白你已经有了可行的答案,你在寻找什么替代方案?是在性能或其他方面吗?反射是唯一的方法,否则就只有生成动态方法并使用它的另一种选择,但对于小问题来说,这需要太多的编码工作。 - Akash Kava
1
请注意,您将需要引用 System.Web.dll - Maxim
3
请注意,您还需要放弃_.NET Framework 4客户端配置文件_:http://msdn.microsoft.com/zh-cn/library/cc656912.aspx - Rick Sladkey

5

过去我曾经做过类似的事情。我选择了lambda方法,因为编译后可以缓存它们。在这段代码中,我已经删除了缓存。

我包含了一些单元测试来展示该方法的使用。希望对你有所帮助。

private static object GetValueForPropertyOrField( object objectThatContainsPropertyName, IEnumerable<string> properties )
  {
     foreach ( var property in properties )
     {
        Type typeOfCurrentObject = objectThatContainsPropertyName.GetType();

        var parameterExpression = Expression.Parameter( typeOfCurrentObject, "obj" );
        Expression memberExpression = Expression.PropertyOrField( parameterExpression, property );

        var expression = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression.Type ), memberExpression, parameterExpression ).Compile();

        objectThatContainsPropertyName = expression.DynamicInvoke( objectThatContainsPropertyName );
     }

     return objectThatContainsPropertyName;
  }

  [TestMethod]
  public void TestOneProperty()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime, new[] { "Day" } );

     Assert.AreEqual( dateTime.Day, result );
  }

  [TestMethod]
  public void TestNestedProperties()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime,  new[] { "Date", "Day" } );

     Assert.AreEqual( dateTime.Date.Day, result );
  }

  [TestMethod]
  public void TestDifferentNestedProperties()
  {
     var dateTime = new DateTime();

     var result = GetValueForPropertyOrField( dateTime, new[] { "Date", "DayOfWeek" } );

     Assert.AreEqual( dateTime.Date.DayOfWeek, result );
  }

最大的原因是表达式利用属性的AST,而反射则不会。唯一更快的方法是使用Reflection.Emit并使用IL编写,但这似乎比它值得麻烦。请注意,如果您可以缓存.Compile()步骤生成的委托,将有助于保持执行时间较低。您需要使用属性和类型来进行缓存,以检索要调用的委托。 - Brian Dishaw
我认为这篇文章可能会更好地解释这个问题,因为我不确定我做得很好。http://stackoverflow.com/questions/2697655/lambda-expression-based-reflection-vs-normal-reflection - Brian Dishaw
我喜欢你的回答,但它不支持数组或列表的属性。所以我稍微修改了一下,使其支持[index]: - yww325

3
这是一个非递归版本,几乎具有相同的语义:

private static object ObjectFromString(object basePoint, IEnumerable<string> pathToSearch)
{
    var value = basePoint;
    foreach (var propertyName in pathToSearch)
    {
        var property = value.GetType().GetProperty(propertyName);
        if (property == null) return null;
        value = property.GetValue(value, null);
    }
    return value;
}

非递归实现为什么一定更好?不过,GetProperty 方法是一个不错的提示。 - Khanzor
1
@Khanzor:递归防止我们在传入的IEnumerable上使用自然foreach迭代器。这就是foreach的作用! - Rick Sladkey
好吧,我想我只是为了递归而递归。不过,我认为对于列表切片并没有太多问题。不过,我明白你的观点,CLR不支持尾递归,因此使用foreach更有意义 :)。 - Khanzor

1

既然您已经对解决字符串属性路径感兴趣,那么您可能会从微软的Scott Guthrie发布的Dynamic LINQ查询库中受益。它解析您的字符串表达式并生成可以编译和缓存的表达式树,正如@Brian Dishaw所建议的那样。

这将为您提供丰富的附加选项,通过提供一个简单而强大的表达式语法,您可以在配置方法中使用它。它支持可枚举的常见LINQ方法,以及简单的运算逻辑、数学计算、属性路径评估等。


0

这是基于Brian的代码,做了一些修改以支持List的索引寻址:

private static object GetValueForPropertyOrField( object objectThatContainsPropertyName, IEnumerable<string> properties )
       {
           foreach ( var property in properties )
           {
               Type typeOfCurrentObject = objectThatContainsPropertyName.GetType();

               var parameterExpression = Expression.Parameter( typeOfCurrentObject, "obj" );
               var arrayIndex = property.IndexOf('[');
               if ( arrayIndex > 0)
               {
                   var property1 = property.Substring(0, arrayIndex);
                   Expression memberExpression1 = Expression.PropertyOrField( parameterExpression, property1 );
                   var expression1 = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression1.Type ), memberExpression1, parameterExpression ).Compile();
                   objectThatContainsPropertyName = expression1.DynamicInvoke( objectThatContainsPropertyName );
                   var index = Int32.Parse(property.Substring(arrayIndex+1, property.Length-arrayIndex-2));
                   typeOfCurrentObject = objectThatContainsPropertyName.GetType(); 
                   parameterExpression = Expression.Parameter( typeOfCurrentObject, "list" );
                   Expression memberExpression2 =  Expression.Call(parameterExpression, typeOfCurrentObject.GetMethod("get_Item"), new Expression[] {Expression.Constant(index)});
                   var expression2 = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression2.Type ), memberExpression2, parameterExpression ).Compile();
                    objectThatContainsPropertyName = expression2.DynamicInvoke( objectThatContainsPropertyName );
               }
               else
               {
                   Expression memberExpression = Expression.PropertyOrField( parameterExpression, property );  
                   var expression = Expression.Lambda( Expression.GetDelegateType( typeOfCurrentObject, memberExpression.Type ), memberExpression, parameterExpression ).Compile(); 
                   objectThatContainsPropertyName = expression.DynamicInvoke( objectThatContainsPropertyName );
               }

           }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接