如何在C#中访问匿名类型的属性?

161

我有这个:

List<object> nodes = new List<object>(); 

nodes.Add(
new {
    Checked     = false,
    depth       = 1,
    id          = "div_" + d.Id
});

我想知道是否可以获取匿名对象的“Checked”属性。我不确定这是否可能。尝试了以下代码:

if (nodes.Any(n => n["Checked"] == false)),但它无法正常工作。

谢谢

5个回答

317

如果您将对象存储为object类型,则需要使用反射。这适用于任何对象类型,无论是匿名还是其他类型。在对象o上,您可以获取其类型:

Type t = o.GetType();

然后您可以查找一个属性:
PropertyInfo p = t.GetProperty("Foo");

然后,您可以得到一个值:

object v = p.GetValue(o, null);

这篇回答已经迟迟没有更新至C# 4,现在是时候进行更新了:

dynamic d = o;
object v = d.Foo;

现在介绍C# 6的另一种替代方案:

object v = o?.GetType().GetProperty("Foo")?.GetValue(o, null);

请注意,通过使用 ?.,我们会在三种不同的情况下导致结果 vnull
  1. onull,因此根本没有对象
  2. onull,但没有属性 Foo
  3. o 具有属性 Foo,但其实际值恰好为 null

因此,这与早期示例不等效,但如果您想要将所有三种情况视为相同,则可能是有意义的。

要在单元测试中使用 dynamic 读取匿名类型的属性,您需要告诉项目的编译器服务将程序集在内部对测试项目可见。 您可以将以下内容添加到项目 (.proj) 文件中。有关更多信息,请参见此链接

<ItemGroup>
    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
        <_Parameter1>Name of your test project</_Parameter1>
    </AssemblyAttribute>
</ItemGroup>

4
直到现在我从未使用过动态语言,.NET 4.0 的更新很不错。 - Alan
1
@ScottGartner 上面的动态示例适用于 ExpandoObject,例如 dynamic o = new ExpandoObject(); o.Foo = "Something"; - Daniel Earwicker
5
如果您在使用动态类型的程序集与源代码不同的情况下,则需要使用 [InternalsVisibleTo]。 - Sarath
2
@DanielEarwicker,感谢您的完成。这也适用于匿名类型。因为为匿名类型生成的所有属性都是内部属性。 - Sarath
2
如果你遇到了一个 RuntimeBinder.RuntimeBinderException 异常,它声称 'object' 不包含定义 some-property,那么你需要在你的 AssemblyInfo.cs 中添加一个 InternalsVisibleTo 属性,请参考 https://juristr.com/blog/2013/08/object-does-not-contain-definition/ 了解更多信息。 - Jijie Chen
显示剩余6条评论

72

如果你想要一个强类型的匿名类型列表,那么你需要将列表也变成一个匿名类型。最简单的方法是将一个序列(如数组)投影成一个列表,例如:

var nodes = (new[] { new { Checked = false, /* etc */ } }).ToList();

那么你就可以像这样访问它:

nodes.Any(n => n.Checked);

由于编译器的工作方式,一旦你创建了列表,下面的代码应该也能正常运行,因为匿名类型具有相同的结构,所以它们也是相同的类型。不过我手头没有编译器来验证这个。

nodes.Add(new { Checked = false, /* etc */ });

12
您可以使用反射(Reflection)迭代匿名类型的属性;查看是否有“Checked”属性,如果有,则获取其值。
请参阅此博客文章:http://blogs.msdn.com/wriju/archive/2007/10/26/c-3-0-anonymous-type-and-net-reflection-hand-in-hand.aspx 因此,代码可能如下所示:
foreach(object o in nodes)
{
    Type t = o.GetType();

    PropertyInfo[] pi = t.GetProperties(); 

    foreach (PropertyInfo p in pi)
    {
        if (p.Name=="Checked" && !(bool)p.GetValue(o))
            Console.WriteLine("awesome!");
    }
}

7
如果您只需要一个属性且已知其名称,那么浏览所有属性将毫无意义;只需使用 GetProperty 和 GetValue。另外,System.out.println 是 Java 语言而非 C#。 - Chris Charabaruk
哎呀,是这样的,克里斯!有点尴尬...现在已经修复了。 - glennkentwell

11

被接受的答案正确描述了列表应该如何声明,并且在大多数情况下强烈推荐使用。

但是我遇到了另一种情况,也涵盖了提出的问题。 如果您必须使用现有的对象列表,比如 ViewData["htmlAttributes"]MVC 中怎么办?如何访问其属性(通常通过 new { @style="width: 100px", ... } 创建)?

对于这种稍微不同的情况,我想与您分享我发现的内容。 在下面的解决方案中,我假设以下声明适用于nodes

List<object> nodes = new List<object>();

nodes.Add(
new
{
    Checked = false, depth = 1, id = "div_1" 
});

nodes.Add(
new
{
    Checked = true, depth = 2, id = "div_2" 
});

现在你有一个对象列表。如何访问对象内的属性,例如返回所有节点中Checked属性为false的节点列表?
1. 使用dynamic解决方案
在C# 4.0及更高版本中,你可以简单地将其转换为dynamic并编写:
if (nodes.Any(n => ((dynamic)n).Checked == false))
    Console.WriteLine("found a  not checked  element!");

注意:这是使用延迟绑定的方式,在运行时只会在对象没有Checked属性时才会识别出来,并在这种情况下引发RuntimeBinderException异常 - 因此,如果您尝试使用不存在的Checked2属性,您将在运行时获得以下消息:“'<>f__AnonymousType0<bool,int,string>'不包含 'Checked2' 的定义”

2. 反射解决方案

反射解决方案适用于旧版和新版 C# 编译器。对于旧版 C# 版本,请参考本答案末尾的提示。

背景

作为起点,我在这里找到了一个很好的答案here。其思路是通过使用反射将匿名数据类型转换为字典。由于属性名称存储为键(您可以像myDict["myProperty"]这样访问它们),因此使用字典可以轻松访问属性。

受到上面链接中的代码启发,我创建了一个扩展类,提供了GetPropUnanonymizePropertiesUnanonymizeListItems作为扩展方法,以简化对匿名属性的访问。使用这个类,你可以简单地进行如下查询:
if (nodes.UnanonymizeListItems().Any(n => (bool)n["Checked"] == false))
{
    Console.WriteLine("found a  not checked  element!");
}

或者你可以使用表达式nodes.UnanonymizeListItems(x => (bool)x["Checked"] == false).Any()作为if条件,它会隐式地过滤并检查是否有任何返回的元素。
要获取包含"Checked"属性的第一个对象,并返回其属性"depth",你可以使用:
var depth = nodes.UnanonymizeListItems()
             ?.FirstOrDefault(n => n.Contains("Checked")).GetProp("depth");

或者更简短一点:nodes.UnanonymizeListItems()?.FirstOrDefault(n => n.Contains("Checked"))?["depth"];

注意:如果你有一个对象列表,这些对象不一定包含所有属性(例如,有些对象不包含"Checked"属性),但你仍然想基于"Checked"值构建查询,你可以这样做:

if (nodes.UnanonymizeListItems(x => { var y = ((bool?)x.GetProp("Checked", true)); 
                                      return y.HasValue && y.Value == false;}).Any())
{
    Console.WriteLine("found a  not checked   element!");
}

这样做可以防止出现“KeyNotFoundException”异常,如果“Checked”属性不存在的话。
下面的类包含以下扩展方法:
  • UnanonymizeProperties:用于将对象中包含的属性“去匿名化”。此方法使用反射。它将对象转换为包含属性及其值的字典。
  • UnanonymizeListItems:用于将对象列表转换为包含属性的字典列表。可以选择在此之前包含一个用于过滤的lambda表达式。
  • GetProp:用于返回与给定属性名称匹配的单个值。允许将不存在的属性视为null值(true),而不是抛出KeyNotFoundException异常(false)。
对于上述示例,只需添加下面的扩展类即可。
public static class AnonymousTypeExtensions
{
    // makes properties of object accessible 
    public static IDictionary UnanonymizeProperties(this object obj)
    {
        Type type = obj?.GetType();
        var properties = type?.GetProperties()
               ?.Select(n => n.Name)
               ?.ToDictionary(k => k, k => type.GetProperty(k).GetValue(obj, null));
        return properties;
    }
    
    // converts object list into list of properties that meet the filterCriteria
    public static List<IDictionary> UnanonymizeListItems(this List<object> objectList, 
                    Func<IDictionary<string, object>, bool> filterCriteria=default)
    {
        var accessibleList = new List<IDictionary>();
        foreach (object obj in objectList)
        {
            var props = obj.UnanonymizeProperties();
            if (filterCriteria == default
               || filterCriteria((IDictionary<string, object>)props) == true)
            { accessibleList.Add(props); }
        }
        return accessibleList;
    }

    // returns specific property, i.e. obj.GetProp(propertyName)
    // requires prior usage of AccessListItems and selection of one element, because
    // object needs to be a IDictionary<string, object>
    public static object GetProp(this object obj, string propertyName, 
                                 bool treatNotFoundAsNull = false)
    {
        try 
        {
            return ((System.Collections.Generic.IDictionary<string, object>)obj)
                   ?[propertyName];
        }
        catch (KeyNotFoundException)
        {
            if (treatNotFoundAsNull) return default(object); else throw;
        }
    }
}

提示:上面的代码使用了C# 6.0版本以后提供的null条件运算符 - 如果你正在使用旧版的C#编译器(例如C# 3.0),只需将“?. ”替换为“。”,将“?[”替换为“[”,并在所有地方进行空值处理(通过使用if语句或捕获NullReferenceExceptions),例如。
var depth = nodes.UnanonymizeListItems()
            .FirstOrDefault(n => n.Contains("Checked"))["depth"];

正如您所见,如果没有空值条件运算符,处理空值将变得繁琐,因为无论您在何处删除它们,都必须添加空值检查 - 或者在很难找到异常根本原因的地方使用catch语句,从而导致更多且难以阅读的代码。

如果您不被迫使用旧版C#编译器,请保持原样,因为使用空值条件运算符可以使空值处理变得更容易。

注意:像使用dynamic的其他解决方案一样,此解决方案也使用了后期绑定,但是在这种情况下,如果引用的是不存在的属性,则不会抛出异常 - 只要保留null-conditional运算符,它就不会找到该元素。

对于某些应用程序可能有用的是,在解决方案2中,属性通过字符串引用,因此可以进行参数化。


2

最近,我在.NET 3.5中遇到了同样的问题(没有动态可用)。以下是我的解决方法:

// pass anonymous object as argument
var args = new { Title = "Find", Type = typeof(FindCondition) };

using (frmFind f = new frmFind(args)) 
{
...
...
}

这段内容源自stackoverflow的某个帖子:

// Use a custom cast extension
public static T CastTo<T>(this Object x, T targetType)
{
   return (T)x;
}

现在通过转换方式获取对象:
public partial class frmFind: Form
{
    public frmFind(object arguments)
    {

        InitializeComponent();

        var args = arguments.CastTo(new { Title = "", Type = typeof(Nullable) });

        this.Text = args.Title;

        ...
    }
    ...
}

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