C# 中的动态相等

Dynamic equality in C#

假设我有以下 class:

public class TsvDataModel : IEquatable<TsvDataModel>
{
    public int ElementId { get; set; }

    public string Location { get; set; }
    public string RoomKey { get; set; 

    public decimal Area { get; set; }

    public bool Equals(TsvDataModel other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return ElementUid == other.ElementUid && Location == other.Location && RoomKey == other.RoomKey &&
               SubType == other.SubType;
    }

    // current equality comparison implementation
    public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is TsvDataModel other && Equals(other));

    public override int GetHashCode() => HashCode.Combine(ElementUid, Location, RoomKey, SubType);
}

我想要的是基于用户提供的属性创建动态相等性,即有时我只想通过 ElementIds 进行比较,有时与 include/exclude 其他属性进行比较。我不想创建所有可能的组合。我尝试使用反射通过 dynamic 类型获取属性,我可以获得值和类型,但比较不起作用。

我更愿意将属性作为 属性 名称(字符串)的列表传递。

我想使用 IEqualityComparerIEquitable(我将此对象用作 HashSet<T>)。

这里是 DynamicEqualityComparer:

public class DynamicEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly IEnumerable<Func<T, object>> _propertySelectors;

    public DynamicEqualityComparer(params Func<T, object>[] propertySelectors)
    {
        _propertySelectors = propertySelectors;
    }

    public bool Equals(T x, T y)
    {
        return _propertySelectors.All(propertySelector =>
            object.Equals(propertySelector(x), propertySelector(y)));

    }

    public int GetHashCode(T obj)
    {
        var hashCode = new HashCode();
        foreach (var propertySelector in _propertySelectors)
        {
            hashCode.Add(propertySelector((T)obj));
        }

        return hashCode.ToHashCode();
    }

    public static DynamicEqualityComparer<T> FromPropertyNames(IEnumerable<string> propertyNames)
    {
        IEnumerable<Func<T, object>> selectors = propertyNames.Select(propertyName =>
        {
            var property = typeof(T).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
            if (property == null)
            {
                throw new Exception($"Unable to find a property named {propertyName} in type {typeof(T)}");
            }
            return new Func<T, object>(obj => property.GetValue(obj));
        });
        return new DynamicEqualityComparer<T>(selectors.ToArray());
    }
}

用法:

var comparer = new DynamicEqualityComparer<TsvDataModel>(
    model => model.Location, 
    model => model.RoomKey,
    model => model.Area);

var hashset = new HashSet<TsvDataModel>(comparer);

var comparer = DynamicEqualityComparer<TsvDataModel>
    .FromPropertyNames(new[] {"Location", "RoomKey", "Area"});

var hashset = new HashSet<TsvDataModel>(comparer);

此外,这里有一些单元测试:

[Fact]
public void Recognizes_Equal_Models()
{
    var modelA = new TsvDataModel { ElementId = 2, Location = "x", RoomKey = "y", Area = 1.1m };
    var modelB = new TsvDataModel { ElementId = 3, Location = "x", RoomKey = "y", Area = 1.1m };

    var comparer = DynamicEqualityComparer<TsvDataModel>
        .FromPropertyNames(new[] { "Location", "RoomKey", "Area" });

    var hashset = new HashSet<TsvDataModel>(comparer);
    hashset.Add(modelA);
    hashset.Add(modelB);

    // ElementId is ignored. Location, RoomKey, and Area are the same.
    // So the comparer indicates that they are "equal."
    Assert.Equal(1, hashset.Count);
}

[Fact]
public void Recognizes_Unequal_Models()
{
    var modelA = new TsvDataModel { ElementId = 2, Location = "xy", RoomKey = "y", Area = 1.1m };
    var modelB = new TsvDataModel { ElementId = 3, Location = "x", RoomKey = "y", Area = 1.1m };

    var comparer = DynamicEqualityComparer<TsvDataModel>
        .FromPropertyNames(new[] { "Location", "RoomKey", "Area" });

    var hashset = new HashSet<TsvDataModel>(comparer);
    hashset.Add(modelA);
    hashset.Add(modelB);

    // ElementId is ignored. RoomKey and Area are the same but Location is different.
    // So the comparer indicates that they are not "equal."
    Assert.Equal(2, hashset.Count);
}