关于C#:这是.NET反射中的错误吗?

Is this a bug in .Net reflection?

答案是:不,这不是一个错误。不同之处在于反射类型。

所以这里真正的问题是:有没有一种方法可以比较两个PropertyInfo对象,对于同一个属性,但从不同类型反映出来,从而返回true

原始问题

此代码通过使用两种不同的方法为同一属性生成两个PropertyInfo对象。结果是,这些属性信息在某种程度上比较不同。我花了一些时间想弄清楚这件事。

我做错什么了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
    {
    }
}

这里的输出是:

1
2
3
4
5
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

< BR>

罪魁祸首:PropertyInfo.ReflectedType

我发现这两个物体有区别…它在ReflectedType中。文件上说:

Gets the class object that was used to obtain this member.


不要假设库中有一个bug,除非你真正知道自己在做什么,并且已经彻底测试了这个问题。

PropertyInfo对象没有平等的概念。当然,它们可能代表相同的结果,但它们不会使==操作符过载,因此您不能假定它们应该如此。因为它们不是,所以只是做一个参考比较和猜测,它们指的是两个独立的对象,因此是!=

另一方面,Type对象也不会使==操作符过载,但似乎可以将两个实例与==操作符进行比较。为什么?因为类型实例实际上是作为单例实现的,这是一个实现细节。因此,对于同一类型的两个引用,它们将按预期进行比较,因为实际上您正在比较对同一实例的引用。

不要期望在调用框架方法时得到的每个对象都会以相同的方式工作。框架中使用单例的内容不多。检查所有相关文件和其他来源。

回顾这一点,我已经得知,从.NET 4开始,已经为该类型实现了Equals()方法和==运算符。不幸的是,文档并不能很好地解释它们的行为,但是使用诸如.NET Reflector之类的工具可以揭示一些有趣的信息。

根据Reflector,mscorlib程序集中方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[__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->ObjectEquals()一直调用基实现到Object,实际上是一个对象引用相等比较。

==操作符专门检查以确保PropertyInfo对象都不是RuntimePropertyInfo对象。据我所知,使用反射(在这里显示的用例中)得到的每个PropertyInfo对象都将返回RuntimePropertyInfo

基于这一点,框架设计者似乎认真地使它(运行时)PropertyInfo对象不可比较,即使它们代表相同的属性。您只能检查属性是否引用同一个PropertyInfo实例。我不能告诉你他们为什么做出这个决定(我有我的理论),你必须从他们那里听到。


我比较了DeclaringTypeName。这表明来自两个不同的通用类型的"相同"属性是不同的(例如,List.CountList.Count)。比较MetadataTokenModule会发现这两个属性是相同的。


为什么不比较metadatatoken和module呢?

根据组合唯一标识的文档。

memberinfo.metadatatoken一个值,与模块结合,唯一地标识一个元数据元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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; }
}

从一开始,如果两个MemberInfo在直接访问该成员(而不是通过反射)时返回相同的值,那么它们看起来是相等的,这似乎是合理的。对于FieldInfo来说,这似乎更合理。但是,对于PropertyInfo而言,还不太清楚,因为该财产可以在一个子类中扩展,并且可以在成员声明中添加不同的CustomAttributes。这意味着严格考虑访问值不足以定义相等。但是,如果这是你想要的平等的定义,那么你可以考虑采用AreEqual3(...)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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