MemberInfo.DeclaringType的确切含义是什么?为什么通过表达式反射和不通过表达式反射之间不一致



根据使用的策略,提取PropertyInfo似乎会产生不一致的结果。下面是我遇到的问题的一个例子:让我们定义这个简单的类层次结构。

abstract class Top { public abstract int Count { get; set; } }
class BelowTop : Top { public override int Count { get; set; } }

我尝试使用反射提取属性Count

var info1 = typeof(BelowTop).GetProperty("Count");
Console.WriteLine("{0} -> {1}", info1.DeclaringType.Name, info1);
// BelowTop -> Int32 Count

这告诉我实际声明CCD_ 3的类型是具体类CCD_。

如果我现在尝试从表达式中提取这些信息:

Expression<Func<BelowTop, int>> lambda = b => b.Count;
var info2 = ((MemberExpression)lambda.Body).Member;
Console.WriteLine("{0} -> {1}", info2.DeclaringType.Name, info2);
// Top -> Int32 Count

反思突然不同意什么应该是相同性质的CCD_ 5。

如果我再加一层,我会得到同样的结果:

class Bottom : BelowTop { }
var info3 = typeof(Bottom).GetProperty("Count");
Console.WriteLine("{0} -> {1}", info3.DeclaringType.Name, info3);
// BelowTop -> Int32 Count
Expression<Func<Bottom, int>> lambda2 = b => b.Count;
var info4 = ((MemberExpression)lambda2.Body).Member;
Console.WriteLine("{0} -> {1}", info4.DeclaringType.Name, info4);
// Top -> Int32 Count

那是个虫子吗?如果不是,这背后的逻辑是什么?如果我不想关心这个差异,我如何比较两个PropertyInfo

这是个bug吗?

否。

如果不是,这背后的逻辑是什么?

对于一个明确的答案,只有.NET类型内省功能(即"反射")的设计者和/或C#的设计者可以说。然而,重要的是要认识到,您的两个场景实际上并不相同。在第一个示例中,您明确说明了要检查的类型。在第二个例子中,您将这个决定留给编译器,它选择了与您预期不同的东西。

至于编译器为什么这样做,IMHO,理解这两种场景之间的区别很重要。在第一部分中,您要求反射API告诉您关于特定派生类型BelowTop的一些信息。它通过为基类的一个成员声明一个新的实现来专门重写该成员。因此,您所询问的成员实际上是由BelowTop声明的。

另一方面,第二个场景涉及到对给定成员的虚拟调度调用。从编译器的角度来看,唯一重要的是调用虚拟调度表("v-table")中的哪个成员。从这个角度来看,感兴趣的成员是在Top类中声明的(即,该类实际上分配了v-table条目

毕竟,lambda主体为什么要关心传入的对象的精确类型?它不需要成为BelowTop实例。它可以是从该类型派生的实例(例如,第三个示例中的Bottom类型)。不管怎样,都会调用同一个虚拟成员,重要的是为其分配虚拟插槽的类型,所以这就是Expression捕获的类型。

事实上,也许您可以看到,在Expression的情况下,DeclaringType的结果返回BelowTop是多么具有误导性。如果您实际上将lambda与Bottom实例一起使用,并且该类型覆盖了Count属性,那么相对于Expression,报告的"声明类型"将是不准确的。唯一相关的结果是告诉您lambda表达式的行为,即它将调用Top中声明的虚拟成员。

当您准确地指定类型时,不会出现这种混乱,因为很明显,您想要了解确切的类型,并且在任何情况下,如果您确实想要的话,您都可以通过返回的方式从基类中收集额外的信息。

如果我不想关心这个差异,我如何比较两个PropertyInfo

这取决于"比较两个PropertyInfo">的意思。你想做什么样的比较?为什么?你期待什么结果?

最终,除非代码对场景的看法与编译器的看法一致,否则可能很难实现。

例如,虽然反射可以提供关于编写的原始代码的大量信息,但这是有局限性的。例如,它不能告诉您代码中包含的注释,也不能告诉您空格的具体排列。编译器对生成IL不感兴趣,并且在从源代码到IL的转换中也没有保留这些特性。

同样,当编译器被要求从lambda构建Expression时,它有一个非常具体的工作。这项工作是在.NET中使用ExpressionAPI来表示如果lamba实际上已经编译,则生成的代码。因此,除非您的代码对lambda的期望视图与此目标一致,即您愿意只关注与IL的代码生成相关的重要事项,否则检查代表lambda的Expression可能无法实现您想要实现的任何目标。在这种情况下,信息可能根本不存在。

也就是说,如果您的目标可以通过简单地确保同一个虚拟成员是被调用的虚拟成员来实现,而不管哪个类已经覆盖了它,那么在第一种情况下,即您从显式声明的Type对象中获取成员信息的情况下,您可以返回继承链,找到实际的声明类型,并将其与从表达式对象检索到的成员信息进行比较。

或者,如果只关心表达式的参数类型是可以的,则可以从Expression对象而不是表达式的Body访问它。


最后,我要注意的是,与您询问的字段或方法相比,您的示例有点特别复杂,因为属性和反射是如何交互的。特别是,因为属性实际上是两个独立的方法,所以反射可能会返回潜在的看似冲突的信息,这取决于您是查看成员的PropertyInfo还是MethodInfo

关于这个特定方面的更多信息,你可以找到以下问答;读起来很有帮助:
为什么C#副本中属性的密封重写不能重写基类型的访问器
对于具有重写访问器的属性,为什么CanRead和CanWrite在C#中返回false?

最新更新