Skip to content

Proposal: Non-boxing default-interface methods#9971

Open
Copilot wants to merge 10 commits intomainfrom
copilot/propose-new-syntax-lowering
Open

Proposal: Non-boxing default-interface methods#9971
Copilot wants to merge 10 commits intomainfrom
copilot/propose-new-syntax-lowering

Conversation

Copy link

Copilot AI commented Jan 30, 2026

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 this modifier on interface members, which provides an implicit receiver typed as the implementing type, enabling non-boxing invocation on structs.

Proposed syntax

interface ICounter
{
    int Count { get; set; }
    
    this void Increment() => Count++;
}

Key design decisions

  • Simple this modifier: The this modifier indicates the member has an implicit receiver typed as the implementing type
  • Anonymous type parameter in lowering: The receiver is an anonymous type parameter constrained to the containing interface (implementation detail of lowering)
  • By-ref parameter: The receiver is passed by reference
  • Instance method semantics: this members are treated as instance methods on the interface for lookup/invocation, while lowering to static virtual methods
  • Receiver handling: All receivers are passed by reference
    • For structs, this avoids boxing
    • For classes, the caller passes a reference to a temporary local copy of the reference variable

Lowering

The this modifier lowers to static virtual methods with an anonymous type parameter and by-ref receiver:

static virtual void Increment<TSelf>(ref TSelf @this) where TSelf : ICounter => @this.Count++;
Original prompt

Please create a proposal PR based on the summary of this issue, in particular the proposed syntax and then the lowering.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
Copilot AI changed the title [WIP] Propose new syntax and lowering implementation Proposal: Non-boxing default-interface methods Jan 30, 2026
Copilot AI requested a review from agocke January 30, 2026 23:29
…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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Let's use the same ICounter definition for this example

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please use the ICounter example in this whole file instead of IFace

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ea54517 - replaced all IFace examples with ICounter throughout the file.

Co-authored-by: agocke <515774+agocke@users.noreply.github.com>
Copilot AI requested a review from agocke January 30, 2026 23:44
{
int Count { get; set; }

static this void Increment(ref TSelf @this) => @this.Count++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b0ec6e8 - simplified the proposal to use just this modifier with implicit receiver. Key changes:

  • Removed static keyword
  • 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>
Copilot AI requested a review from agocke January 31, 2026 04:00
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI requested a review from agocke January 31, 2026 04:13
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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI requested a review from agocke January 31, 2026 18:43

### 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot no you can keep the same ‘this’ modifier. We’re just going to change the lowering

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should read: the receiver is an anonymous type parameter constrained to the containing interface. It’s also a by-ref parameter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@agocke agocke marked this pull request as ready for review February 2, 2026 19:52
@agocke agocke requested a review from a team as a code owner February 2, 2026 19:52
Copilot AI requested a review from agocke February 2, 2026 20:05
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this bullet point is basically duplicated from above.

struct Counter : ICounter
{
public int Count { get; set; }
// Uses default Increment() implementation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have an example of lowering an override too?


### Properties and indexers

The `this` modifier can also be applied to properties and indexers:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about instance operators?

public int Count { get; set; }

// Custom implementation of the this member
static void ICounter.Increment<TSelf>(ref TSelf @this) => @this.Count += 2; // Custom increment
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could allow readonly this default interface members I guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants