如何在C#中迭代匿名对象的属性?

66
我想将一个匿名对象作为参数传递给一个方法,然后迭代它的属性,将每个属性/值添加到动态的ExpandoObject中。
所以我需要做的是从
new { Prop1 = "first value", Prop2 = SomeObjectInstance, Prop3 = 1234 }

如何知道每个属性的名称和值,并能够将它们添加到 ExpandoObject 中呢?

我该怎么做呢?

顺便说一句:我会在很多单元测试中使用它(我正在使用它来重构设置中的许多垃圾),因此性能在某种程度上是相关的。我不太了解反射,但从我的理解来看,它的性能非常高,所以如果可能的话,我想避免使用它……

后续问题: 正如我所说,我将这个匿名对象作为参数传递给一个方法。在方法签名中应该使用什么数据类型?如果使用 object,所有属性都将可用吗?


1
反射性能并不太差。如果你需要对大量实例进行操作,可以为给定的匿名类型缓存PropertyInfo条目,然后重新迭代这些PropertyInfo条目以解析每个实例的属性。甚至可以为每个属性的GetMethod创建委托并缓存它们。 - Dan Bryant
7个回答

87
foreach(var prop in myVar.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
   Console.WriteLine("Name: {0}, Value: {1}",prop.Name, prop.GetValue(myVar,null));
}

好的,我必须使用反射。这会影响性能吗?另外,我如何获取属性的名称以便于将其添加到ExpandoObject中? - Tomas Aschan
3
我解决了后续问题:“(MyExpandoObject as ICollection<KeyValuePair<string, object>>).Add(...)”做到了这一点。谢谢! - Tomas Aschan
原则上,不应该需要使用反射来完成这个任务。如果提供了一个编译时循环功能,就可以更高效地实现相同的结果,并且内省可以限制在编译时进行。 - isekaijin
纯天才!谢谢 :) - Stronghold

9

通过反射匿名对象以获取其属性名称和值,然后利用ExpandoObject实际上是一个字典来填充它。以下是一个示例,表示为单元测试:

    [TestMethod]
    public void ShouldBeAbleToConvertAnAnonymousObjectToAnExpandoObject()
    {
        var additionalViewData = new {id = "myControlId", css = "hide well"};
        dynamic result = new ExpandoObject();
        var dict = (IDictionary<string, object>)result;
        foreach (PropertyInfo propertyInfo in additionalViewData.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            dict[propertyInfo.Name] = propertyInfo.GetValue(additionalViewData, null);
        }
        Assert.AreEqual(result.id, "myControlId");
        Assert.AreEqual(result.css, "hide well");
    }

4

另一种方法是使用 DynamicObject 而不是 ExpandoObject,这样只有在实际尝试从另一个对象访问属性时才需要进行反射的开销。

public class DynamicForwarder : DynamicObject 
{
    private object _target;

    public DynamicForwarder(object target)
    {
        _target = target;
    }

    public override bool TryGetMember(
        GetMemberBinder binder, out object result)
    {
        var prop = _target.GetType().GetProperty(binder.Name);
        if (prop == null)
        {
            result = null;
            return false;
        }

        result = prop.GetValue(_target, null);
        return true;
    }
}

现在只有在通过动态get访问属性时才进行反射。不足之处在于,如果多次访问同一属性,则每次都必须进行反射。因此,您可以缓存结果:
public class DynamicForwarder : DynamicObject 
{
    private object _target;
    private Dictionary<string, object> _cache = new Dictionary<string, object>();

    public DynamicForwarder(object target)
    {
        _target = target;
    }

    public override bool TryGetMember(
        GetMemberBinder binder, out object result)
    {
        // check the cache first
        if (_cache.TryGetValue(binder.Name, out result))
            return true;

        var prop = _target.GetType().GetProperty(binder.Name);
        if (prop == null)
        {
            result = null;
            return false;
        }

        result = prop.GetValue(_target, null);
        _cache.Add(binder.Name, result); // <-------- insert into cache
        return true;
    }
}

你可以支持存储目标对象列表来合并它们的属性,并支持设置属性(使用类似覆盖的TrySetMember方法),允许您在缓存字典中动态设置值。当然,反射的开销可能不值得担心,但对于大型对象,这可能会限制其影响。更有趣的是它给你带来的额外灵活性。

1
这是一个旧问题,但现在您应该能够使用以下代码来实现:
dynamic expObj = new ExpandoObject();
    expObj.Name = "James Kirk";
    expObj.Number = 34;

// print the dynamically added properties
// enumerating over it exposes the Properties and Values as a KeyValuePair
foreach (KeyValuePair<string, object> kvp in expObj){ 
    Console.WriteLine("{0} = {1} : Type: {2}", kvp.Key, kvp.Value, kvp.Value.GetType());
}

输出将如下所示:

名称 = James Kirk :类型:System.String

数字 = 34 :类型:System.Int32


1
问题是关于从“匿名”类型开始;这有其自身的好处。首先使用ExpandoObject并不是OP所寻求的基本方法。也许他们无法控制对象的创建方式。 - xanadont

0

你必须使用反射……(从此网址“借用”的代码)

using System.Reflection;  // reflection namespace

// get all public static properties of MyClass type
PropertyInfo[] propertyInfos;
propertyInfos = typeof(MyClass).GetProperties(BindingFlags.Public |
                                              BindingFlags.Static);
// sort properties by name
Array.Sort(propertyInfos,
        delegate(PropertyInfo propertyInfo1, PropertyInfo propertyInfo2)
        { return propertyInfo1.Name.CompareTo(propertyInfo2.Name); });

// write property names
foreach (PropertyInfo propertyInfo in propertyInfos)
{
  Console.WriteLine(propertyInfo.Name);
}

好的 - 我必须使用反射。如果我在大多数单元测试中都这样做一次,这会成为性能问题吗? - Tomas Aschan
1
@Tomas:没有足够的信息来回答那种问题。只需尝试并查看结果。 - Adam Robinson

0
使用Reflection.Emit创建一个通用方法来填充ExpandoObject。
或者使用表达式(我认为这只可能在.NET 4中实现)。
这两种方法都不会在调用时使用反射,只在委托设置期间使用(显然需要缓存)。
以下是一些Reflection.Emit代码来填充字典(我猜ExpandoObject也差不多);
static T CreateDelegate<T>(this DynamicMethod dm) where T : class
{
  return dm.CreateDelegate(typeof(T)) as T;
}

static Dictionary<Type, Func<object, Dictionary<string, object>>> cache = 
   new Dictionary<Type, Func<object, Dictionary<string, object>>>();

static Dictionary<string, object> GetProperties(object o)
{
  var t = o.GetType();

  Func<object, Dictionary<string, object>> getter;

  if (!cache.TryGetValue(t, out getter))
  {
    var rettype = typeof(Dictionary<string, object>);

    var dm = new DynamicMethod(t.Name + ":GetProperties", rettype, 
       new Type[] { typeof(object) }, t);

    var ilgen = dm.GetILGenerator();

    var instance = ilgen.DeclareLocal(t);
    var dict = ilgen.DeclareLocal(rettype);

    ilgen.Emit(OpCodes.Ldarg_0);
    ilgen.Emit(OpCodes.Castclass, t);
    ilgen.Emit(OpCodes.Stloc, instance);

    ilgen.Emit(OpCodes.Newobj, rettype.GetConstructor(Type.EmptyTypes));
    ilgen.Emit(OpCodes.Stloc, dict);

    var add = rettype.GetMethod("Add");

    foreach (var prop in t.GetProperties(
      BindingFlags.Instance |
      BindingFlags.Public))
    {
      ilgen.Emit(OpCodes.Ldloc, dict);

      ilgen.Emit(OpCodes.Ldstr, prop.Name);

      ilgen.Emit(OpCodes.Ldloc, instance);
      ilgen.Emit(OpCodes.Ldfld, prop);
      ilgen.Emit(OpCodes.Castclass, typeof(object));

      ilgen.Emit(OpCodes.Callvirt, add);
    }

    ilgen.Emit(OpCodes.Ldloc, dict);
    ilgen.Emit(OpCodes.Ret);

    cache[t] = getter = 
      dm.CreateDelegate<Func<object, Dictionary<string, object>>>();
  }

  return getter(o);
}

0
在ASP.NET中,您可以使用RouteValueDictionary类将匿名变量快速转换为属性字典。
它内部也使用反射,但还维护了一个内部缓存的属性。因此,后续调用将更快(proof)。
所以如果您正在编写ASP.NET应用程序,那么您可以使用它。
var d = new RouteValueDictionary(new { A = 1, B = 2 });
d["A"] //equals 1
d["B"] //equals 2

顺便说一句,每次编写类似以下 ASP.NET 代码时都会使用到这个类:

Url.Action("Action, "Controller", new { parameter = xxx })

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