DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Natural Types for Method Groups in C# — Smarter Overload Resolution

NaturalTypesForMethodGroupsInCSharp13

Natural Types for Method Groups in C# — Smarter Overload Resolution

In modern C#, method groups — collections of methods with the same name but different signatures — are frequently used in scenarios like:

  • Passing methods as delegates
  • LINQ projections
  • Action<>, Func<> assignments
  • Method references in event subscriptions

C# 13+ introduces an important enhancement in how the compiler handles these method groups by optimizing the resolution of a natural type (i.e., an expected delegate type) for a method group.

Let’s explore what changed, how it affects overload resolution, and what it means for you as a C# expert.


What Is a "Method Group"?

A method group is a set of method overloads identified by a shared name:

void Log(string message) { ... }
void Log(string message, LogLevel level) { ... }
Enter fullscreen mode Exit fullscreen mode

Calling Log without parentheses creates a method group, which the compiler tries to match to an expected delegate signature:

Action<string> logger = Log; // Resolves to Log(string)
Enter fullscreen mode Exit fullscreen mode

Old Behavior: Too Broad, Too Eager

Before C# 13, the compiler would:

  1. Collect all methods in the group (across scopes)
  2. Attempt to determine a "natural type" from the entire group
  3. Use the complete set, even if many overloads were clearly invalid

Problem?

  • ❌ Generic methods with incompatible arity could pollute the candidate list
  • ❌ Deep overload sets slowed down inference
  • ❌ Non-applicable overloads confused delegate conversion

New Behavior: Scoped Filtering First

The updated compiler algorithm:

  • Narrows overloads by scope first — prioritizing nearest declarations
  • Filters invalid overloads early, including:
    • Generic methods with incompatible arity
    • Overloads with constraints not met
  • Only considers outer scopes if no valid candidates are found

This follows the general overload resolution rules more strictly and efficiently.


Example

class A
{
    public void Process(string s) => Console.WriteLine($"A: {s}");
}

class B : A
{
    public void Process(string s, int level) => Console.WriteLine($"B: {s}, level {level}");

    public void Test()
    {
        Action<string> act = Process; // Picks Process(string) from base A
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, only applicable methods in the current or base scope are checked. If Process(string, int) isn't a match, the compiler backs off and checks A.Process(string).


Use Cases Benefiting from This Change

Scenario Benefit
✅ LINQ with overloads Fewer ambiguous matches
Func<> / Action<> assignment Better inference, faster resolution
✅ Generic constraints Compiler skips overloads with unmet conditions
✅ Layered class designs More predictable shadowing behavior

Final Thoughts

The Natural Type for Method Groups refinement is part of the C# compiler’s continuous pursuit of accuracy, performance, and clarity. While subtle, it improves:

  • Readability and maintainability in overload-rich code
  • Delegate conversions with complex method hierarchies
  • Compile-time inference for generics and scoping

Mastering C# means understanding how your code is resolved, not just what it does.


Written by: [Cristian Sifuentes] – .NET Compiler Whisperer | C# Overload Strategist | Clean Delegation Evangelist

Have you ever been bitten by method group ambiguity? Tell us your story!

Top comments (0)