Proposal: Non-boxing default-interface methods#9971
Conversation
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
…xtensions Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
| A new modifier combination `static this` is introduced for interface members. When applied to a method, property, or indexer, it indicates that the member should be treated as a static virtual member with an explicit receiver parameter: | ||
|
|
||
| ```csharp | ||
| interface IFace<TSelf> where TSelf : IFace<TSelf> |
There was a problem hiding this comment.
@copilot Let's use the same ICounter definition for this example
There was a problem hiding this comment.
Updated in 402dd79 - the Syntax section now uses ICounter with Count property and Increment method to match the Motivation section example.
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
| #### Source code | ||
|
|
||
| ```csharp | ||
| interface IFace<TSelf> where TSelf : IFace<TSelf> |
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
| { | ||
| int Count { get; set; } | ||
|
|
||
| static this void Increment(ref TSelf @this) => @this.Count++; |
There was a problem hiding this comment.
@copilot Actually I think we can simplify this proposal. Since we already marking the methods with this, why not remove the receiver parameter and make it implict, just like in regular instance methods. We can also remove static. But we still need the TSelf type parameter. The requirement should be that it must be the first type parameter on the type and it must have a recursive constraint.
There was a problem hiding this comment.
Done in b0ec6e8 - simplified the proposal to use just this modifier with implicit receiver. Key changes:
- Removed
statickeyword - Made receiver parameter implicit (like regular instance methods)
- Added "Type parameter requirements" section: TSelf must be the first type parameter with a recursive constraint
New syntax:
interface ICounter<TSelf> where TSelf : ICounter<TSelf>
{
this void Increment() => Count++;
}Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
| The `this` modifier is lowered to a static virtual method with an explicit receiver parameter. However, the member is **treated as an instance method on the containing interface** for lookup and invocation purposes. This means: | ||
|
|
||
| - The method can be invoked using instance method syntax on values of the implementing type | ||
| - For structs, the receiver is passed by reference (avoiding boxing) |
There was a problem hiding this comment.
@copilot technically, the type parameter is not required to be constrained to a struct, therefore all receivers are passed by reference. However, if the type parameter is a class, the caller should create a temporary local copy to avoid mutating the argument.
There was a problem hiding this comment.
Updated in b7ff24d - clarified that all receivers are passed by reference. For classes, the caller passes a reference to a temporary local copy of the reference variable (to avoid mutating the caller's variable if the method reassigns this). Also removed the "Class support" open question since this is now addressed.
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
| A new `this` modifier is introduced for interface members. When applied to a method, property, or indexer, it indicates that the member has an implicit receiver typed as the self-constrained type parameter (rather than the interface type): | ||
|
|
||
| ```csharp | ||
| interface ICounter<TSelf> where TSelf : ICounter<TSelf> |
There was a problem hiding this comment.
@copilot I actually don’t think the type parameter at the type level is necessary. Instead let’s use a type parameter on the method itself (for the receiver).
There was a problem hiding this comment.
Done in 76ad9e2 - changed the proposal to use a method-level type parameter instead of interface-level. The new syntax is this<TSelf> on each member:
interface ICounter
{
int Count { get; set; }
this<TSelf> void Increment() where TSelf : ICounter => Count++;
}
struct Counter : ICounter
{
public int Count { get; set; }
}The interface no longer needs to be generic. The type parameter is declared on the method itself and must be constrained to the containing interface.
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
|
|
||
| ### Syntax | ||
|
|
||
| A new `this<TSelf>` modifier is introduced for interface members. When applied to a method, property, or indexer, it declares a type parameter for the receiver and indicates that the member has an implicit receiver typed as that type parameter (rather than the interface type): |
There was a problem hiding this comment.
@copilot no you can keep the same ‘this’ modifier. We’re just going to change the lowering
There was a problem hiding this comment.
Done in 5438aef - reverted to the simple this modifier. The method-level type parameter is now an implementation detail of the lowering only, not visible in source syntax.
New syntax:
interface ICounter
{
this void Increment() => Count++;
}Lowered form (compiler-generated):
static virtual void Increment<TSelf>(ref TSelf @this) where TSelf : ICounter => @this.Count++;|
|
||
| 1. The type parameter `TSelf` must be constrained to the containing interface | ||
| 2. Within the member body, `this` has the type `TSelf` (not the interface type) | ||
| 3. The receiver is always passed by reference |
There was a problem hiding this comment.
@copilot this should read: the receiver is an anonymous type parameter constrained to the containing interface. It’s also a by-ref parameter
There was a problem hiding this comment.
Done in 5dfd6d8 - updated the wording throughout to clarify that the receiver is an anonymous type parameter constrained to the containing interface, passed by reference.
… only Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
|
|
||
| ## Drawbacks | ||
|
|
||
| ### Complexity |
There was a problem hiding this comment.
@copilot Please add as a primary drawback:
The type of this is not resolved by dynamic dispatch. Meaning that further explicit interface implementations will still have this as type TSelf where TSelf : <parent interface> rather than the concrete type.
| When `this` is applied to an interface member: | ||
|
|
||
| 1. Within the member body, `this` has the type of the implementing type (not the interface type) | ||
| 2. The receiver is an anonymous type parameter constrained to the containing interface, passed by reference |
There was a problem hiding this comment.
Can we somehow control whether the "receiver type parameter" has allows ref struct constraint?
| 2. The receiver is an anonymous type parameter constrained to the containing interface, passed by reference | ||
| - For structs, this avoids boxing | ||
| - For classes, the caller passes a reference to a temporary local copy of the reference variable (to avoid mutating the caller's variable if the method reassigns `this`) | ||
| 3. The member is lowered to a static virtual method with an anonymous type parameter and a by-ref receiver parameter |
There was a problem hiding this comment.
nit: this bullet point seems superfluous given there is a whole section for lowering below
| The `this` modifier is lowered to a static virtual method with an anonymous type parameter constrained to the containing interface, and a by-ref receiver parameter. However, the member is **treated as an instance method on the containing interface** for lookup and invocation purposes. This means: | ||
|
|
||
| - The method can be invoked using instance method syntax on values of the implementing type | ||
| - The receiver is an anonymous type parameter constrained to the containing interface, passed by reference |
There was a problem hiding this comment.
It looks like this bullet point is basically duplicated from above.
| struct Counter : ICounter | ||
| { | ||
| public int Count { get; set; } | ||
| // Uses default Increment() implementation |
There was a problem hiding this comment.
Can we have an example of lowering an override too?
|
|
||
| ### Properties and indexers | ||
|
|
||
| The `this` modifier can also be applied to properties and indexers: |
| public int Count { get; set; } | ||
|
|
||
| // Custom implementation of the this member | ||
| static void ICounter.Increment<TSelf>(ref TSelf @this) => @this.Count += 2; // Custom increment |
There was a problem hiding this comment.
Oh, okay, so it's not possible to override the default implementation using normal methods like for non-this default interface members? Is there a reason for that (presumably the compiler could lower such override to this static form)?
|
|
||
| ### Default implementations | ||
|
|
||
| If a `this` member has a body, that body serves as the default implementation: |
There was a problem hiding this comment.
I assume this is disabled on interface members without bodies. Should we clarify that?
|
|
||
| ### Manual static virtual + extension pattern | ||
|
|
||
| Developers can already achieve this behavior manually using static virtual methods and [C# 14 extension members](csharp-14.0/extensions.md): |
There was a problem hiding this comment.
It's not possible to "override" the default implementation with this though right?
|
|
||
| ## Open questions | ||
|
|
||
| 1. **Ref kind**: Should the implicit receiver be `ref`, `in`, or `ref readonly`? The proposal currently assumes `ref` for mutability. |
There was a problem hiding this comment.
We could allow readonly this default interface members I guess.
This PR adds a proposal document for non-boxing default-interface methods based on issue #9969.
Summary
Introduce a syntax for declaring default interface methods that avoid boxing when implemented by value types. This is achieved by using a
thismodifier on interface members, which provides an implicit receiver typed as the implementing type, enabling non-boxing invocation on structs.Proposed syntax
Key design decisions
thismodifier: Thethismodifier indicates the member has an implicit receiver typed as the implementing typethismembers are treated as instance methods on the interface for lookup/invocation, while lowering to static virtual methodsLowering
The
thismodifier lowers to static virtual methods with an anonymous type parameter and by-ref receiver:Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.