65.9K
CodeProject 正在变化。 阅读更多。
Home

对象比较器

2017年5月9日

MIT

2分钟阅读

viewsIcon

30900

downloadIcon

80

用于.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'.
© . All rights reserved.