.NET中如何深度判斷2個物件相等
背景
最近在群裡,有人問如何深度比較2個物件相等,感覺很有意思,就自己研究了一下,並寫了一個開源的小類庫,地址如下https://github.com/lamondlu/ObjectEquality。
如果想直接使用這個類庫,可以使用Nuget進行安裝
Install-Package ObjectEquality
物件比較有幾種情況
- 物件是值型別或者String,這裡僅需要判斷值是否相等
- 物件是Struct,需要判斷Struct的每個欄位是否一致
- 物件是集合,需要判斷對應位置的物件是否相等
- 物件是陣列,需要判斷對應位置的物件是否相等
- 物件是Class, 需要判斷Class的每個欄位是否一致
這裡可能有缺漏,大家可以幫我補充。
編寫程式碼
這裡我首先建立了一個IEquality
介面,在其中定義了一個IsEqual
方法,這個方法就是判斷2個物件是否一致的方法。後面我會針對上面說明的幾種對比場景,分別建立對應的實現類。
public interface IEquality
{
Func<object, bool> MatchCondition { get; }
bool IsEqual(object source, object target);
}
這裡MatchCondition
是一個委託,它定義了當前對比類的匹配條件。
第二步,我們針對上述的幾種對比場景,建立對應的實現類
值型別相等判斷實現類
internal class ValueTypeEquality : IEquality { public Func<object, bool> MatchCondition { get { return p => p.GetType().IsValueType || p.GetType() == typeof(string); } } public bool IsEqual(object source, object target) { return source.Equals(target); } }
值型別的判斷比較簡單,直接呼叫Object
類的Equals
方法即可。
String型別雖然不是值型別,但是這裡我們需要把它歸到值型別中。
Struct相等判斷實現類
internal class StructEquality : IEquality
{
public Func<object, bool> MatchCondition
{
get
{
return p => p.GetType().IsValueType
&& !p.GetType().IsPrimitive
&& !p.GetType().IsEnum;
}
}
public bool IsEqual(object source, object target)
{
var type = source.GetType();
foreach (var prop in type.GetProperties())
{
var equality = EqualityCollection.Equalities
.First(p => p.MatchCondition(prop.GetValue(source)));
var result = equality.IsEqual(prop.GetValue(source),
prop.GetValue(target));
if (!result)
{
return false;
}
}
return true;
}
}
這裡我們讀取了Struct中的每個屬性,分別進行判斷,如果有一個判斷失敗,即認為2個Struct物件不相等。
這裡
EqualityCollection
是判斷器集合,後續會新增這個類的程式碼。
集合相等判斷實現類
internal class GenericCollectionEquality : IEquality
{
public Func<object, bool> MatchCondition
{
get
{
return p => p.GetType().IsGenericType;
}
}
public bool IsEqual(object source, object target)
{
var type = source.GetType();
var genericType = type.GetGenericArguments()[0];
var genericCollectionType = typeof(IEnumerable<>).MakeGenericType(genericType);
if (type.GetInterfaces().Any(p => p == genericCollectionType))
{
var countMethod = type.GetMethod("get_Count");
var sourceCount = (int)countMethod.Invoke(source, null);
var targetCount = (int)countMethod.Invoke(target, null);
if (sourceCount != targetCount)
{
return false;
}
var sourceCollection = (source as IEnumerable<object>).ToList();
var targetCollection = (target as IEnumerable<object>).ToList();
for (var i = 0; i < sourceCount; i++)
{
var equality = EqualityCollection.Equalities.First(p => p.MatchCondition(sourceCollection[i]));
var result = equality.IsEqual(sourceCollection[i], targetCollection[i]);
if (!result)
{
return false;
}
}
}
return true;
}
}
這裡我們首先判斷了集合的元素的數量是否一致,如果不一致,即這2個集合不相等。如果一致,我們繼續判斷對應位置的每個元素是否一致,如果全部都一直,則2個集合相當,否則2個集合不相等。
陣列相等判斷實現類
internal class ArrayEquality : IEquality
{
public Func<object, bool> MatchCondition
{
get
{
return p => p.GetType().IsArray;
}
}
public bool IsEqual(object source, object target)
{
Array s = source as Array;
Array t = target as Array;
if (s.Length != t.Length)
{
return false;
}
for (var i = 0; i < s.Length; i++)
{
var equality = EqualityCollection.Equalities
.First(p => p.MatchCondition(s.GetValue(i)));
var result = equality.IsEqual(s.GetValue(i), t.GetValue(i));
if (!result)
{
return false;
}
}
return true;
}
}
陣列相等的判斷類似集合,我們首先判斷陣列的長度是否一致,然後判斷對應位置的元素是否一致。
類判斷相等實現類
internal class ClassEquality : IEquality
{
public Func<object, bool> MatchCondition
{
get
{
return p => p.GetType().IsClass;
}
}
public bool IsEqual(object source, object target)
{
var type = source.GetType();
foreach (var prop in type.GetProperties())
{
var equality = EqualityCollection.Equalities
.First(p => p.MatchCondition(prop.GetValue(source)));
var result = equality.IsEqual(prop.GetValue(source), prop.GetValue(target));
if (!result)
{
return false;
}
}
return true;
}
}
新增判斷相等實現類集合
public static class EqualityCollection
{
public static readonly List<IEquality> Equalities = new List<IEquality> {
new StructEquality(),
new ValueTypeEquality(),
new ArrayEquality(),
new GenericCollectionEquality(),
new ClassEquality()
};
}
這裡我們定義了一個靜態類,來儲存程式中使用的所有判斷器。
這裡在判斷器集合中,實現類的其實是有順序的,
StructEquality
必須要放到ValueTypeEquality
的前面,因為Struct也是值型別,如果不放到最前面,會導致判斷失敗。
建立判斷器入口類
public class ObjectEquality
{
public bool IsEqual(object source, object target)
{
if (source.GetType() != target.GetType())
{
return false;
}
if (source == null && target == null)
{
return true;
}
else if (source == null && target != null)
{
return false;
}
else if (source != null && target == null)
{
return false;
}
var equality = EqualityCollection.Equalities
.First(p => p.MatchCondition(source));
return equality.IsEqual(source, target);
}
}
前面所有實現類的訪問級別都是Internal
, 所以我們需要建立一個判斷器入口類, 外部只能通過ObjectEquality
類的例項來實現判斷相等。
最終效果
下面我列舉幾個測試用例,看看效果是不是我們想要的
對比Struct
public struct DemoStruct
{
public int Id { get; set; }
public string Name { get; set; }
}
var a = new DemoStruct();
a.Id = 1;
a.Name = "Test";
var b = new DemoStruct();
b.Id = 1;
b.Name = "Test";
var c = new DemoStruct();
b.Id = 2;
b.Name = "Test";
ObjectEquality objectEquality = new ObjectEquality();
objectEquality.IsEqual(a,b); //true
objectEquality.IsEqual(a,c); //false
對比類
public class SimpleClass
{
public int Id { get; set; }
public string Name { get; set; }
}
var a = new SimpleClass
{
Id = 1,
Name = "A"
};
var b = new SimpleClass
{
Id = 1,
Name = "A"
};
var c = new SimpleClass
{
Id = 2,
Name = "A"
};
ObjectEquality objectEquality = new ObjectEquality();
objectEquality.IsEqual(a,b); //true
objectEquality.IsEqual(a,c); //false
對比陣列
var a = new int[] { 1, 2, 3 };
var b = new int[] { 1, 2, 3 };
var c = new int[] { 1, 1, 2 };
ObjectEquality objectEquality = new ObjectEquality();
objectEquality.IsEqual(a,b); //true
objectEquality.IsEqual(a,c); //false