如何使用LINQ Distinct()方法对多个字段进行去重

99

我有以下从数据库派生出来的EF类(简化版)

class Product
{ 
     public string ProductId;
     public string ProductName;
     public string CategoryId;
     public string CategoryName;
}

ProductId是该表的主键

由于数据库设计者做出了错误的设计决策(我无法修改它),因此我在该表中有CategoryIdCategoryName

我需要一个下拉列表,其中(不同的)CategoryId作为CategoryName作为文本。因此,我应用了以下代码:

product.Select(m => new {m.CategoryId, m.CategoryName}).Distinct();
逻辑上它应该创建一个具有 CategoryIdCategoryName 属性的匿名对象。 Distinct() 保证没有重复的配对 (CategoryId, CategoryName)。

但实际上它并不起作用。就我所知,只有在集合中只有一个字段时,Distinct() 才会起作用,否则它会忽略它们... 这是正确的吗? 有什么解决方法吗?谢谢!

更新

抱歉,product 是:

List<Product> product = new List<Product>();

我找到了一种替代方法,可以获得与 Distinct() 相同的结果:

product.GroupBy(d => new {d.CategoryId, d.CategoryName}) 
       .Select(m => new {m.Key.CategoryId, m.Key.CategoryName})

“集合中只有一个字段”是毫无意义的。你的意思是什么? - leppie
1
@leppie 我猜他的意思是,当投影到单个值时,而不是匿名类型(包含多个字段)。 - sehe
“由于数据库设计师做出的糟糕设计决策(我无法修改)”,你也许不能改变数据库,但这并不意味着你不能在你的 EF 模型中修复它。这就是 EF 的美妙之处。 - Steven
它在哪里不起作用?你在经典ASP.NET、MVC中的哪个环节?"product.Select"中的“product”是什么? - Raphaël Althaus
@leppie,抱歉我是指“集合中的属性”。 - CiccioMiami
10个回答

100

我假设您在列表上使用Distinct作为方法调用。 您需要将查询结果用作DropDownList的数据源,例如通过使用ToList 来实现。

var distinctCategories = product
                        .Select(m => new {m.CategoryId, m.CategoryName})
                        .Distinct()
                        .ToList();
DropDownList1.DataSource     = distinctCategories;
DropDownList1.DataTextField  = "CategoryName";
DropDownList1.DataValueField = "CategoryId";

如果你需要真正的对象而不是只有几个属性的匿名类型,另一种方法是使用带有匿名类型的 GroupBy

List<Product> distinctProductList = product
    .GroupBy(m => new {m.CategoryId, m.CategoryName})
    .Select(group => group.First())  // instead of First you can also apply your logic here what you want to take, for example an OrderBy
    .ToList();

第三个选项是使用MoreLinq的DistinctBy


2
@CiccioMiami 的点赞者们,请确保您仅将此答案应用于匿名类型。有类型的类可能需要像这个委托一样的东西。 - crokusek
正确,distinct 只能与匿名类型一起使用。 - imdadhusen
1
@imdadhusen:不仅需要使用匿名类型,该类型还必须重写 Equals+GetHashCode 或者在 Distinct 的重载中提供自定义的 IEQualityCpomparer<TypeName> - Tim Schmelter
@Tim,我可以同时使用Where和Group By条件吗?在你的第二个查询或代码中,我只想在分组之前应用where条件,这是可能的吗? - Md Aslam

21

12

Distinct()保证了没有重复的(CategortId, CategoryName)这一对。

- 就是这样

匿名类型会 '神奇地' 实现EqualsGetHashCode方法。

我猜测其他地方可能存在错误。是否区分大小写?可变类?不可比较的字段?


这就是我想的,但下面的答案说不是。/ 我很困惑。 - leppie
@Ieppie https://dev59.com/tHRB5IYBdhLWcg3wtZIV - Raphaël Althaus

6

这是我的解决方案,它支持不同类型的keySelectors:

public static IEnumerable<TSource> DistinctBy<TSource>(this IEnumerable<TSource> source, params Func<TSource, object>[] keySelectors)
{
    // initialize the table
    var seenKeysTable = keySelectors.ToDictionary(x => x, x => new HashSet<object>());

    // loop through each element in source
    foreach (var element in source)
    {
        // initialize the flag to true
        var flag = true;

        // loop through each keySelector a
        foreach (var (keySelector, hashSet) in seenKeysTable)
        {                    
            // if all conditions are true
            flag = flag && hashSet.Add(keySelector(element));
        }

        // if no duplicate key was added to table, then yield the list element
        if (flag)
        {
            yield return element;
        }
    }
}

使用它的方法如下:
list.DistinctBy(d => d.CategoryId, d => d.CategoryName)

1
这种方法的优点是它适用于匿名类型和已定义类型。 - crokusek
在 .net 4.7.2 中使用此解决方案遇到了问题。也许这只适用于 .net Core。 - drzounds

5

在您的选择中使用Key关键字会起作用,如下所示。

product.Select(m => new {Key m.CategoryId, Key m.CategoryName}).Distinct();

我意识到这引起了一个旧的线程,但想到它可能会帮助一些人。当我在使用.NET时,通常使用VB.NET进行编码,因此Key在C#中可能有不同的翻译。


5
在VB.Net中对多个字段进行去重,需要在匿名类型的每个属性上使用“Key”关键字。(否则,用于比较的哈希码将无法正确计算) 在C#中,没有“Key”关键字 - 所有匿名类型中的属性都自动成为关键字段。 - voon

4

Distinct 方法从序列中返回不同的元素。

如果您使用 Reflector 查看其实现,您会发现它为匿名类型创建 DistinctIterator。 Distinct 迭代器在枚举集合时将元素添加到 Set 中。此枚举器跳过已经存在于 Set 中的所有元素。 Set 使用 GetHashCodeEquals 方法来定义元素是否已经存在于 Set 中。

匿名类型的 GetHashCodeEquals 如何实现?正如 msdn 所述:

匿名类型上的 Equals 和 GetHashCode 方法是根据属性的 Equals 和 GetHashcode 方法定义的,只有当所有属性都相等时,两个相同匿名类型的实例才相等。

因此,在迭代不同的集合时,您绝对应该拥有不同的匿名对象。结果不取决于您为匿名类型使用了多少个字段。

4

回答问题标题(吸引人们来这里的原因)并忽略使用匿名类型的示例...

此解决方案也适用于非匿名类型。对于匿名类型,不需要使用此解决方案。

辅助类:

/// <summary>
/// Allow IEqualityComparer to be configured within a lambda expression.
/// From https://dev59.com/M3VD5IYBdhLWcg3wE3Xz
/// </summary>
/// <typeparam name="T"></typeparam>
public class LambdaEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    /// <summary>
    /// Simplest constructor, provide a conversion to string for type T to use as a comparison key (GetHashCode() and Equals().
    /// https://dev59.com/M3VD5IYBdhLWcg3wE3Xz, user "orip"
    /// </summary>
    /// <param name="toString"></param>
    public LambdaEqualityComparer(Func<T, string> toString)
        : this((t1, t2) => toString(t1) == toString(t2), t => toString(t).GetHashCode())
    {
    }

    /// <summary>
    /// Constructor.  Assumes T.GetHashCode() is accurate.
    /// </summary>
    /// <param name="comparer"></param>
    public LambdaEqualityComparer(Func<T, T, bool> comparer)
        : this(comparer, t => t.GetHashCode())
    {
    }

    /// <summary>
    /// Constructor, provide a equality comparer and a hash.
    /// </summary>
    /// <param name="comparer"></param>
    /// <param name="hash"></param>
    public LambdaEqualityComparer(Func<T, T, bool> comparer, Func<T, int> hash)
    {
        _comparer = comparer;
        _hash = hash;
    }

    public bool Equals(T x, T y)
    {
        return _comparer(x, y);
    }

    public int GetHashCode(T obj)
    {
        return _hash(obj);
    }    
}

最简单的用法:

List<Product> products = duplicatedProducts.Distinct(
    new LambdaEqualityComparer<Product>(p =>
        String.Format("{0}{1}{2}{3}",
            p.ProductId,
            p.ProductName,
            p.CategoryId,
            p.CategoryName))
        ).ToList();

最简单(但不太高效)的用法是将映射到字符串表示形式,以避免自定义哈希。 相等的字符串已经具有相等的哈希码。

参考资料:
将委托包装在IEqualityComparer中


1
public List<ItemCustom2> GetBrandListByCat(int id)
    {

        var OBJ = (from a in db.Items
                   join b in db.Brands on a.BrandId equals b.Id into abc1
                   where (a.ItemCategoryId == id)
                   from b in abc1.DefaultIfEmpty()
                   select new
                   {
                       ItemCategoryId = a.ItemCategoryId,
                       Brand_Name = b.Name,
                       Brand_Id = b.Id,
                       Brand_Pic = b.Pic,

                   }).Distinct();


        List<ItemCustom2> ob = new List<ItemCustom2>();
        foreach (var item in OBJ)
        {
            ItemCustom2 abc = new ItemCustom2();
            abc.CategoryId = item.ItemCategoryId;
            abc.BrandId = item.Brand_Id;
            abc.BrandName = item.Brand_Name;
            abc.BrandPic = item.Brand_Pic;
            ob.Add(abc);
        }
        return ob;

    }

0

你的问题的解决方案看起来像这样:

public class Category {
  public long CategoryId { get; set; }
  public string CategoryName { get; set; }
} 

...

public class CategoryEqualityComparer : IEqualityComparer<Category>
{
   public bool Equals(Category x, Category y)
     => x.CategoryId.Equals(y.CategoryId)
          && x.CategoryName .Equals(y.CategoryName, 
 StringComparison.OrdinalIgnoreCase);

   public int GetHashCode(Mapping obj)
     => obj == null 
         ? 0
         : obj.CategoryId.GetHashCode()
           ^ obj.CategoryName.GetHashCode();
}

...

 var distinctCategories = product
     .Select(_ => 
        new Category {
           CategoryId = _.CategoryId, 
           CategoryName = _.CategoryName
        })
     .Distinct(new CategoryEqualityComparer())
     .ToList();

-3
Employee emp1 = new Employee() { ID = 1, Name = "Narendra1", Salary = 11111, Experience = 3, Age = 30 };Employee emp2 = new Employee() { ID = 2, Name = "Narendra2", Salary = 21111, Experience = 10, Age = 38 };
Employee emp3 = new Employee() { ID = 3, Name = "Narendra3", Salary = 31111, Experience = 4, Age = 33 };
Employee emp4 = new Employee() { ID = 3, Name = "Narendra4", Salary = 41111, Experience = 7, Age = 33 };

List<Employee> lstEmployee = new List<Employee>();

lstEmployee.Add(emp1);
lstEmployee.Add(emp2);
lstEmployee.Add(emp3);
lstEmployee.Add(emp4);

var eemmppss=lstEmployee.Select(cc=>new {cc.ID,cc.Age}).Distinct();

如果我现在想按年龄排序怎么办? - Si8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接