使用 linq 将列表转换为字典,而不用担心重复
Posted
技术标签:
【中文标题】使用 linq 将列表转换为字典,而不用担心重复【英文标题】:Convert list to dictionary using linq and not worrying about duplicates 【发布时间】:2011-03-20 03:23:09 【问题描述】:我有一个 Person 对象列表。我想转换为字典,其中键是名字和姓氏(串联),值是 Person 对象。
问题是我有一些重复的人,所以如果我使用这段代码就会爆炸:
private Dictionary<string, Person> _people = new Dictionary<string, Person>();
_people = personList.ToDictionary(
e => e.FirstandLastName,
StringComparer.OrdinalIgnoreCase);
我知道这听起来很奇怪,但我现在并不真正关心重复名称。如果有多个名字,我只想抓住一个。无论如何我可以在上面编写这段代码,所以它只需要其中一个名称并且不会因重复而爆炸?
【问题讨论】:
重复项(基于密钥),我不确定您要保留它们还是丢失它们?保留它们需要Dictionary<string, List<Person>>
(或等效项)。
@Anthony Pegram - 只想保留其中一个。我将问题更新为更明确
你可以在执行 ToDictionary 之前使用 distinct。但是您必须重写 person 类的 Equals() 和 GetHashCode() 方法,以便 CLR 知道如何比较 person 对象
@Sujit.Warrier - 你也可以创建一个相等比较器传递给Distinct
【参考方案1】:
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;
【讨论】:
@LukeH 次要注意:您的两个 sn-ps 不等价:LINQ 变体保留第一个元素,非 LINQ sn-p 保留最后一个元素? @toong:这是真的,绝对值得一提。 (尽管在这种情况下,OP 似乎并不关心他们最终会使用哪个元素。) 对于“第一个值”情况:nonLinq 解决方案执行两次字典查找,但 Linq 执行冗余对象实例化和迭代。两者都不理想。 @SerG 幸好字典查找通常被认为是 O(1) 操作,影响可以忽略不计。【参考方案2】:这是显而易见的非 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 不难忽略大小写。只需将所有键添加为大写即可。 我如何让这个不区分大小写 或者使用 StringComparer 创建字典,它会忽略大小写,如果这是您需要的,那么您的添加/检查代码并不关心您是否忽略大小写。 回答之前的 cmets,大约 10 年后...忽略大小写,我们应该使用Dictionary.ContainsKey
,比 Dictionary.Keys.Contains
快得多(O(1) 而不是 O(N )),并使用不区分大小写的字典。【参考方案3】:
使用 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 才能按预期完成这项工作。 忽略我之前的评论。见***.com/questions/543482/… 如果您想覆盖确定的不同程度,请查看***.com/questions/489258/…【参考方案4】:这应该适用于 lambda 表达式:
personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
【讨论】:
必须是:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
这仅在 Person 类的默认 IEqualityComparer 按名字和姓氏比较时才有效,忽略大小写。否则编写这样的 IEqualityComparer 并使用相关的 Distinct 重载。此外,您的 ToDIctionary 方法应该采用不区分大小写的比较器来匹配 OP 的要求。【参考方案5】:
您还可以使用ToLookup
LINQ 函数,然后您几乎可以将其与字典互换使用。
_people = personList
.ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase);
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary
这实际上将在LukeH's answer 中执行 GroupBy,但会提供 Dictionary 提供的散列。因此,您可能不需要将其转换为字典,只需在需要访问键值时使用 LINQ First
函数即可。
【讨论】:
【参考方案6】:您可以创建类似于 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;
在这种情况下,如果有重复,则最后一个值获胜。
【讨论】:
我不敢相信这个答案不是投票最多的。ToLookup
、Distinct
等都有可怕的性能影响。这是 AFAICT,唯一一个与原始 ToDictionary
执行速度大致相同的选项
没错。其他答案会进行额外的迭代+额外的内存分配【参考方案7】:
要处理消除重复,实现一个IEqualityComparer<Person>
可以在Distinct()
方法中使用,然后获取您的字典将很容易。
给定:
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);
【讨论】:
【参考方案8】:如果我们想要返回字典中的所有 Person(而不是只有一个 Person),我们可以:
var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
【讨论】:
对不起,忽略我的评论编辑(我找不到删除我的评论编辑的地方)。我只是想添加一个关于使用 g.First() 而不是 g.Select(x => x) 的建议。 “StringComparer.OrdinalIgnoreCase”怎么样?您的示例中的 GroupBy 会忽略它。【参考方案9】: 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);
【讨论】:
我建议你阅读Minimal, Complete and verifiable example来改进你的例子。【参考方案10】:大多数其他答案的问题是他们使用Distinct
、GroupBy
或ToLookup
,这会在后台创建一个额外的字典。同样 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;
因为索引器上的一个集合导致它添加键,它不会抛出,并且也只会进行一次键查找。你也可以给它一个IEqualityComparer
,例如StringComparer.OrdinalIgnoreCase
【讨论】:
【参考方案11】:使用 LINQ 的 foldLeft
等效功能
persons.Aggregate(new Dictionary<string,Person>(StringComparer.OrdinalIgnoreCase),
(acc, current) =>
acc[current.FirstAndLastName] = current;
return acc;
);
【讨论】:
【参考方案12】:从 Carra 的解决方案开始,你也可以这样写:
foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
myDictionary.Add(person.FirstAndLastName, person);
【讨论】:
并不是说任何人都会尝试使用它,但不要尝试使用它。在迭代集合时修改集合是个坏主意。以上是关于使用 linq 将列表转换为字典,而不用担心重复的主要内容,如果未能解决你的问题,请参考以下文章
Linq ToDictionary 匿名与具体类型 [重复]