与LINQ to Objects不兼容的区别


120
class Program
{
    static void Main(string[] args)
    {
        List<Book> books = new List<Book> 
        {
            new Book
            {
                Name="C# in Depth",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },
                     new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },                       
                }
            },
            new Book
            {
                Name="LINQ in Action",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Fabrice", LastName="Marguerie"
                    },
                     new Author 
                    {
                        FirstName = "Steve", LastName="Eichert"
                    },
                     new Author 
                    {
                        FirstName = "Jim", LastName="Wooley"
                    },
                }
            },
        };


        var temp = books.SelectMany(book => book.Authors).Distinct();
        foreach (var author in temp)
        {
            Console.WriteLine(author.FirstName + " " + author.LastName);
        }

        Console.Read();
    }

}
public class Book
{
    public string Name { get; set; }
    public List<Author> Authors { get; set; }
}
public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object obj)
    {
        return true;
        //if (obj.GetType() != typeof(Author)) return false;
        //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName;
    }

}

这基于“ LINQ in Action”中的示例。清单4.16

这将打印两次Jon Skeet。为什么?我什至尝试在Author类中重写Equals方法。仍然不同似乎没有用。我想念什么?

编辑:我也添加了==和!=运算符重载。仍然没有帮助。

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }

Answers:


159

对于自定义对象,LINQ Distinct不是那么聪明。

它所做的只是查看您的列表,并看到它具有两个不同的对象(它不在乎它们的成员字段具有相同的值)。

一个解决办法是实现IEquatable接口如图所示这里

如果您像这样修改Author类,它应该可以工作。

public class Author : IEquatable<Author>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public bool Equals(Author other)
    {
        if (FirstName == other.FirstName && LastName == other.LastName)
            return true;

        return false;
    }

    public override int GetHashCode()
    {
        int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode();
        int hashLastName = LastName == null ? 0 : LastName.GetHashCode();

        return hashFirstName ^ hashLastName;
    }
}

尝试作为DotNetFiddle


22
IEquatable很好,但不完整;您应该始终一起实现Object.Equals()和Object.GetHashCode();IEquatable <T> .Equals不会覆盖Object.Equals,因此在进行非强类型比较时,此操作将失败,这种比较通常在框架中发生,并且总是在非通用集合中发生。
2009年

那么最好使用Rex M建议的使用IEqualityComparer <T>的Distinct替代吗?我的意思是如果我不想陷入陷阱,该怎么办。
Tanmoy

3
@Tanmoy这取决于。如果希望Author通常表现得像普通对象一样(即仅引用相等),但为了区分而检查名称值,请使用IEqualityComparer。如果您始终希望根据名称值比较Author对象,请重写GetHashCode和Equals或实现IEquatable。
Rex M

3
我实现了IEquatable(并覆盖Equals/ GetHashCode),但是在Linq上这些方法中没有触发任何断点Distinct
PeterX 2014年

2
@PeterX我也注意到了这一点。我在GetHashCode和中都有断点Equals,它们在foreach循环中被击中。这是因为var temp = books.SelectMany(book => book.Authors).Distinct();返回一个IEnumerable,表示该请求不会立即执行,仅在使用数据时才执行。如果您想立即获得触发示例,请在.ToList()后面添加.Distinct(),您将在EqualsGetHashCode之前看到断点。
JabberwockyDecompiler 2015年

70

Distinct()方法检查引用类型的引用相等性。这意味着它实际上是在寻找相同的对象,而不是包含相同值的不同对象。

有一个需要IEqualityComparer重载,因此您可以指定不同的逻辑以确定给定对象是否等于另一个。

如果您希望Author通常表现得像普通对象一样(即仅引用相等),但是出于区分检查名称值相等性的目的,请使用IEqualityComparer。如果您始终希望根据名称值比较Author对象,请重写GetHashCode和Equals实现IEquatable

IEqualityComparer接口上的两个成员是EqualsGetHashCodeAuthor如果名字和姓氏字符串相同,则确定两个对象是否相等的逻辑似乎是。

public class AuthorEquals : IEqualityComparer<Author>
{
    public bool Equals(Author left, Author right)
    {
        if((object)left == null && (object)right == null)
        {
            return true;
        }
        if((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.FirstName == right.FirstName && left.LastName == right.LastName;
    }

    public int GetHashCode(Author author)
    {
        return (author.FirstName + author.LastName).GetHashCode();
    }
}

1
谢谢!您的GetHashCode()实现向我展示了我仍然缺少的东西。我返回的是{传递的对象} .GetHashCode(),而不是返回的{属性是用于比较的} .GetHashCode()。这有所作为,并解释了为什么我的仍然失败–两个不同的引用将具有两个不同的哈希码。
pelazem 2015年

44

而不实施另一种解决方案IEquatableEquals并且GetHashCode是使用LINQs GroupBy方法和选择来自IGrouping的第一项。

var temp = books.SelectMany(book => book.Authors)
                .GroupBy (y => y.FirstName + y.LastName )
                .Select (y => y.First ());

foreach (var author in temp){
  Console.WriteLine(author.FirstName + " " + author.LastName);
}

1
仅仅考虑性能,确实对我有帮助吗,就像考虑上述方法一样,该速度是否相同?
Biswajeet 2015年

比将其与实现方法复杂化要好得多,并且如果使用EF会将工作委派给sql服务器。
Zapnologica 2015年

尽管此方法可能有效,但由于将事物分组的原因,因此会出现性能问题
Bellash

@Bellash使它工作,然后使其快速。当然,这种分组可能导致更多的工作要做。但是有时候实现比您想要的还要麻烦的事情。
Jehof

2
我更喜欢这种解决方案,但是随后在分组依据中使用了“新”对象: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

还有另一种方法可以从用户定义的数据类型列表中获得不同的值:

YourList.GroupBy(i => i.Id).Select(i => i.FirstOrDefault()).ToList();

当然,它将提供不同的数据集


21

Distinct()对可枚举中的对象执行默认的相等比较。如果尚未覆盖Equals()GetHashCode(),则它将使用上的默认实现object,该实现比较引用。

简单的解决办法是增加一个正确的实施Equals()GetHashCode()向参与您比较(即图书和作者)对象图的所有类。

IEqualityComparer接口是方便,可让您实现Equals()GetHashCode()在一个单独的类,当你不能够访问你需要比较,或者如果您使用比较不同的方法类的内部结构。


非常感谢您对参与对象的出色评论。
suhyura

11

您已经覆盖了Equals(),但是请确保您还覆盖了GetHashCode()


+1用于强调GetHashCode()。不要添加基本的HashCode实现,例如<custom>^base.GetHashCode()
Dani 2013年

8

以上答案是错误的!!!在MSDN上声明的Distinct返回默认的Equator,该Equator如所述。Default属性检查类型T是否实现System.IEquatable接口,如果是,则返回使用该实现的EqualityComparer。否则,它返回一个EqualityComparer,它使用T提供的Object.Equals和Object.GetHashCode的覆盖。

这意味着只要您覆盖等于等于您就可以了。

您的代码无法使用的原因是因为您检查了firstname == lastname。

参见https://msdn.microsoft.com/library/bb348436(v=vs.100).aspxhttps://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx


0

您可以在列表上使用扩展方法,该方法基于计算的哈希值检查唯一性。您还可以更改扩展方法以支持IEnumerable。

例:

public class Employee{
public string Name{get;set;}
public int Age{get;set;}
}

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

扩展方式:

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }

-1

您可以通过两种方法来实现:

1. 您可以实现IEquatable接口,如Enumerable.Distinct方法所示,或者您可以在此帖中看到@skalb的答案

2.如果您的对象没有唯一键,则可以使用GroupBy方法来获得不同的对象列表,即必须将对象的所有属性分组,然后选择第一个对象。

例如下面这样为我工作:

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

MyObject类如下所示:

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

3.如果您的对象具有唯一键,则只能在分组依据中使用它。

例如,我对象的唯一键是Id。

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.