使用LINQ将列表转换为字典,无需担心重复项

216
我有一个人员对象的列表。我想将其转换为字典,其中键是第一个和最后一个名字(连接在一起),值是该人员对象。
问题在于,如果我使用以下代码,则会出现一些重复的人员:
private Dictionary<string, Person> _people = new Dictionary<string, Person>();

_people = personList.ToDictionary(
    e => e.FirstandLastName,
    StringComparer.OrdinalIgnoreCase);

我知道这听起来很奇怪,但现在我并不太关心重复的名称。如果有多个名称,我只想抓取其中一个。有没有办法修改上面的代码,以便它只获取一个名称,并且不会在遇到重复名称时出错?


1
重复项(基于键),我不确定您是想保留它们还是删除它们?如果要保留它们,则需要使用Dictionary<string,List<Person>>(或等效). - Anthony Pegram
@Anthony Pegram - 我只想保留其中一个。我更新了问题以更明确。 - leora
你可以在使用ToDictionary之前使用Distinct。但是你需要重写Person类的Equals()和GetHashCode()方法,以便CLR知道如何比较Person对象。 - Sujit.Warrier
@Sujit.Warrier - 你也可以创建一个相等比较器来传递给 Distinct - Kyle Delaney
13个回答

494

LINQ解决方案:

// Use the first value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);

// Use the last value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);

如果您更倾向于非LINQ的解决方案,那么可以这样做:

// Use the first value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    if (!_people.ContainsKey(p.FirstandLastName))
        _people[p.FirstandLastName] = p;
}

// Use the last value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    _people[p.FirstandLastName] = p;
}

7
@LukeH 小注:你的两个片段不等价:LINQ 变体保留第一个元素,非 LINQ 片段保留最后一个元素。 - toong
4
@toong: 这是真的,值得注意。 (尽管在这种情况下,原帖似乎不在乎他们最终得到哪个元素。) - LukeH
1
对于“第一个值”的情况:非Linq解决方案需要两次字典查找,但Linq则需要冗余的对象实例化和迭代。这两种方法都不是理想的。 - SerG
@SerG 感谢上帝,字典查找通常被认为是O(1)操作,并且影响可以忽略不计。 - MHollis

87

以下是显而易见的非 LINQ 解决方案:

foreach(var person in personList)
{
  if(!myDictionary.ContainsKey(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}

如果你不介意始终获得最后添加的一个,你可以像这样避免双重查找:

foreach(var person in personList)
{
    myDictionary[person.FirstAndLastName] = person;
}

是的,在工作中我们该更新 .net 2.0 框架了... @onof 区分大小写并不是很难。只需将所有键都转换为大写即可。 - Carra
我该如何使这个不区分大小写? - leora
12
如果需要忽略大小写,可以使用带有StringComparer的方式创建字典,这样添加和检查代码就不用关心是否忽略大小写。请注意,此翻译并未改变原意,尽可能通俗易懂。 - Binary Worrier
回答之前的评论,10年过去了...为了忽略大小写,我们应该使用Dictionary.ContainsKey,比Dictionary.Keys.Contains要快得多(O(1)而不是O(N)),并且使用大小写不敏感的字典。 - Alberto Chiesa
@A.Chiesa 这不是真的。两个调用都是类似的。都是 O(1)。后者在内部调用前者。 - nawfal
1
@nawfal 你说得对:框架保护你调用KeyCollection的Contains方法,该方法会调用ContainsKey。我认为我更喜欢更明确的ContainsKey,但现在我知道如果我看到野生的Contains,我不应该惊慌失措。谢谢! - Alberto Chiesa

48

使用Distinct()而不进行分组的Linq解决方案是:

var _people = personList
    .Select(item => new { Key = item.Key, FirstAndLastName = item.FirstAndLastName })
    .Distinct()
    .ToDictionary(item => item.Key, item => item.FirstFirstAndLastName, StringComparer.OrdinalIgnoreCase);

我不知道它是否比LukeH的解决方案更好,但它同样有效。


你确定这样行得通吗?Distinct 方法将如何比较您创建的新引用类型?我认为您需要传递某种 IEqualityComparer 到 Distinct 方法中,以使其按预期工作。 - Simon Gillbee
6
忽略我之前的评论。请查看https://dev59.com/tHRB5IYBdhLWcg3wtZIV。 - Simon Gillbee
如果您想要覆盖如何确定“不同”,请查看https://dev59.com/JHRB5IYBdhLWcg3w2613。 - James McMahon

30
这应该可以使用lambda表达式来实现:
personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);

2
必须是: personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i); - Gh61
9
只有当Person类的默认IEqualityComparer通过姓和名比较而忽略大小写时,此方法才有效。否则需编写这样的IEqualityComparer并使用相关的Distinct重载方法。另外,您的ToDictionary方法应采用不区分大小写的比较器以符合原帖要求。 - Joe

20

您可以创建一个类似于ToDictionary()的扩展方法,区别在于它允许重复。例如:

    public static Dictionary<TKey, TElement> SafeToDictionary<TSource, TKey, TElement>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector, 
        Func<TSource, TElement> elementSelector, 
        IEqualityComparer<TKey> comparer = null)
    {
        var dictionary = new Dictionary<TKey, TElement>(comparer);

        if (source == null)
        {
            return dictionary;
        }

        foreach (TSource element in source)
        {
            dictionary[keySelector(element)] = elementSelector(element);
        }

        return dictionary; 
    }

在这种情况下,如果有重复值,则最后一个值将获胜。


2
我简直不敢相信这个答案没有得到最高票数。ToLookupDistinct等都有严重的性能影响。据我所知,这是唯一一个与原始的ToDictionary速度大致相同的选项。 - Alberto Chiesa
没错。其他答案会多做一次迭代和额外的内存分配。 - Alex from Jitbit

17

您还可以使用 ToLookup LINQ 函数,这个函数几乎可以与 Dictionary 互换使用。

_people = personList
    .ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase);
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary

这基本上是在LukeH的答案中进行的GroupBy操作,但会提供一个Dictionary提供的哈希。因此,您可能不需要将其转换为Dictionary,只需在需要访问键的值时使用LINQ First函数即可。


中间浪费了枚举。使用GroupBy会是更好的选择(而不是ToLookup)。 - nawfal

7
为了处理去重,需要实现一个可以在Distinct()方法中使用的IEqualityComparer<Person>,然后获取你的字典将会变得容易。
给定:
class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.FirstAndLastName.Equals(y.FirstAndLastName, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(Person obj)
    {
        return obj.FirstAndLastName.ToUpper().GetHashCode();
    }
}

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

获取您的字典:

List<Person> people = new List<Person>()
{
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Jane Thomas" }
};

Dictionary<string, Person> dictionary =
    people.Distinct(new PersonComparer()).ToDictionary(p => p.FirstAndLastName, p => p);

4

如果我们想要返回字典中所有人(而不仅仅是一个人),我们可以这样做:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));

2
抱歉,请忽略我的评论编辑(我找不到删除评论编辑的地方)。我只是想建议使用 g.First() 而不是 g.Select(x => x)。 - Alex 75
'StringComparer.OrdinalIgnoreCase' 怎么样?在你的示例中,GroupBy 忽略了它。 - Valentine Zakharenko

3
大多数其他答案的问题在于它们使用了 DistinctGroupByToLookup,这会在内部创建额外的字典。同样,ToUpper 也会创建额外的字符串。 以下是我所做的事情,几乎完全复制了 Microsoft 的代码,只有一个变化:
    public static Dictionary<TKey, TSource> ToDictionaryIgnoreDup<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null) =>
        source.ToDictionaryIgnoreDup(keySelector, i => i, comparer);

    public static Dictionary<TKey, TElement> ToDictionaryIgnoreDup<TSource, TKey, TElement>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
    {
        if (keySelector == null)
            throw new ArgumentNullException(nameof(keySelector));
        if (elementSelector == null)
            throw new ArgumentNullException(nameof(elementSelector));
        var d = new Dictionary<TKey, TElement>(comparer ?? EqualityComparer<TKey>.Default);
        foreach (var element in source)
            d[keySelector(element)] = elementSelector(element);
        return d;
    }

因为在索引器上设置一个键会导致索引器添加该键,不会抛出异常,并且只进行一次键查找。您还可以为它提供一个,例如。

2
        DataTable DT = new DataTable();
        DT.Columns.Add("first", typeof(string));
        DT.Columns.Add("second", typeof(string));

        DT.Rows.Add("ss", "test1");
        DT.Rows.Add("sss", "test2");
        DT.Rows.Add("sys", "test3");
        DT.Rows.Add("ss", "test4");
        DT.Rows.Add("ss", "test5");
        DT.Rows.Add("sts", "test6");

        var dr = DT.AsEnumerable().GroupBy(S => S.Field<string>("first")).Select(S => S.First()).
            Select(S => new KeyValuePair<string, string>(S.Field<string>("first"), S.Field<string>("second"))).
           ToDictionary(S => S.Key, T => T.Value);

        foreach (var item in dr)
        {
            Console.WriteLine(item.Key + "-" + item.Value);
        }

我建议你通过阅读最小、完整和可验证的示例来改善你的示例。 - IlGala

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