使用自定义 EqualityComparer 检查 C# 项目是否已在列表中更新

Using custom EqualityComparer to check if C# item has been updated in a list

我正在使用自定义比较器比较 2 个对象列表,如下所示:

public class LocationEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        var idComparer = string.Equals(x.Id, y.Id, 
            System.StringComparison.OrdinalIgnoreCase); 
        var nameComparer = string.Equals(x.Name, y.Name, 
            System.StringComparison.OrdinalIgnoreCase);
        var addressComparer = string.Equals(x.Address, y.Address, 
            System.StringComparison.OrdinalIgnoreCase);
        var postcodeComparer = string.Equals(x.PostCode, y.PostCode, 
            System.StringComparison.OrdinalIgnoreCase); 
     
        if (idComparer && nameComparer && addressComparer && postcodeComparer) 
        {
            return true; 
        }

        return false; 
    }
}

当我使用 Linq 检查相等性时,这对我来说非常有用: 如果我有两个 LocationData 列表(previousRuncurrentRun,我会得到正确的结果: List<LocationData> result = previousRun.Intersect(currentRun, new LocationEqualityComparer()).ToList();

我还可以使用 Linq 中的 Except 检查列表之间添加或删除了哪些项目。

我想要做的是检查列表之间是否更新了项目。这是因为它们代表一个旧列表(以前的运行)和一个新列表(当前运行)。因此,例如 LocationData 对象将具有相同的 ID、相同的地址和相同的邮政编码,但名称可能略有不同。

有谁知道我如何获得列表之间已更新的对象列表(即只有一个或两个属性已更改)但未定义为已添加或已删除?

谢谢

您可以简单地编写一个方法来进行 属性 比较,但是 returns true 如果特定数量的属性匹配(您说的是 1 或 2,所以我猜它是可变的?):

public static bool IsUpdated(LocationData previous, LocationData current, 
    int numPropsToMatch = 2)
{
    // If they are equal, return false
    if (new LocationEqualityComparer().Equals(previous, current)) return false;

    int numMatchingProps = 0;

    if (string.Equals(previous.Id, current.Id,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.Name, current.Name,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.Address, current.Address,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.PostCode, current.PostCode,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;

    // Change to == if you *only* want a specific number to match
    return numMatchingProps >= numPropsToMatch;
}

那么你可以在你的 Linq 语句中使用这个方法:

List<LocationData> updated = currentRun
    .Where(curr => previousRun.Any(prev => IsUpdated(prev, curr)))
    .ToList();

请注意,很可能不止一个地点具有相同的邮政编码,因此应该 包括在内,但由于未指定,我将其保留.

好吧,要获取在运行之间更新的项目,您需要为这种情况编写新的 IEqualityComparer。 主要是检查 ID 是否与原来相同,但其他任何内容都可能发生变化,例如姓名、地址等。 这是带有测试的此类比较器的示例 - 在我这边工作。

public class LocationIdEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        bool idComparer = string.Equals(x.Id, y.Id,
            StringComparison.OrdinalIgnoreCase);
        bool nameComparer = string.Equals(x.Name, y.Name,
            StringComparison.OrdinalIgnoreCase);
        bool addressComparer = string.Equals(x.Address, y.Address,
            StringComparison.OrdinalIgnoreCase);
        bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,
            StringComparison.OrdinalIgnoreCase);
        
        // so you need to check that ID is the same, but everything else may be different
        return idComparer && (!nameComparer || !addressComparer || !postcodeComparer);
    }

    public int GetHashCode(LocationData obj)
    {
        return obj.Id.GetHashCode();
    }
}

class TestUpdatedItemsInList
{
    [Test]
    public void TestItemsAreUpdated()
    {
        List<LocationData> originalList = new List<LocationData>
        {
            new LocationData("1", "first", "somewhere1", "postCode1"),
            new LocationData("2", "second", "somewhere2", "postCode2"),
            new LocationData("3", "third", "somewhere3", "postCode3"),
            new LocationData("4", "fourth", "somewhere4", "postCode4"),
        };

        List<LocationData> updatedList = new List<LocationData>
        {
            new LocationData("1", "1st", "somewhere1", "postCode1"),
            new LocationData("2", "second", "who knows where", "postCode2"),
            new LocationData("3", "third", "somewhere3", "updated postCode3"),
            new LocationData("4", "fourth", "somewhere4", "postCode4"),
            new LocationData("5", "fifth", "somewhere5", "postCode5"),
            new LocationData("6", "sixth", "somewhere6", "postCode6"),
        };
        
        // newly added and updated items will end up here
        var differentItems = updatedList.Except(originalList, new LocationFullEqualityComparer());
        // only updated items will be here
        var updatedItems = updatedList.Except(originalList, new LocationIdEqualityComparer());
        // only non-changed items will be here (item 4)
        var itemsWithoutChanges = updatedList.Intersect(originalList, new LocationFullEqualityComparer());


        Assert.That(differentItems, Has.Exactly(5).Items);
        Assert.That(updatedItems, Has.Exactly(3).Items);

        Assert.That(itemsWithoutChanges, Has.Exactly(1).Items);
    }
}

public class LocationData
{
    public LocationData(string id, string name, string address, string postCode)
    {
        Id = id;
        Name = name;
        Address = address;
        PostCode = postCode;
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string PostCode { get; set; }

    public override string ToString()
    {
        return $"{Id}, {Name}, {Address}, {PostCode}";
    }
}

// code provided by you
public class LocationFullEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        bool idComparer = string.Equals(x.Id, y.Id,
            StringComparison.OrdinalIgnoreCase);
        bool nameComparer = string.Equals(x.Name, y.Name,
            StringComparison.OrdinalIgnoreCase);
        bool addressComparer = string.Equals(x.Address, y.Address,
            StringComparison.OrdinalIgnoreCase);
        bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,
            StringComparison.OrdinalIgnoreCase);

        return idComparer && nameComparer && addressComparer && postcodeComparer;
    }

    public int GetHashCode(LocationData obj)
    {
        return obj.Id.GetHashCode();
    }
}

两个事物结构相同可以说它们相同,标识相同也可以说它们相同。

你的比较器主要检查它们在结构上是否相等。也就是说,您正在比较所有字段以确定所有属性是否相同。

这意味着您将 LocationData 视为 value-object。

但是,它有一个id,表明它是一个真正的实体。

这些应该区别对待,您可以阅读更多关于这些概念的信息 here and here

假设“Id”实际上是用来唯一标识位置的,这在大多数情况下是“Id”的目的,那么查找更新的问题就变得微不足道了。

一种方法是使用 extension-class,例如:

public static class LocationDataExt
{
    public static bool IsUpdated(this LocationData previous, LocationData current)
    {
        if (!string.Equals(previous.Id, current.Id, StringComparison.OrdinalIgnoreCase))
            return false;   // it is not updated, because it is not the same entity

        // else, return true if any other property has changed
        return !string.Equals(previous.Name, current.Name, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(previous.Address, current.Address, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(previous.PostCode, current.PostCode, StringComparison.OrdinalIgnoreCase);

    }
}

您在哪里可以使用它来查找像这样的更新:

var updated = currentRun
            .Where(current => previousRun.Any(previous => previous.IsUpdated(current)))
            .ToList();