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

用于 Linq Distinct() 的通用 IEqualityComparer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (17投票s)

2010年7月15日

CPOL

3分钟阅读

viewsIcon

195842

downloadIcon

734

IEqualityComparer 的实现,可用于按类的某个属性进行比较。

引言

我喜欢 Linq,并且发现自己越来越频繁地使用它,但每次我(重新)发现无法对集合中的类的属性执行 Distinct 过滤时,我总会感到有些恼火。例如,如果我有一个 Contact 对象的列表,并且我想根据电子邮件地址从该列表中提取一个不重复的 Contact列表。无参的 Distinct() 方法将基于默认相等比较器比较 Contact 对象,但没有快速的方法来指定我想基于电子邮件地址进行比较。本文介绍了一个 IEqualityComparer 的通用实现,该实现可由 Distinct() 用于根据类的属性来比较任何类。

背景

本文假定您对 .NET 集合的 LINQ 扩展有一般的了解。另外,请注意,本文讨论的是 Linq 在内存对象上的操作,而不是 Linq to SQL 或 Linq to Entities 等。

问题

首先,让我们看看我们的示例 Contact

public class Contact
{
    public string Name {get; set;}
    public string EmailAddress { get; set; }
}

没什么特别的,只是一个带有基本属性的类。我们要解决的问题是,如果我们有一个 Contact 对象的列表,其中一些联系人具有相同的电子邮件地址,我们想通过执行类似以下的操作来仅获取不重复的电子邮件地址列表:

IEnumerable<Contact> collection = //retrieve list of Contacts here
IEnumerable<Contact> distinctEmails = collection.Distinct();

但是如果我们这样做,Distinct 将基于默认相等比较器比较 Contact 对象,该比较器将按引用比较它们。在这种情况下,Distinct 将返回我们原始集合中的所有 Contact(假设它们都是唯一的实例)。

解决方案 1:重写默认相等比较器

让 Linq 操作 EmailAddress 属性的一个解决方案是为 Contact 类重写 Equals GetHashCode 方法,并让它使用 Contact EmailAddress 属性。这将导致无参的 Distinct() 方法使用您的重写。除了这种方法存在细微的复杂性使其难以处理之外,您可能并不总是希望根据 EmailAddress 比较 Contact 对象。您可能还想根据 Name 进行比较。因此,Equals 运算符可能不是最佳解决方案。

解决方案 2:实现 IEqualityComparer<Contact>

Distinct() 方法还有一个重载,允许您指定一个 IEqualityComparer 实现。因此,另一个解决方案是编写一个实现 IEqualityComparer<Contact> 的类,并基于 EmailAddress 属性执行比较。

要做到这一点,我们必须创建我们的比较器类

class ContactEmailComparer : IEqualityComparer<Contact>
{
    #region IEqualityComparer<Contact> Members

    public bool Equals(Contact x, Contact y)
    {
        return x.EmailAddress.Equals(y.EmailAddress);
    }

    public int GetHashCode(Contact obj)
    {
        return obj.EmailAddress.GetHashCode();
    }

    #endregion
}
IEqualityComparer<Contact> customComparer = new ContactEmailComparer();
IEnumerable<Contact> distinctEmails = collection.Distinct(customComparer); 

这将导致 Distinct() 方法根据我们自定义的 Equals 实现来比较我们的对象,该实现使用 Contact EmailAddress 属性。

通用解决方案

ContactEmailComparer 的实现非常简单,但仅仅是为了获取不重复的电子邮件地址列表似乎做了很多工作。

一个更通用的解决方案是编写一个泛型类,您可以告诉它比较对象的哪个属性。我们将扩展我们的 IEqualityComparer ,使其使用反射来提取指定属性的值,而不是将我们的类限制为单个属性。

这是该类的实现

public class PropertyComparer<T> : IEqualityComparer<T>
{
    private PropertyInfo _PropertyInfo;
    
    /// <summary>
    /// Creates a new instance of PropertyComparer.
    /// </summary>
    /// <param name="propertyName">The name of the property on type T 
    /// to perform the comparison on.</param>
    public PropertyComparer(string propertyName)
    {
        //store a reference to the property info object for use during the comparison
        _PropertyInfo = typeof(T).GetProperty(propertyName, 
	BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public);
        if (_PropertyInfo == null)
        {
            throw new ArgumentException(string.Format("{0} 
		is not a property of type {1}.", propertyName, typeof(T)));
        }
    }
    
    #region IEqualityComparer<T> Members
    
    public bool Equals(T x, T y)
    {
        //get the current value of the comparison property of x and of y
        object xValue = _PropertyInfo.GetValue(x, null);
        object yValue = _PropertyInfo.GetValue(y, null);
        
        //if the xValue is null then we consider them equal if and only if yValue is null
        if (xValue == null)
            return yValue == null;
            
        //use the default comparer for whatever type the comparison property is.
        return xValue.Equals(yValue);
    }
    
    public int GetHashCode(T obj)
    {
        //get the value of the comparison property out of obj
        object propertyValue = _PropertyInfo.GetValue(obj, null);
        
        if (propertyValue == null)
            return 0;
            
        else
            return propertyValue.GetHashCode();
    }
    
    #endregion
}  

现在,要获取不重复的电子邮件地址列表,我们可以这样做:

IEqualityComparer<Contact> customComparer =
                   new PropertyComparer<Contact>(“EmailAddress”);
IEnumerable<Contact> distinctEmails = collection.Distinct(customComparer);

这个解决方案最好的地方在于它适用于任何属性和任何类型,所以我们不必编写自定义的 IEqualityComparer,而是可以重用我们的通用 PropertyComparer

例如,无需额外工作,我们还可以通过以下方式获取不重复的 Contact姓名列表:

IEqualityComparer<Contact> customComparer =  new PropertyComparer<Contact>(“Name”);
  IEnumerable<Contact> distinctEmails = collection.Distinct(customComparer); 

增强功能

目前,此实现仅适用于类的 public 属性。很容易将其扩展到检查 public 字段,这将是一个有用的功能。

结论

这段代码真的没什么特别之处。它只是 IEqualityComparer 的一个通用实现,它在其构造函数中接受一个指定属性名称的 string 。但是,对属性执行 Distinct 过滤,我总觉得应该很容易,但事实证明却有点麻烦。这个类让它变得更容易一些,希望您觉得它有用。

历史

  • 2010 年 7 月 15 日:初始发布
© . All rights reserved.