对象比较器






4.96/5 (16投票s)
用于.Net的对象比较器
引言
当需要比较复杂对象时,这是一个非常常见的情况。 有时对象可以包含嵌套元素,或者某些成员应从比较中排除(自动生成的标识符,创建/更新日期等),或者某些成员可以具有自定义比较规则(相同数据以不同的格式,例如电话号码)。 为了解决此类问题,我开发了一个小型框架来比较对象。
简而言之,对象比较器是一个对象到对象的比较器,它允许逐个成员递归地比较对象,并为某些属性,字段或类型定义自定义比较规则。
对象比较器可以被认为是随时可用的框架,也可以被认为是类似解决方案的想法。 本文主要关注框架的使用,而不是实现。 如果您对实现,修改感兴趣,或者您有任何想法可以使此框架更好,请随时以任何方式与我联系。
安装
将对象比较器安装为NuGet包,或从CodeProject或GitHub下载代码,然后将对象比较器项目添加到您的解决方案中。
Install-Package ObjectsComparer
基本示例
public class ClassA
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
}
下面有两个示例,说明如何使用对象比较器来比较此类的实例。
var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };
var a2 = new ClassA { StringProperty = "String", IntProperty = 1 };
var comparer = new Comparer<ClassA>();
var isEqual = comparer.Compare(a1, a2);
Debug.WriteLine("a1 and a2 are " + (isEqual ? "equal" : "not equal"));
a1 和 a2 相等
var a1 = new ClassA { StringProperty = "String", IntProperty = 1 };
var a2 = new ClassA { StringProperty = "String", IntProperty = 2 };
var comparer = new Comparer<ClassA>();
IEnumerable<Difference> differenses;
var isEqual = comparer.Compare(a1, a2, out differenses);
var differensesList = differenses.ToList();
Debug.WriteLine("a1 and a2 are " + (isEqual ? "equal" : "not equal"));
if (!isEqual)
{
Debug.WriteLine("Differences:");
Debug.WriteLine(string.Join(Environment.NewLine, differensesList));
}
a1 和 a2 不相等
区别
Difference: MemberPath='IntProperty', Value1='1', Value2='2'
覆盖比较规则
要覆盖比较规则,我们需要创建自定义值比较器。 此类应从 AbstractValueComparer<T>
继承,或者应实现 IValueComparer<T>
。
public class MyComparer: AbstractValueComparer<T>
{
public override bool Compare(string obj1, string obj2, ComparisonSettings settings)
{
return obj1 == obj2; //Implement comparison logic here
}
}
类型比较规则覆盖。
comparer.AddComparerOverride<string>(new MyComparer());
字段比较规则覆盖。
comparer.AddComparerOverride(() => new ClassA().StringProperty, new MyComparer());
comparer.AddComparerOverride( () => new ClassA().StringProperty, (s1, s2, parentSettings) => s1 == s2, s => s.ToString());
comparer.AddComparerOverride( () => new ClassA().StringProperty, (s1, s2, parentSettings) => s1 == s2);
比较设置
比较器有一个可选的 *settings* 参数来配置比较。
递归比较
.
默认为 True。 如果为 true,则所有非基元类型、没有自定义比较规则且未实现 ICompareble
的成员将作为单独的对象进行比较,使用与当前对象相同的规则。
空和 Null 枚举相等。
默认为 False。 如果为 true,则空枚举和 null 值将被视为相等的值。
比较设置类允许存储可在自定义比较器中使用的自定义值。
SetCustomSetting<T>(T value, string key = null)
GetCustomSetting<T>(string key = null)
Factory
工厂提供了一种封装比较器创建和配置的方法。 工厂应实现 IComparersFactory
或应从 ComparersFactory
继承。
public class MyComparersFactory: ComparersFactory
{
public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)
{
if (typeof(T) == typeof(ClassA))
{
var comparer = new Comparer<ClassA>(settings, parentComparer, this);
comparer.AddComparerOverride<Guid>(new MyCustomGuidComparer());
return (IComparer<T>)comparer;
}
return base.GetObjectsComparer<T>(settings, parentComparer);
}
}
非泛型比较器
var comparer = new Comparer();
var isEqual = comparer.Compare(a1, a2);
此比较器为每次比较创建比较器的泛型实现。 泛型比较器工作速度更快。
有用的值比较器
框架包含几个可能有用的自定义比较器。
DoNotCompareValueComparer
.
允许跳过某些字段/类型。 具有单例实现(DoNotCompareValueComparer.Instance
)。
DynamicValueComparer<T>
.
接收比较规则作为函数。
NulableStringsValueComparer
.
Null 和空字符串被视为相等的值。
示例
这里有一些对象比较器的使用示例。
NUnit 用于开发单元测试以展示示例如何工作。
示例 1:预期消息
挑战:检查收到的消息是否等于预期消息。
public class Error
{
public int Id { get; set; }
public string Messgae { get; set; }
}
public class Message
{
public string Id { get; set; }
public DateTime DateCreated { get; set; }
public int MessageType { get; set; }
public int Status { get; set; }
public List<Error> Errors { get; set; }
public override string ToString()
{
return $"Id:{Id}, Date:{DateCreated}, Type:{MessageType}, Status:{Status}";
}
}
[TestFixture]
public class Example1Tests
{
private IComparer<Message> _comparer;
[SetUp]
public void SetUp()
{
_comparer = new Comparer<Message>(
new ComparisonSettings
{
//Null and empty error lists are equal
EmptyAndNullEnumerablesEqual = true
});
//Do not compare DateCreated
_comparer.AddComparerOverride<DateTime>(DoNotCompareValueComparer.Instance);
//Do not compare Id
_comparer.AddComparerOverride(() => new Message().Id, DoNotCompareValueComparer.Instance);
//Do not compare Message Text
_comparer.AddComparerOverride(() => new Error().Messgae, DoNotCompareValueComparer.Instance);
}
[Test]
public void EqualMessagesWithoutErrorsTest()
{
var expectedMessage = new Message
{
MessageType = 1,
Status = 0,
};
var actualMessage = new Message
{
Id = "M12345",
DateCreated = DateTime.Now,
MessageType = 1,
Status = 0,
};
var isEqual = _comparer.Compare(expectedMessage, actualMessage);
Assert.IsTrue(isEqual);
}
[Test]
public void EqualMessagesWithErrorsTest()
{
var expectedMessage = new Message
{
MessageType = 1,
Status = 1,
Errors = new List<Error>
{
new Error { Id = 2 },
new Error { Id = 7 }
}
};
var actualMessage = new Message
{
Id = "M12345",
DateCreated = DateTime.Now,
MessageType = 1,
Status = 1,
Errors = new List<Error>
{
new Error { Id = 2, Messgae = "Some error #2" },
new Error { Id = 7, Messgae = "Some error #7" },
}
};
var isEqual = _comparer.Compare(expectedMessage, actualMessage);
Assert.IsTrue(isEqual);
}
}
示例 2:人员比较
挑战:比较来自不同来源的人员。
public class Person
{
public Guid PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public string PhoneNumber { get; set; }
public override string ToString()
{
return $"{FirstName} {MiddleName} {LastName} ({PhoneNumber})";
}
}
电话号码可以有不同的格式。 让我们只比较数字。
public class PhoneNumberComparer: AbstractValueComparer<string>
{
public override bool Compare(string obj1, string obj2, ComparisonSettings settings)
{
return ExtractDigits(obj1) == ExtractDigits(obj2);
}
private string ExtractDigits(string str)
{
return string.Join(
string.Empty,
(str ?? string.Empty)
.ToCharArray()
.Where(char.IsDigit));
}
}
工厂允许不必每次需要创建比较器时都配置比较器。
public class MyComparersFactory: ComparersFactory
{
public override IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null, IBaseComparer parentComparer = null)
{
if (typeof(T) == typeof(Person))
{
var comparer = new Comparer<Person>(settings, parentComparer, this);
//Do not compare PersonId
comparer.AddComparerOverride<Guid>(DoNotCompareValueComparer.Instance);
//Sometimes MiddleName can be skipped. Compare only if property has value.
comparer.AddComparerOverride(
() => new Person().MiddleName,
(s1, s2, parentSettings) => string.IsNullOrWhiteSpace(s1) || string.IsNullOrWhiteSpace(s2) || s1 == s2);
comparer.AddComparerOverride(
() => new Person().PhoneNumber,
new PhoneNumberComparer());
return (IComparer<T>)comparer;
}
return base.GetObjectsComparer<T>(settings, parentComparer);
}
}
[TestFixture] public class Example2Tests { private MyComparersFactory _factory; private IComparer<Person> _comparer; [SetUp] public void SetUp() { _factory = new MyComparersFactory(); _comparer = _factory.GetObjectsComparer<Person>(); } [Test] public void EqualPersonsTest() { var person1 = new Person { PersonId = Guid.NewGuid(), FirstName = "John", LastName = "Doe", MiddleName = "F", PhoneNumber = "111-555-8888" }; var person2 = new Person { PersonId = Guid.NewGuid(), FirstName = "John", LastName = "Doe", PhoneNumber = "(111) 555 8888" }; IEnumerable<Difference> differenses; var isEqual = _comparer.Compare(person1, person2, out differenses); Assert.IsTrue(isEqual); Debug.WriteLine($"Persons {person1} and {person2} are equal"); } [Test] public void DifferentPersonsTest() { var person1 = new Person { PersonId = Guid.NewGuid(), FirstName = "Jack", LastName = "Doe", MiddleName = "F", PhoneNumber = "111-555-8888" }; var person2 = new Person { PersonId = Guid.NewGuid(), FirstName = "John", LastName = "Doe", MiddleName = "L", PhoneNumber = "222-555-9999" }; IEnumerable<Difference> differenses; var isEqual = _comparer.Compare(person1, person2, out differenses); var differensesList = differenses.ToList(); Assert.IsFalse(isEqual); Assert.AreEqual(3, differensesList.Count); Assert.IsTrue(differensesList.Any(d => d.MemberPath == "FirstName" && d.Value1 == "Jack" && d.Value2 == "John")); Assert.IsTrue(differensesList.Any(d => d.MemberPath == "MiddleName" && d.Value1 == "F" && d.Value2 == "L")); Assert.IsTrue(differensesList.Any(d => d.MemberPath == "PhoneNumber" && d.Value1 == "111-555-8888" && d.Value2 == "222-555-9999")); Debug.WriteLine($"Persons {person1} and {person2}"); Debug.WriteLine("Differences:"); Debug.WriteLine(string.Join(Environment.NewLine, differensesList)); } }
人员 John F Doe (111-555-8888) 和 John Doe ((111) 555 8888) 相等
人员 Jack F Doe (111-555-8888) 和 John L Doe (222-555-9999)
区别
Difference: MemberPath='FirstName', Value1='Jack', Value2='John'. Difference: MemberPath='MiddleName', Value1='F', Value2='L'. Difference: MemberPath='PhoneNumber', Value1='111-555-8888', Value2='222-555-9999'.