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) { ... }
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)
Old Behavior: Too Broad, Too Eager
Before C# 13, the compiler would:
- Collect all methods in the group (across scopes)
- Attempt to determine a "natural type" from the entire group
- 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
}
}
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)