【问题标题】:Build Dictionary with LINQ使用 LINQ 构建字典
【发布时间】:2021-08-08 19:20:13
【问题描述】:

假设我们有一个变量“数据”,它是 ID 和子 ID 的列表:

var data = new List<Data>
{
    new()
    {
        Id = 1,
        ChildIds = new List<int> {123, 234, 345}
    },
    new()
    {
        Id = 1,
        ChildIds = new List<int> {123, 234, 345}
    },
    new()
    {
        Id = 2,
        ChildIds = new List<int> {678, 789}
    },
};

我想要一本带有 ChildId 和相关 Id 的字典。如果 ChildId 已经在字典中,它应该用新的 Id 覆盖。

目前我有这个代码:

var dict = new Dictionary<int, int>();
foreach (var dataItem in data)
{
    foreach (var child in dataItem.ChildIds)
    {
        dict[child] = dataItem.Id;
    }
}

这很好用,但我不喜欢我使用两个循环的事实。我更喜欢使用 Linq ToDictionary 以函数式方式构建字典。 使用 Linq 构建字典的最佳方法是什么?

为什么?我更喜欢功能代码而不是改变状态。除此之外,我只是好奇如何使用 Linq 建立字典 ;-)

【问题讨论】:

  • This works fine, but I don't like the fact that I am using two loops. I prefer to use Linq ToDictionary to build up the dictionary in a Functional way 为什么,你的方法有什么问题?你将如何使用这些数据?您是否尝试过 Linq,如果没有,请尝试一下,如果您有任何具体问题,请告诉我们。如果有,请发布相关的 Linq,我们可以提供进一步的帮助。
  • 你的方法很好,LINQ 唯一能做的就是抽象出你已经在做的事情。除了额外的错误更正之外,我认为您的实施没有任何问题。
  • 这段代码有效,但我更喜欢函数式代码而不是改变状态。除此之外,我只是好奇如何使用 Linq 建立字典 ;-)
  • @zaggler 完全一样,不过我喜欢 Linq 的简洁性和可读性。

标签: c# .net linq dictionary .net-core


【解决方案1】:

在这种情况下,您的 foreach 方法既可读又高效。因此,即使我是 LINQ 的粉丝,我也会使用它。该循环的好处是您可以轻松调试它或在必要时添加日志记录(例如无效的 id)。

但是,如果您想使用 LINQ,我可能会使用 SelectManyToLookup。前者用于展平像ChildIds 这样的子集合,后者用于创建与您的字典非常相似的集合。但是一个区别是它允许重复键,在这种情况下你会得到多个值:

ILookup<int, int> idLookup = data
    .SelectMany(d => d.ChildIds.Select(c => (Id:d.Id, ChildId:c)))
    .ToLookup(x => x.ChildId, x => x.Id);

现在您已经拥有所需的一切,因为它可以像字典一样使用,具有相同的查找性能。如果你想创建那个字典,你可以使用:

Dictionary<int, int> dict = idLookup.ToDictionary(x => x.Key, x => x.First());   

如前所述,如果您想用新 ID 覆盖重复项,只需使用 Last()

.NET-Fiddle:https://dotnetfiddle.net/mUBZPi

【讨论】:

  • 这应该被标记为正确答案。所有其他答案要么是“错误的”,要么不是那么好。
【解决方案2】:

SelectMany linq 运算符实际上有一些鲜为人知的重载。其中一个有一个结果收集器,它是您场景的完美用例。

以下是将其转换为字典的示例代码 sn-p。请注意,我必须使用 Distinct,因为您有 2 个值为 1 的 id,其中有一些重复的子 id,这会给字典带来问题。

void Main()
{
    // Get the data
    var list = GetData();
    
    // Turn it into a dictionary
    var dict = list
        .SelectMany(d => d.ChildIds, (data, childId) => new {data.Id, childId})
        .Distinct()
        .ToDictionary(x => x.childId, x => x.Id);

    // show the content of the dictionary
    dict.Keys
        .ToList()
        .ForEach(k => Console.WriteLine($"{k} {dict[k]}"));
}

public List<Data> GetData()
{
    return 
        new List<Data>
        {
            new Data
            {
                Id = 1,
                ChildIds = new List<int> {123, 234, 345}
            },
            new Data
            {
                Id = 1,
                ChildIds = new List<int> {123, 234, 345}
            },
            new Data
            {
                Id = 2,
                ChildIds = new List<int> {678, 789}
            },
        };
}

public class Data
{
    public int Id { get; set; }
    public List<int> ChildIds { get; set; }
}

【讨论】:

  • 这是一个绝妙的答案。我不知道 SelectMany 中的这种重载。
  • 如果输入稍微不同的数据集,此代码将引发异常。只需将数据中的 678 替换为 123。
【解决方案3】:

方法是创建 Id 和 ChildId 的每个组合的对,并构建它们的字典:

        var list = new List<(int Id, int[] ChildIds)>()
        {
            (1, new []{10, 11}),
            (2, new []{11, 12})
        };

        var result = list
            .SelectMany(pair => pair.ChildIds.Select(childId => (childId, pair.Id)))
            .ToDictionary(p => p.childId, p => p.Id);

ToDictionary 会在有重复键时抛出,为避免这种情况,您可以查看 this answer 并创建自己的 ToDictionary:

    public static Dictionary<K, V> ToDictionaryOverWriting<TSource, K, V>(
        this IEnumerable<TSource> source,
        Func<TSource, K> keySelector,
        Func<TSource, V> valueSelector)
    {
        Dictionary<K, V> output = new Dictionary<K, V>();
        foreach (TSource item in source)
        {
            output[keySelector(item)] = valueSelector(item);
        }
        return output;
    }

【讨论】:

    【解决方案4】:

    使用 LINQ,您可以实现如下结果:

    Dictionary<int, int> dict = (from item in data
                                 from childId in item.ChildIds
                                 select new { item.Id, childId}
                                 ).Distinct()
                                 .ToDictionary(kv => kv.childId, kv => kv.Id);
    
    

    更新:

    foreach 循环完全兼容的版本将使用group byLast(),而不是Distict()

    Dictionary<int, int> dict2 = (from item in data
                                    from childId in item.ChildIds
                                    group new { item.Id, childId } by childId into g
                                    select g.Last()
                                    ).ToDictionary(kv => kv.childId, kv => kv.Id);
    

    正如一些人已经指出的那样,依赖于输入元素的顺序并不会让人感觉“实用”。 LINQ 表达式变得比原来的 foreach 循环更加复杂。

    【讨论】:

    • 感谢您的解决方案,虽然我得到了以下异常:System.ArgumentException: 'An item with the same key has already been added.
    • 考虑到如果一个孩子已经在字典中,则需要覆盖,这个 LINQ 应该在构建字典之前执行 GroupBy() 后跟 Last() 以从重复项中选择最后一个.
    • @Rogier Distinct() 丢失,因为您有重复的数据。
    • 这是错误的。 Distinct 不仅适用于这里的键,它适用于始终是不同值的键值对。
    • 你说得对,Equals 比较匿名类型的值。但是,如果您在数据中将 678 替换为 123,您的代码仍然会引发异常
    【解决方案5】:

    SelectMany 中有一个 overload,它不仅可以使集合变平,还可以让您获得任何形式的结果。

    var all = data.SelectMany(
         data => data.ChildIds, //collectionSelector
         (data, ChildId) => new { data.Id, ChildId } //resultSelector 
    );
    

    现在,如果您想将all 转换为字典,您必须先删除重复的 ChildId。您可以使用GroupBy,如下所示,然后从每个组中选择最后一项(正如您在问题中所述,您希望随时覆盖 ID)。你的字典的键也应该是唯一的=ChildId:

    var dict = all.GroupBy(x => x.ChildId)
                  .Select(x => x.Last())
                  .ToDictionary(x => x.ChildId, x => x.Id);
    

    或者您可以编写一个实现IEquatable&lt;&gt; 的新类并将其用作resultSelector 的返回类型(而不是new { data.Id, ChildId })。然后写all.Reverse().Distinct().ToDictionary(x =&gt; x.ChildId);,这样它会根据你自己的Equals方法实现来检测重复。反向,因为你说你想要最后一次出现的重复。

    【讨论】:

      猜你喜欢
      • 2017-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多