是否可以组合 GroupBy 和 Select 以获得正确命名的密钥?

Is it possible to combine a GroupBy and Select to get a proper named Key?

我非常喜欢 C# 中的 GroupBy LINQ 方法。

我不喜欢的一件事是密钥总是被称为 Key

当我在群组中循环时,Key 什么也没说。所以我必须查看上下文的其余部分以了解 Key 实际上是什么。

var grouped_by_competition = all_events
    .GroupBy(e => e.Competition);

foreach ( var group in grouped_by_competition )
{
    [...]
    // ok what is Key for kind of object? I have to look at the outer loop to understand
    group.Key.DoSomething();
}

当然你可以认为这是一件小事,因为你只需要查看外层循环。

在我看来,当您阅读它时,它仍然吃 'braincycles'。因此,如果有需要很少样板的好替代品,我想使用它。

有一些替代方案,但它们都添加了样板代码或依赖于注释——两者我都尽量省略。

选项 1 - 循环内的额外变量

var grouped_by_competition = all_events
    .GroupBy(e => e.Competition)

foreach ( var group in grouped_by_competition )
{
    var competition = group.Key;
    [...]
    competition.DoSomething();
}

此解决方案添加了不必要的样板代码。

选项 2 - 额外 Select

var grouped_by_competition = all_events
    .GroupBy(e => e.Competition)
    .Select(x => new { Competition = x.Key, Items = x } );

foreach ( var group in grouped_by_competition )
{
    [...]
    group.Competition.DoSomething();
}

此解决方案添加了不必要的样板代码。

选项 3 - 在 Key 的使用附近添加注释

我宁愿拥有富有表现力的代码,也不愿拥有过时的注释。

总结

理论上是否有可能设计出一种与匿名 class 功能类似的扩展方法?

var example = new { e.Competition };
example.Competition.DoSomething(); // property name deduced

所以我们得到:

var grouped_by_competition = all_events
    .NamedGroupBy(e => e.Competition);

foreach ( var group in grouped_by_competition )
{
    [...]
    group.Competition.DoSomething();
}

Is it theoretically possible to design an extension method that somehow works similar like the anonymous class functionaliy?

不,因为名称必须在编译时出现,而您提议的扩展方法将依赖于 运行 时检查的 Expression 对象。

只有 才能按名称引用成员,前提是被引用的对象实际上具有该名称的成员。必须有一个实际的编译时名称供编译器使用。

(我在撒谎。当您使用 dynamic 关键字时,您告诉编译器将编译时操作推迟到 运行-time。这允许您逃避那些直到 运行 时间才解析具有特定成员名称的类型的技巧。这实际上是实现您既定目标的一种方法。但是,运行 在 运行 编译器]-time 可能很昂贵,并且放弃了编译时类型安全,这是 C# 和类似静态类型语言的标志性特征。dynamic 关键字在某些情况下是一个重要特征,但我不会说这个特定的用例证明了成本。)

就我个人而言,我不觉得 Key 这个名字有问题。在一个非常重要的方面,它非常 富有表现力:也就是说,它告诉我我正在处理一个分组的键。如果我想了解更多,其 类型 通常足以进一步详细说明,将鼠标悬停在其上可立即获得。

但是,因人而异。如果目标是让命名成员不同于 Key,您的匿名类型投影是恕我直言,您将获得的最好结果。请注意,您可以通过使用允许您直接投影每个组的 GroupBy() 重载来提高简洁性,而不是使用 Select() 方法:

var grouped_by_competition = all_events
    .GroupBy(e => e.Competition, (k, g) => new { Competition = k, Items = g });

所以,还有一件事:当我写下 "The only way for you to be able to refer to the member by name, would be if the object being referenced actually has a member of that name" 时,那里有点挥手。也就是说,真正重要的是编译器在编译时知道名称。

通常,这意味着确实存在一个类型,其命名成员具有该名称。但该规则有一个例外,最近添加到 C#,即使用 ValueTuple 的新元组语法。这样,您就可以隐式地给出一个名称,而无需引入实际的新类型。编译器只是在本地方法中跟踪它,并将其视为实际类型。

在此特定情况下,元组语法并没有真正改进匿名类型语法。它有点短,但不会短很多:

var grouped_by_competition = all_events
    .GroupBy(e => e.Competition, (k, g) => (Competition: k, Items: g));

它确实具有使用 ValueTuple class 的潜在性能优势;作为一种值类型,使用它可以显着减少 GC 开销。但并非所有情况都会从中受益。

我不确定哪种代码对你来说是干净的,但是在 Select 方法中使用动态类型怎么样?

var grouped_by_competition = all_events
.GroupBy(e => e.Competition)
.Select(x => {{
   dynamic ret=new System.Dynamic.ExpandoObject();
   ret.Competition=x.Key;
   ret.Items=x;
   return ret;
} );
foreach( var group in grouped_by_competition )
{
    group.Competition.DoSomething();
}

如果你能接受进入dynamic的痛苦世界,那么你就可以实施它。我不推荐它。

class DynamicGroup<TKey, TElement> : DynamicObject, IGrouping<TKey, TElement> {
    private string keyname;
    private IGrouping<TKey, TElement> items;

    public DynamicGroup(IGrouping<TKey, TElement> pItems, string pKeyname) {
        items = pItems;
        keyname = pKeyname;
    }

    public IEnumerator<TElement> GetEnumerator() => items.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public TKey Key { get => items.Key; }

    public override bool TryGetMember(GetMemberBinder binder, out object result) {
        if (binder.Name == keyname) {
            result = items.Key;
            return true;
        }
        else {
            result = null;
            return false;
        }
    }
}

static class Ext {
    public static IEnumerable<dynamic> NamedGroupBy<TElement, TKey>(this IEnumerable<TElement> src, Expression<Func<TElement, TKey>> keySelector) {
        var ksb = keySelector.Body as MemberExpression;

        return src.GroupBy(keySelector.Compile()).Select(g => new DynamicGroup<TKey, TElement>(g, ksb.Member.Name));
    }
}