有没有更好的方法使用LINQ来聚合一个字典?

8
我正在尝试从一个可枚举对象中构建一个字典,但我需要一个聚合器来处理所有可能重复的键。直接使用ToDictionary()有时会导致键重复。
在这种情况下,我有一堆时间条目({ DateTime Date, double Hours }),如果同一天出现多个时间条目,则我想要该天的总时间。也就是说,我需要一个自定义的聚合器,为字典条目提供唯一的键。
是否有比这更好的方法?
(这种方法确实有效。)
    private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries )
    {
        return
            timeEntries
                .GroupBy(te => new {te.Date})
                .Select(group => new {group.Key.Date, Hours = group.Select(te => te.Hours).Sum()})
                .ToDictionary(te => te.Date, te => te.Hours);
    }

我觉得我真的需要像这样的东西:

IEnumerable<T>.ToDictionary( 
    /* key selector : T -> TKey */, 
    /* value selector : T -> TValue */, 
    /* duplicate resolver : IEnumerable<TValue> -> TValue */ );

so...

timeEntries.ToDictionary( 
    te => te.Date, 
    te => te.Hours, 
    duplicates => duplicates.Sum() );

'解析器'可以是.First()或.Max()或其他任何东西。

或者类似的东西。


我有一个实现...当我在工作时,另一个答案出现了。

我的:

    public static Dictionary<TKey, TValue> ToDictionary<T, TKey, TValue>(
        this IEnumerable<T> input, 
        Func<T, TKey> keySelector, 
        Func<T, TValue> valueSelector, 
        Func<IEnumerable<TValue>, TValue> duplicateResolver)
    {
        return input
            .GroupBy(keySelector)
            .Select(group => new { group.Key, Value = duplicateResolver(group.Select(valueSelector)) })
            .ToDictionary(k => k.Key, k => k.Value);
    }

我希望已经有这样的东西了,但我猜没有。这将是一个不错的补充。
谢谢大家 :-)

你的意思是想要使键(key)不重复,还是想要删除重复项(dups)? - Abel
我更新了描述。尝试聚合重复项以使它们唯一,然后从中构建字典。 - Jonathan Mitchem
5个回答

6
public static Dictionary<KeyType, ValueType> ToDictionary
  <SourceType, KeyType, ValueType>
(
  this IEnumerable<SourceType> source,
  Func<SourceType, KeyType> KeySelector,
  Func<SourceType, ValueType> ValueSelector,
  Func<IGrouping<KeyType, ValueType>, ValueType> GroupHandler
)
{
  Dictionary<KeyType, ValueType> result = source
    .GroupBy(KeySelector, ValueSelector)
    .ToDictionary(g => g.Key, GroupHandler);
}

调用者:

Dictionary<DateTime, double> result = timeEntries.ToDictionary(
  te => te.Date,
  te => te.Hours,
  g => g.Sum()
);

3
如果重复键是一个问题,也许你的意思是ToLookup?相同的原则,但每个键可以有多个值...
private static ILookup<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries )
{
    return
        timeEntries
            .GroupBy(te => new {te.Date})
            .Select(group => new {group.Key.Date, Hours = group.Select(te => te.Hours).Sum()})
            .ToLookup(te => te.Date, te => te.Hours);
}

然后你只需要像这样做:
var lookup = CreateAggregatedDictionaryByDate(...);
foreach(var grp in lookup) {
    Console.WriteLine(grp.Key); // the DateTime
    foreach(var hours in grp) { // the set of doubles per Key
        Console.WriteLine(hours)
    }
}

或者当然可以使用SelectMany(从...从)方法。

1

我喜欢你的方法,因为它很清晰。但是,如果你想让它更加高效,你可以采取以下措施,在一个单一的Aggregate调用中完成所有聚合和分组,尽管有点复杂。

private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate(IEnumerable<TimeEntry> timeEntries)
{
    return timeEntries.Aggregate(new Dictionary<DateTime, double>(),
                                 (accumulator, entry) =>
                                    {
                                        double value;
                                        accumulator.TryGetValue(entry.Date, out value);
                                        accumulator[entry.Date] = value + entry.Hours;
                                        return accumulator;
                                    });
}

2
不错。有点复杂...但是没错。我想我真的不确定我在寻找什么。也许是为ToDictionary()提供第三个参数以解决重复项的重载? - Jonathan Mitchem

0
如果您访问字典的索引器,但没有找到任何内容,它会允许您设置返回数据类型的默认构造函数。例如,对于 double 类型,它将返回 0。我可能会这样做:
public void blabla(List<TimeEntry> hoho)
{
    Dictionary<DateTime, double> timeEntries = new Dictionary<DateTime, double>();
    hoho.ForEach((timeEntry) =>
        {
            timeEntries[timeEntry.Day] = 0;
        });

    hoho.ForEach((timeEntry) =>
        {
            timeEntries[timeEntry.Day] += timeEntry.Hours;
        });

}

只是使用List,因为由于未知原因,在ienumerable上没有实现.ForEach()扩展,尽管我想象实现将逐行相同,但您可以直接执行字面foreach(),这也是它在底层执行的操作。

我认为从可读性的角度来看,这更容易理解正在做什么,除非这不是您想要做的事情..


2
timeEntries[] += 调用中会生成 KeyNotFoundException: The given key was not present in the dictionary。你需要在使用 += 之前初始化字典值。 - Samuel Neff

0
你是否在寻找类似这样的东西?
private static Dictionary<DateTime, double> CreateAggregatedDictionaryByDate( IEnumerable<TimeEntry> timeEntries ) 
{ 
    return 
        (from te in timeEntries
        group te by te.Date into grp)
        .ToDictionary(grp => grp.Key, (from te in grp select te.Hours).Sum());
} 

是的,那正是我所拥有的,只是纯粹使用扩展方法语法。 - Jonathan Mitchem
我的方法与众不同,它将聚合操作放在了 ToDictionary 调用中,而不是先进行计算。 - Gabe

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