如何使用LINQ实现这一功能?

5

我试图描述的最好方式是“嵌套DistinctBy”。

假设我有一个对象集合,每个对象包含一个昵称集合。

class Person
{
    public string Name { get; set; }
    public int Priority { get; set; }
    public string[] Nicknames { get; set; }
}

public class Program
{
    public static void Main()
    {
        var People = new List<Person>
        {
            new Person { Name = "Steve", Priority = 4, Nicknames = new string[] { "Stevo", "Lefty", "Slim" }},
            new Person { Name = "Karen", Priority = 6, Nicknames = new string[] { "Kary", "Birdie", "Snookie" }},
            new Person { Name = "Molly", Priority = 3, Nicknames = new string[] { "Mol", "Lefty", "Dixie" }},
            new Person { Name = "Greg", Priority = 5, Nicknames = new string[] { "G-man", "Chubs", "Skippy" }}
        };      
    }
}

我想选择所有人,但要确保没有人选择与另一个人相同的昵称。Molly和Steve都分享昵称“Lefty”,所以我想过滤掉其中一个人。只有最高优先级的人应被包含在内。如果2个或多个人具有相同的最高优先级,则只需选择它们中的第一个人。因此,在这个例子中,我希望得到除Steve之外的所有人的IEnumerable。
编辑:这里有另一个例子,使用音乐专辑而不是人,可能更有意义。
class Album
{
   string Name {get; set;}
   int Priority {get;set;}
   string[] Aliases {get; set;}
{

class Program
{
var NeilYoungAlbums = new List<Album>
    {
        new Person{ Name = "Harvest (Remastered)", Priority = 4, Aliases = new string[] { "Harvest (1972)", "Harvest (2012)"}},
        new Person{ Name = "On The Beach", Priority = 6, Aliases = new string[] { "The Beach Album", "On The Beach (1974)"}},
        new Person{ Name = "Harvest", Priority = 3, Aliases = new string[] { "Harvest (1972)"}},
        new Person{ Name = "Freedom", Priority = 5, Aliases = new string[] { "Freedom (1989)"}}
    };
}

这里的想法是展示他的唱片目录,但我们希望跳过准重复的内容。


1
在这个例子中,较低的数字是否意味着“更高”的优先级? - samgak
1
@samgak Steve 的优先级为 4,而 Molly 的优先级为 3。两者都使用 Lefty 作为昵称,但是选择了 Molly 而不是 Steve,因此我认为较低的数字意味着更高的优先级。 - Tobias Tengler
@samgak 由于人员2已经被排除,人员2和3之间不存在冲突,因此人员3留下。 - AgentFire
@BasilKosovan,然后Greg会被过滤掉,因为他的优先级很低。这个想法是,如果你选择了最终结果并选择了所有的昵称,就不会有重复的了。我应该举一个更具体的例子而不是一个人。 - Sean O'Neil
@Steve 那就随便选一个,无所谓哪个。 - Sean O'Neil
显示剩余8条评论
3个回答

5

我会使用自定义的IEqualityComparer<T>来解决这个问题:

class Person
{
    public string Name { get; set; }

    public int Priority { get; set; }

    public string[] Nicknames { get; set; }
}

class PersonEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null) return false;

        return x.Nicknames.Any(i => y.Nicknames.Any(j => i == j));
    }

    // This is bad for performance, but if performance is not a
    // concern, it allows for more readability of the LINQ below
    // However you should check the Edit, if you want a truely 
    // LINQ only solution, without a wonky implementation of GetHashCode
    public int GetHashCode(Person obj) => 0;
}

// ...

var people = new List<Person>
{
    new Person { Name = "Steve", Priority = 4, Nicknames = new[] { "Stevo", "Lefty", "Slim" } },
    new Person { Name = "Karen", Priority = 6, Nicknames = new[] { "Kary", "Birdie", "Snookie" } },
    new Person { Name = "Molly", Priority = 3, Nicknames = new[] { "Mol", "Lefty", "Dixie" } },
    new Person { Name = "Greg", Priority = 5, Nicknames = new[] { "G-man", "Chubs", "Skippy" } }
};

var distinctPeople = people.OrderBy(i => i.Priority).Distinct(new PersonEqualityComparer());

编辑:

仅为完整起见,以下是可能的仅使用LINQ方法的解决方案:

var personNicknames = people.SelectMany(person => person.Nicknames
        .Select(nickname => new { person, nickname }));
var groupedPersonNicknames = personNicknames.GroupBy(i => i.nickname);
var duplicatePeople = groupedPersonNicknames.SelectMany(i => 
        i.OrderBy(j => j.person.Priority)
        .Skip(1).Select(j => j.person)
    );

var distinctPeople = people.Except(duplicatePeople);

4
仅使用LINQ的解决方案
var dupeQuery = people
    .SelectMany( p => p.Nicknames.Select( n => new { Nickname = n, Person = p } ) )
    .ToLookup( e => e.Nickname, e => e.Person )
    .SelectMany( e => e.OrderBy( p => p.Priority ).Skip( 1 ) );

var result = people.Except( dupeQuery ).ToList();

请查看.net fiddle示例


当,你比我快,我正要发布答案。别担心 :) - Caramiriel
@Caramiriel 抱歉 ;o) - Sir Rufo

2

这个代码只能运行一次,之后你需要清除集合或将结果存储在一个集合中。

Original Answer翻译成:"最初的回答"

var uniqueNicknames = new HashSet<string>();

IEnumerable<Person> uniquePeople = people
    .OrderBy(T => T.Priority) // ByDescending?
    .Where(T => T.Nicknames.All(N => !uniqueNicknames.Contains(N)))
    .Where(T => T.Nicknames.All(N => uniqueNicknames.Add(N)));

uniqueNicknames 可以通过 SelectMany、GroupBy 和 Having 进行初始化。 - Basil Kosovan
第一个where似乎不需要,因为它正在检查空集。 - Slai
1
@TobiasTengler 在我看来,这段代码比你的实现IEqualityComparer<T>GetHashCode返回常量值要好得多(public int GetHashCode(Person obj) => 0;)。 - Ivan Stoev
1
@IvanStoev 当然,这是有争议的。从“Where”语句执行加法操作似乎很荒谬。想象一下,如果有人在没有上下文的情况下查看此代码,他会看到一个“Where”语句,并正确地假设它执行过滤操作。这是一个不必要的副作用。 - Tobias Tengler
@IvanStoev 只是为了澄清一下,我并不是说他执行这个任务的加法操作是错误的。我对操作发生的位置感到困扰(没有双关语)。我讨厌引用文档来证明一个观点,但是 Where 文档中写道:根据谓词筛选值序列。在 Where 语句的上下文中执行添加操作并不是它的预期使用方式。但我的答案/代码也不完美,所以我现在停止抱怨。 - Tobias Tengler
显示剩余2条评论

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