.NET结构中的成员相等性测试使用的算法是什么?我想知道这一点,以便我可以将它作为我自己的算法的基础。
我正在尝试为任意对象(在C#中)编写递归成员相等性测试,以测试DTO的逻辑相等性。如果DTO是结构体,这是相当容易的(因为ValueType.Equals主要是正确的事情),但这并不总是合适的。我还想覆盖任何IEnumerable对象的比较(但不是字符串!),以便比较它们的内容而不是它们的属性。
事实证明,这比我预期的要难。任何提示将不胜感激。我会接受证明最有用的答案或提供最有用信息的链接。感谢。
答案 0 :(得分:13)
没有默认的成员相等,但对于基值类型(float
,byte
,decimal
等),语言规范要求按位比较。 JIT优化器将此优化为适当的汇编指令,但从技术上讲,此行为等于C memcmp
函数。
DateTime
只是比较其内部InternalTicks
成员字段,这是一个很长的字段; PointF
比较(left.X == right.X) && (left.Y == right.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只是一个参考实现,而不是最终的库。然后,我想再次实现这种相对简单的算法的方法有限。
我想要了解的部分更多是对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
如果我不是那么懒,我可能会研究ContainsPointers
和IsNotTightlyPacked
的实现。但是,我已经明确地找到了我想知道的东西(而且我是懒惰的),所以这是另一天的工作。
答案 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);