【问题标题】:Convert list to dictionary using linq and not worrying about duplicates使用 linq 将列表转换为字典,而不用担心重复
【发布时间】: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&lt;string, List&lt;Person&gt;&gt;(或等效项)。
  • @Anthony Pegram - 只想保留其中一个。我将问题更新为更明确
  • 你可以在执行 ToDictionary 之前使用 distinct。但是您必须重写 person 类的 Equals() 和 GetHashCode() 方法,以便 CLR 知道如何比较 person 对象
  • @Sujit.Warrier - 你也可以创建一个相等比较器传递给Distinct

标签: c# linq dictionary


【解决方案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 的解决方案更好,但它也很有效。

【讨论】:

【解决方案4】:

这应该适用于 lambda 表达式:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);

【讨论】:

  • 必须是:personList.Distinct().ToDictionary(i =&gt; i.FirstandLastName, i =&gt; 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; 
        }
    

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

    【讨论】:

    • 我不敢相信这个答案不是投票最多的。 ToLookupDistinct 等都有可怕的性能影响。这是 AFAICT,唯一一个与原始 ToDictionary 执行速度大致相同的选项
    • 没错。其他答案会进行额外的迭代+额外的内存分配
    【解决方案7】:

    要处理消除重复,实现一个IEqualityComparer&lt;Person&gt; 可以在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);
              }
      

      【讨论】:

      【解决方案10】:

      大多数其他答案的问题是他们使用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;
          }
      

      因为索引器上的一个集合导致它添加键,它不会抛出,并且也只会进行一次键查找。你也可以给它一个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);
          }
          

          【讨论】:

          • 并不是说任何人都会尝试使用它,但不要尝试使用它。在迭代集合时修改集合是个坏主意。
          猜你喜欢
          • 2013-07-29
          • 1970-01-01
          • 2020-07-13
          • 2011-10-17
          • 2017-10-01
          • 2019-11-27
          • 2020-03-19
          • 2020-11-11
          相关资源
          最近更新 更多