14

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

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

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

谢谢。

4

5 回答 5

15

没有默认的成员相等性,但对于基值类型(float,bytedecimal),语言规范要求按位比较。JIT 优化器将其优化为正确的汇编指令,但从技术上讲,这种行为等同于 Cmemcmp函数。

一些 BCL 示例

  • DateTime只是比较它的内部InternalTicks成员字段,它是一个long;
  • PointF比较 X 和 Y,如(left.X == right.X) && (left.Y == right.Y);
  • Decimal不比较内部字段但回退到 InternalImpl,这意味着它位于内部不可见的 .NET 部分(但您可以检查 SSCLI);
  • Rectangle显式比较每个字段(x、y、宽度、高度);
  • ModuleHandle使用它的Equals覆盖,还有更多这样做;
  • SqlString和其他 SqlXXX 结构使用它的IComparable.Compare实现;
  • Guid是这个列表中最奇怪的:它有自己的短路长列表 if 语句比较每个内部字段(_ato _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不应被视为覆盖,已修复。

于 2009-11-05T13:36:42.397 回答
5

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

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 只是一个参考实现,而不是最终的库。再说一次,我想实现这个相对简单的算法的方法是有限的。

我想更多了解的部分是对CanCompareBits和的调用FastEqualsCheck。这些都是作为本地方法实现的,但它们的代码也包含在 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

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

于 2010-04-29T23:35:11.150 回答
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));

如果结构包含引用或非“简单”值类型,则将每个声明的字段与 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;
}

在大多数普通情况下,这不会产生影响,但需要注意这一点。

于 2009-11-05T14:57:02.203 回答
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;
    }
}
于 2009-11-06T16:17:53.627 回答
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);
于 2011-02-23T09:20:39.390 回答