这是.Net反射中的一个bug吗?

5

答案是:不,这不是一个bug。差别在于ReflectedType

那么真正的问题是:有没有一种方法可以比较两个PropertyInfo对象,这两个对象表示同一个属性,但是从不同类型反射而来,并返回true

原始问题

通过两种不同的方式,这段代码为完全相同的属性生成了两个PropertyInfo对象。结果是,这些属性信息在某种程度上进行了不同的比较。我已经浪费了一些时间来解决这个问题。

我做错了什么?

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace TestReflectionError
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.BufferWidth = 200;
            Console.WindowWidth = 200;

            Expression<Func<object>> expr = () => ((ClassA)null).ValueA;
            PropertyInfo pi1 = (((expr as LambdaExpression)
                .Body as UnaryExpression)
                .Operand as MemberExpression)
                .Member as PropertyInfo;

            PropertyInfo pi2 = typeof(ClassB).GetProperties()
                .Where(x => x.Name == "ValueA").Single();

            Console.WriteLine("{0}, {1}, {2}, {3}, {4}", pi1, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
            Console.WriteLine("{0}, {1}, {2}, {3}, {4}", pi2, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);

            // these two comparisons FAIL
            Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
            Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));

            // this comparison passes
            Console.WriteLine("pi1.DeclaringType == pi2.DeclaringType: {0}", pi1.DeclaringType == pi2.DeclaringType);
            Console.ReadKey();
        }
    }

    class ClassA
    {
        public int ValueA { get; set; }
    }

    class ClassB : ClassA
    {
    }
}

这里的输出为:
Int32 ValueA, TestReflectionError.ClassA, Property, 385875969, TestReflectionError.exe
Int32 ValueA, TestReflectionError.ClassA, Property, 385875969, TestReflectionError.exe
pi1 == pi2: False
pi1.Equals(pi2): False
pi1.DeclaringType == pi2.DeclaringType: True

罪魁祸首:PropertyInfo.ReflectedType

我发现这两个对象之间有所不同...就在于 ReflectedType。文档说:

获取用于获取此成员的类对象。


如果只执行 PropertyInfo pi1 = typeof(ClassA)...,你会得到相同的结果。有趣的是,如果将 ClassA 与 ClassA 或 ClassB 与 ClassB 进行比较,则所有结果都为 true。所以我必须同意这种行为似乎不正确。 - paparazzo
我应该发一个新问题来询问比较这些对象的可靠方法,还是应该编辑这个问题的标题?有什么意见吗? - Miguel Angelo
4个回答

4

永远不要假设库中有bug,除非你确实知道自己在做什么并且已经全面测试了该问题。

PropertyInfo对象没有相等的概念。虽然它们可能代表相同的结果,但它们不会重载==运算符,所以不能假设它们应该这样。因为它们不是,所以只是进行引用比较,而它们指向了两个不同的对象,因此为!=

另一方面,Type对象也没有重载==运算符,但似乎使用==运算符比较两个实例将起作用。为什么?因为类型实例实际上是单例实现的,这是一个实现细节。因此,给定对同一类型的两个引用,它们将按预期进行比较,因为你实际上是在比较对同一实例的引用。

不要期望调用框架方法时获得的每个对象都能以相同的方式工作。在框架中几乎没有使用单例。在执行之前,请检查所有相关文档和其他来源。


重新审视这个问题,我已被告知自.NET 4起,该类型实现了Equals()方法和==运算符。不幸的是,文档并没有很好地解释它们的行为,但使用像.NET Reflector这样的工具可以揭示一些有趣的信息。

根据反射器,在mscorlib程序集中实现这些方法的方式如下:

[__DynamicallyInvokable]
public override bool Equals(object obj)
{
    return base.Equals(obj);
}

[__DynamicallyInvokable]
public static bool operator ==(PropertyInfo left, PropertyInfo right)
{
    return (object.ReferenceEquals(left, right)
        || ((((left != null) && (right != null)) &&
             (!(left is RuntimePropertyInfo) && !(right is RuntimePropertyInfo)))
        && left.Equals(right)));
}

在继承链中上下移动(RuntimePropertyInfo -> PropertyInfo -> MemberInfo -> Object),Equals() 一路调用基本实现直到 Object,因此它实际上执行了对象引用相等比较。

== 运算符专门检查确保没有一个 PropertyInfo 对象是 RuntimePropertyInfo 对象。据我所知,在使用反射时(在此处显示的用例中),每个 PropertyInfo 对象都将返回 RuntimePropertyInfo

基于此,看起来框架设计人员认真地使 (运行时) PropertyInfo 对象不可比较,即使它们表示相同的属性。您只能检查属性是否引用相同的 PropertyInfo 实例。我不能告诉您为什么他们做出了这个决定(我有我的理论),您必须从他们那里听到这个决定的原因。


我曾经假设PropertyInfo对象的行为与Type对象相同。无论您如何获取类型的Type,它们始终是相同的对象。我认为对于属性信息也是如此。 - Miguel Angelo
我的意思是...如果所有的PropertyInfo都来自于一个Type,那么第二个实例是如何创建的呢?除非有另一种隐藏的方法来获取这些对象...这就是为什么我说这可能是一个bug。 - Miguel Angelo
1
-1: 加粗和斜体字让人误解 PropertyInfo 确实 重写了 Equals。声称 "PropertyInfo 对象没有相等的概念" 是完全错误的。另外,自从 .NET 4 开始,PropertyInfo 还定义了 ==!= 操作符。 - Sam Harwell
1
关于引用相等性是实现细节的另一个错误说法:我在ECMA-335中找不到它(但至少)自.NET 1.1以来,System.Type已经有了类似以下的注释:“表示类型的Type对象是唯一的;也就是说,仅当它们表示相同的类型时,两个Type对象引用才引用同一个对象。这允许使用引用相等性比较Type对象。” - Sam Harwell
我正在研究Mono的实现,它实际上确实实现了自定义的==和!=,因此加粗的语句是不正确的。它甚至在PropertyInfo中重写了Equals()方法。 - julx
显示剩余9条评论

4
为什么不直接比较MetadataToken和Module。
根据文档,这个组合可以唯一地标识一个元素。 MemberInfo.MetadataToken
一个值,与Module结合使用,可以唯一地标识一个元数据元素。
static void Main(string[] args)
{
    Console.BufferWidth = 200;
    Console.WindowWidth = 140;

    PropertyInfo pi1 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi2 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi0 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueB").Single();
    PropertyInfo pi3 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueB").Single();
    PropertyInfo pi4 = typeof(ClassC).GetProperties()
        .Where(x => x.Name == "ValueA").Single();
    PropertyInfo pi5 = typeof(ClassC).GetProperties()
        .Where(x => x.Name == "ValueB").Single();


    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi1, pi1.ReflectedType, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi2, pi2.ReflectedType, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi0, pi0.ReflectedType, pi0.DeclaringType, pi0.MemberType, pi0.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi3, pi3.ReflectedType, pi3.DeclaringType, pi3.MemberType, pi3.MetadataToken, pi3.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi4, pi4.ReflectedType, pi4.DeclaringType, pi4.MemberType, pi4.MetadataToken, pi4.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi5, pi5.ReflectedType, pi5.DeclaringType, pi5.MemberType, pi5.MetadataToken, pi5.Module);

    // these two comparisons FAIL
    Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
    Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));

    // this comparison passes
    Console.WriteLine("pi1.DeclaringType == pi2.DeclaringType: {0}", pi1.DeclaringType == pi2.DeclaringType);


    pi1 = typeof(ClassA).GetProperties()
        .Where(x => x.Name == "ValueB").Single();

    pi2 = typeof(ClassB).GetProperties()
        .Where(x => x.Name == "ValueB").Single();

    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi1, pi1.ReflectedType, pi1.DeclaringType, pi1.MemberType, pi1.MetadataToken, pi1.Module);
    Console.WriteLine("{0}, {1}, {2}, {3}, {4}, {5}", pi2, pi2.ReflectedType, pi2.DeclaringType, pi2.MemberType, pi2.MetadataToken, pi2.Module);

    // these two comparisons FAIL
    Console.WriteLine("pi1 == pi2: {0}", pi1 == pi2);
    Console.WriteLine("pi1.Equals(pi2): {0}", pi1.Equals(pi2));


    Console.ReadKey();
}
class ClassA
{
    public int ValueA { get; set; }
    public int ValueB { get; set; }
}
class ClassB : ClassA
{
    public new int ValueB { get; set; } 
}
class ClassC
{
    public int ValueA { get; set; }
    public int ValueB { get; set; }
}

2

我比较了DeclaringTypeName。这表明两个不同的泛型类型中的“相同”属性是不同的(例如,List<int>.CountList<string>.Count)。如果比较MetadataTokenModule,则会报告这两个属性是相同的。


0

一开始看起来,如果两个MemberInfo直接访问成员时返回相同的值,则它们相等似乎是有道理的(而不是通过反射)。对于FieldInfo来说,这似乎更合理。然而,对于PropertyInfo来说,情况并不那么清晰,因为属性可以在子类中扩展,并且可以向成员声明添加不同的CustomAttributes。这意味着仅考虑访问的值是不足以定义相等性的。但是,如果您想要这种相等性的定义,那么您可能需要考虑AreEqual3(...)方法:

private class Person {
    [CustomAttribute1]
    public virtual String Name { get; set; }
}

private class Person2 : Person {
    [CustomAttribute2]
    public override String Name { get; set; }
}

public static void TestMemberInfoEquality() {
    MemberInfo m1 = ExpressionEx.GetMemberInfo<Person>(p => p.Name);
    MemberInfo m2 = ExpressionEx.GetMemberInfo<Person2>(p => p.Name);
    bool b1 = m1.MetadataToken == m2.MetadataToken; // false
    bool b2 = m1 == m2; // false (because ReflectedType is different)
    bool b3 = m1.DeclaringType == m2.DeclaringType; // false
    bool b4 = AreEqual1(m1, m2); // false
    bool b5 = AreEqual2(m1, m2); // false
    bool b6 = AreEqual3(m1, m2); // true
}

public static bool AreEqual1(MemberInfo m1, MemberInfo m2) {
    return m1.MetadataToken == m2.MetadataToken && m1.Module == m2.Module;
}

public static bool AreEqual2(MemberInfo m1, MemberInfo m2) {
    return m1.DeclaringType == m2.DeclaringType && m1.Name == m2.Name;
}

public static bool AreEqual3(MemberInfo m1, MemberInfo m2) {
    return m1.GetRootDeclaration() == m2.GetRootDeclaration();
}

public static MemberInfo GetRootDeclaration(this MemberInfo mi) {
    Type ty = mi.ReflectedType;
    while (ty != null) {
        MemberInfo[] arr = ty.GetMember(mi.Name, mi.MemberType, BindingFlags.Instance | BindingFlags.Public);
        if (arr == null || arr.Length == 0)
            break;
        mi = arr[0];
        ty = ty.BaseType;
    }
    return mi;
}

该方法仅适用于PublicInstance成员。其他讨论线程建议使用AreEqual1(...)AreEqual2(...)方法,但对于给定的示例,它们返回false

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