.NET结构中的成员相等性测试使用的算法是什么?

时间:2009-11-05 13:29:46

标签: .net equality

.NET结构中的成员相等性测试使用的算法是什么?我想知道这一点,以便我可以将它作为我自己的算法的基础。

我正在尝试为任意对象(在C#中)编写递归成员相等性测试,以测试DTO的逻辑相等性。如果DTO是结构体,这是相当容易的(因为ValueType.Equals主要是正确的事情),但这并不总是合适的。我还想覆盖任何IEnumerable对象的比较(但不是字符串!),以便比较它们的内容而不是它们的属性。

事实证明,这比我预期的要难。任何提示将不胜感激。我会接受证明最有用的答案或提供最有用信息的链接。

感谢。

5 个答案:

答案 0 :(得分:13)

没有默认的成员相等,但对于基值类型(floatbytedecimal等),语言规范要求按位比较。 JIT优化器将此优化为适当的汇编指令,但从技术上讲,此行为等于C memcmp函数。

一些BCL示例

  • DateTime只是比较其内部InternalTicks成员字段,这是一个很长的字段;
  • PointF比较(left.X == right.X) && (left.Y == right.Y);
  • 中的X和Y.
  • Decimal不会比较内部字段,但会回退到InternalImpl,这意味着它位于内部不可查看的.NET部分(但您可以检查SSCLI);
  • Rectangle明确比较每个字段(x,y,width,height);
  • ModuleHandle使用其Equals覆盖,还有更多内容可以执行此操作;
  • SqlString和其他SqlXXX结构使用其IComparable.Compare实现;
  • Guid是此列表中最奇怪的:它有自己的短路长if-statements列表,比较每个内部字段(_a_k,所有int)对于不平等,在不平等时返回false。如果所有不是不相等的,则返回true。

结论

这个列表相当随意,但我希望它能够解决这个问题:没有可用的默认方法,甚至BCL也会根据其目的为每个结构使用不同的方法。底线似乎是后来添加更频繁地调用他们的Equals覆盖或Icomparable.Compare,但这只会将问题转移到另一种方法。

其他方式:

您可以使用反射来遍历每个字段,但这非常慢。您还可以创建单个扩展方法或静态帮助程序,在内部字段上进行逐位比较。使用StructLayout.Sequential,获取内存地址和大小,并比较内存块。这需要不安全的代码,但它快速,简单(有点脏)。

更新: 改述,添加了一些实际示例,添加了新结论


更新:成员比较的实施

以上显然是对这个问题的轻微误解,但我把它留在那里,因为我认为它对未来的访客有一些价值。这是一个更重要的答案:

这是对象和值类型的成员比较的实现,无论多深,都可以递归地遍历所有属性,字段和可枚举内容。它没有经过测试,可能包含一些拼写错误,但它编译得很好。有关更多详细信息,请参阅代码中的注释:

public static bool MemberCompare(object left, object right)
{
    if (Object.ReferenceEquals(left, right))
        return true;

    if (left == null || right == null)
        return false;

    Type type = left.GetType();
    if (type != right.GetType())
        return false;

    if(left as ValueType != null)
    {
        // do a field comparison, or use the override if Equals is implemented:
        return left.Equals(right);
    }

    // check for override:
    if (type != typeof(object)
        && type == type.GetMethod("Equals").DeclaringType)
    {
        // the Equals method is overridden, use it:
        return left.Equals(right);
    }

    // all Arrays, Lists, IEnumerable<> etc implement IEnumerable
    if (left as IEnumerable != null)
    {
        IEnumerator rightEnumerator = (right as IEnumerable).GetEnumerator();
        rightEnumerator.Reset();
        foreach (object leftItem in left as IEnumerable)
        {
            // unequal amount of items
            if (!rightEnumerator.MoveNext())
                return false;
            else
            {
                if (!MemberCompare(leftItem, rightEnumerator.Current))
                    return false;
            }                    
        }
    }
    else
    {
        // compare each property
        foreach (PropertyInfo info in type.GetProperties(
            BindingFlags.Public | 
            BindingFlags.NonPublic | 
            BindingFlags.Instance | 
            BindingFlags.GetProperty))
        {
            // TODO: need to special-case indexable properties
            if (!MemberCompare(info.GetValue(left, null), info.GetValue(right, null)))
                return false;
        }

        // compare each field
        foreach (FieldInfo info in type.GetFields(
            BindingFlags.GetField |
            BindingFlags.NonPublic |
            BindingFlags.Public |
            BindingFlags.Instance))
        {
            if (!MemberCompare(info.GetValue(left), info.GetValue(right)))
                return false;
        }
    }
    return true;
}

更新: 修复了一些错误,当且仅当可用时才添加使用被覆盖的Equals 更新: object.Equals不应被视为重写,已修复。

答案 1 :(得分:4)

这是共享源公共语言基础结构(版本2.0)中ValueType.Equals的实现。

public override bool Equals (Object obj) {
    BCLDebug.Perf(false, "ValueType::Equals is not fast.  "+
        this.GetType().FullName+" should override Equals(Object)");
    if (null==obj) {
        return false;
    }
    RuntimeType thisType = (RuntimeType)this.GetType();
    RuntimeType thatType = (RuntimeType)obj.GetType();

    if (thatType!=thisType) {
        return false;
    }

    Object thisObj = (Object)this;
    Object thisResult, thatResult;

    // if there are no GC references in this object we can avoid reflection 
    // and do a fast memcmp
    if (CanCompareBits(this))
        return FastEqualsCheck(thisObj, obj);

    FieldInfo[] thisFields = thisType.GetFields(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

    for (int i=0; i<thisFields.Length; i++) {
        thisResult = ((RtFieldInfo)thisFields[i])
            .InternalGetValue(thisObj, false);
        thatResult = ((RtFieldInfo)thisFields[i])
            .InternalGetValue(obj, false);

        if (thisResult == null) {
            if (thatResult != null)
                return false;
        }
        else
        if (!thisResult.Equals(thatResult)) {
            return false;
        }
    }

    return true;
}

有趣的是,这几乎就是Reflector中显示的代码。这让我很吃惊,因为我认为SSCLI只是一个参考实现,而不是最终的库。然后,我想再次实现这种相对简单的算法的方法有限。

我想要了解的部分更多是对CanCompareBitsFastEqualsCheck的调用。这些都是作为本机方法实现的,但它们的代码也包含在SSCLI中。从下面的实现中可以看出,CLI查看对象类的定义(通过它的方法表),以查看它是否包含指向引用类型的指针以及如何布置对象的内存。如果没有引用且对象是连续的,则使用C函数memcmp直接比较内存。

// Return true if the valuetype does not contain pointer and is tightly packed
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj != NULL);
    MethodTable* mt = obj->GetMethodTable();
    FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND

FCIMPL2(FC_BOOL_RET, ValueTypeHelper::FastEqualsCheck, Object* obj1,
    Object* obj2)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj1 != NULL);
    _ASSERTE(obj2 != NULL);
    _ASSERTE(!obj1->GetMethodTable()->ContainsPointers());
    _ASSERTE(obj1->GetSize() == obj2->GetSize());

    TypeHandle pTh = obj1->GetTypeHandle();

    FC_RETURN_BOOL(memcmp(obj1->GetData(),obj2->GetData(),pTh.GetSize()) == 0);
}
FCIMPLEND

如果我不是那么懒,我可能会研究ContainsPointersIsNotTightlyPacked的实现。但是,我已经明确地找到了我想知道的东西(而且我懒惰的),所以这是另一天的工作。

答案 2 :(得分:2)

这比眼睛更复杂。简短的回答是:

public bool MyEquals(object obj1, object obj2)
{
  if(obj1==null || obj2==null)
    return obj1==obj2;
  else if(...)
    ...  // Your custom code here
  else if(obj1.GetType().IsValueType)
    return
      obj1.GetType()==obj2.GetType() &&
      !struct1.GetType().GetFields(ALL_FIELDS).Any(field =>
       !MyEquals(field.GetValue(struct1), field.GetValue(struct2)));
  else
    return object.Equals(obj1, obj2);
}

const BindingFlags ALL_FIELDS =
  BindingFlags.Instance |
  BindingFlags.Public |
  BindingFlags.NonPublic;

然而,还有更多的东西。以下是详细信息:

如果声明一个结构并且不重写.Equals(),则.NET Framework将使用两种不同策略中的一种,具体取决于您的结构是否只有“简单”值类型(“简单”定义如下):

如果结构只包含“简单”值类型,则进行逐位比较,基本上是:

strncmp((byte*)&struct1, (byte*)&struct2, Marshal.Sizeof(struct1));

如果struct包含引用或非“简单”值类型,则每个声明的字段都与object.Equals()进行比较:

struct1.GetType()==struct2.GetType() &&
!struct1.GetType().GetFields(ALL_FIELDS).Any(field =>
  !object.Equals(field.GetValue(struct1), field.GetValue(struct2)));

什么是“简单”类型?从我的测试看,它似乎是任何基本的标量类型(int,long,decimal,double等),加上任何没有.Equals覆盖的结构,只包含“简单”类型(递归)。

这有一些有趣的后果。例如,在此代码中:

struct DoubleStruct
{
  public double value;
}

public void TestDouble()
{
  var test1 = new DoubleStruct { value = 1 / double.PositiveInfinity };
  var test2 = new DoubleStruct { value = 1 / double.NegativeInfinity };

  bool valueEqual = test1.value.Equals(test2.value);
  bool structEqual = test1.Equals(test2);

  MessageBox.Show("valueEqual=" + valueEqual + ", structEqual=" + structEqual);
}

无论分配给test1.value和test2.value的内容如何,​​您都希望valueEqual始终与structEqual相同。事实并非如此!

这个令人惊讶的结果的原因是double.Equals()考虑了IEEE 754编码的一些复杂性,例如多个NaN和零表示,但是按位比较则没有。因为“double”被认为是一个简单类型,所以当位数不同时,structEqual返回false,即使valueEqual返回true也是如此。

上面的示例使用了备用零表示,但这也可能出现多个NaN值:

...
  var test1 = new DoubleStruct { value = CreateNaN(1) };
  var test2 = new DoubleStruct { value = CreateNaN(2) };
...
public unsafe double CreateNaN(byte lowByte)
{
  double result = double.NaN;
  ((byte*)&result)[0] = lowByte;
  return result;
}

在大多数普通情况下,这不会产生任何影响,但需要注意的事项。

答案 3 :(得分:2)

这是我自己尝试解决这个问题。它有效,但我不相信我已经涵盖了所有的基础。

public class MemberwiseEqualityComparer : IEqualityComparer
{
    public bool Equals(object x, object y)
    {
        // ----------------------------------------------------------------
        // 1. If exactly one is null, return false.
        // 2. If they are the same reference, then they must be equal by
        //    definition.
        // 3. If the objects are both IEnumerable, return the result of
        //    comparing each item.
        // 4. If the objects are equatable, return the result of comparing
        //    them.
        // 5. If the objects are different types, return false.
        // 6. Iterate over the public properties and compare them. If there
        //    is a pair that are not equal, return false.
        // 7. Return true.
        // ----------------------------------------------------------------

        //
        // 1. If exactly one is null, return false.
        //
        if (null == x ^ null == y) return false;

        //
        // 2. If they are the same reference, then they must be equal by
        //    definition.
        //
        if (object.ReferenceEquals(x, y)) return true;

        //
        // 3. If the objects are both IEnumerable, return the result of
        //    comparing each item.
        // For collections, we want to compare the contents rather than
        // the properties of the collection itself so we check if the
        // classes are IEnumerable instances before we check to see that
        // they are the same type.
        //
        if (x is IEnumerable && y is IEnumerable && false == x is string)
        {
            return contentsAreEqual((IEnumerable)x, (IEnumerable)y);
        }

        //
        // 4. If the objects are equatable, return the result of comparing
        //    them.
        // We are assuming that the type of X implements IEquatable<> of itself
        // (see below) which is true for the numeric types and string.
        // e.g.: public class TypeOfX : IEquatable<TypeOfX> { ... }
        //
        var xType = x.GetType();
        var yType = y.GetType();
        var equatableType = typeof(IEquatable<>).MakeGenericType(xType);
        if (equatableType.IsAssignableFrom(xType)
            && xType.IsAssignableFrom(yType))
        {
            return equatablesAreEqual(equatableType, x, y);
        }

        //
        // 5. If the objects are different types, return false.
        //
        if (xType != yType) return false;

        //
        // 6. Iterate over the public properties and compare them. If there
        //    is a pair that are not equal, return false.
        //
        if (false == propertiesAndFieldsAreEqual(x, y)) return false;

        //
        // 7. Return true.
        //
        return true;
    }

    public int GetHashCode(object obj)
    {
        return null != obj ? obj.GetHashCode() : 0;
    }

    private bool contentsAreEqual(IEnumerable enumX, IEnumerable enumY)
    {
        var enumOfObjX = enumX.OfType<object>();
        var enumOfObjY = enumY.OfType<object>();

        if (enumOfObjX.Count() != enumOfObjY.Count()) return false;

        var contentsAreEqual = enumOfObjX
            .Zip(enumOfObjY) // Custom Zip extension which returns
                             // Pair<TFirst,TSecond>. Similar to .NET 4's Zip
                             // extension.
            .All(pair => Equals(pair.First, pair.Second))
            ;

        return contentsAreEqual;
    }

    private bool equatablesAreEqual(Type equatableType, object x, object y)
    {
        var equalsMethod = equatableType.GetMethod("Equals");
        var equal = (bool)equalsMethod.Invoke(x, new[] { y });
        return equal;
    }

    private bool propertiesAndFieldsAreEqual(object x, object y)
    {
        const BindingFlags bindingFlags
            = BindingFlags.Public | BindingFlags.Instance;

        var propertyValues = from pi in x.GetType()
                                         .GetProperties(bindingFlags)
                                         .AsQueryable()
                             where pi.CanRead
                             select new
                             {
                                 Name   = pi.Name,
                                 XValue = pi.GetValue(x, null),
                                 YValue = pi.GetValue(y, null),
                             };

        var fieldValues = from fi in x.GetType()
                                      .GetFields(bindingFlags)
                                      .AsQueryable()
                          select new
                          {
                              Name   = fi.Name,
                              XValue = fi.GetValue(x),
                              YValue = fi.GetValue(y),
                          };

        var propertiesAreEqual = propertyValues.Union(fieldValues)
            .All(v => Equals(v.XValue, v.YValue))
            ;

        return propertiesAreEqual;
    }
}

答案 4 :(得分:0)

public static bool CompareMembers<T>(this T source, T other, params Expression<Func<object>>[] propertiesToSkip)
{
    PropertyInfo[] sourceProperties = source.GetType().GetProperties();

    List<string> propertiesToSkipList = (from x in propertiesToSkip
                                         let a = x.Body as MemberExpression
                                         let b = x.Body as UnaryExpression
                                         select a == null ? ((MemberExpression)b.Operand).Member.Name : a.Member.Name).ToList();

    List<PropertyInfo> lstProperties = (
        from propertyToSkip in propertiesToSkipList
        from property in sourceProperties
        where property.Name != propertyToSkip
        select property).ToList();

    return (!(lstProperties.Any(property => !property.GetValue(source, null).Equals(property.GetValue(other, null)))));
}

使用方法:

bool test = myObj1.MemberwiseEqual(myObj2,
        () => myObj.Id,
        () => myObj.Name);