Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I really love the GroupBy LINQ method in C#.

One thing that I don't like though is that the Key is always called Key.

When I loop through the groups, Key doesn't say me anything. So I have to look at the rest of the context to understand what Key actually is.

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();
}

Of course you could argue it's a small thing, because you only have to look at the outer loop.

In my opinion it still eats 'braincycles' when you're reading it though. So if there are good alternatives that need little boilerplate I would like to use it.

There are some alternatives, but they all add boilerplate code or depend on comments - both I try to omit.

Option 1 - Extra variable inside loop

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

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

This solution adds unnecessary boilerplate code.

Option 2 - Extra 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();
}

This solution adds unnecessary boilerplate code.

Option 3 - Add comment close to the use of Key

I rather have expressive code then comments that get outdated.

Summary

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

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

So we get:

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

foreach ( var group in grouped_by_competition )
{
    [...]
    group.Competition.DoSomething();
}
like image 377
Dirk Boer Avatar asked Oct 17 '22 08:10

Dirk Boer


2 Answers

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

No, because the name has to be present at compile time, and your proposed extension method would rely on an Expression object being examined at run-time.

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. There has to be an actual compile-time name for the compiler to use.

(I'm lying. When you use the dynamic keyword, you tell the compiler to defer compile-time operations to run-time. This allows you to get away with tricks that don't resolve types with specific member names until run-time. It is in fact one way to meet your stated goal. However, running the compiler at run-time is potentially expensive, and abandons the compile-time type-safety that is a hallmark feature of C# and similar statically-typed languages. The dynamic keyword is an important feature in some scenarios, but I would not say that this particular use case justifies the cost.)

Personally, I don't find the name Key a problem. In one very important respect, it's very expressive: that is, it tells me that I am dealing with the key of a grouping. If I want to know more about it, its type is usually sufficient to elaborate further, and that's immediately available by hovering over it.

But, different strokes for different folks. If the goal is to have a named member different from Key, your anonymous type projection is IMHO the best you're going to get. Note that you can improve the conciseness by using the GroupBy() overload that allows you to project each group directly, instead of having the Select() method:

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

So, one other thing: when I wrote that "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", there's a bit of hand-waving there. That is, what's really important is that the compiler knows about the name at compile time.

Usually, this means that there is really a type with a named member having that name. But there is an exception to that rule, recently added to C#, which is the new tuple syntax using ValueTuple. With that, you can implicitly give a name without introducing an actual new type. The compiler just keeps track of it in the local method, and treats it as if it's an actual type.

The tuple syntax doesn't really improve over the anonymous type syntax, in this particular scenario. It's a little shorter, but not by much:

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

It does have the potential performance advantage of using the ValueTuple class; being a value type, there can be a significant reduction in GC overhead using it. But not all scenarios would benefit from that.

like image 199
Peter Duniho Avatar answered Oct 21 '22 08:10

Peter Duniho


If you are okay with stepping into the painful world of dynamic, then you can implement it. I wouldn't recommend it.

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));
    }
}
like image 42
NetMage Avatar answered Oct 21 '22 06:10

NetMage