Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal]: Extensions as static types #8474

Open
1 of 4 tasks
MadsTorgersen opened this issue Oct 2, 2024 · 6 comments
Open
1 of 4 tasks

[Proposal]: Extensions as static types #8474

MadsTorgersen opened this issue Oct 2, 2024 · 6 comments

Comments

@MadsTorgersen
Copy link
Contributor

Extensions as static types

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

Summary

Disallow use of an extension as an instance type. Just like static classes this means that it cannot be the type of a value or variable, and cannot be a type parameter.

Motivation

A large part of the complexity of the roles and extensions pair of features comes from allowing them as types of values. For roles, that is the core of the feature; an indispensable part of the design. For extensions, however, their "typeness" is a more peripheral aspect, mainly to do with disambiguation. If we can live without those aspects of the extension feature, we can ship it faster and with less implementation complexity and risk. This does not prevent us from adding roles later, and in the process "upgrading" extensions to also be instance types. It allows us to further stratify the work across multiple waves of effort.

Detailed design

Separate out the design for extensions from that for roles. Disallow the use of extension types as the type of values and variables, including in variable and member declarations, cast expressions, and as type arguments.

Inside extension declarations, change the type of this to be the underlying type rather than the extension type. Other members of the extension can still be accessed on (implicit or explicit) this, as they show up as extension members on the underlying type.

Drawbacks

The type of this in extension declarations

In the current design, the type of this in an extension declaration is the extension type itself. This enables inheritance-like lookup behavior, where the members of the extension take precedence over the members of the underlying type. With this having the underlying type, that would no longer be the case - members of the underlying type would win over extension members, even ones from the enclosing declaration.

This does not seem like a big loss in practice. Why would an extension declare a member that the underlying type would hide? Such an extension member would be effectively unusable, given the other restrictions of this proposal. In fact, such a declaration might warrant a warning:

public extension StringExtensions for string
{
    public string Length => ...; // Useless - warning?
    public bool IsUtf8 => ... Length ...; // Would bind to string.Length
}

It is still the case that the extension's members would compete on equal terms with other extensions and could clash with them. This is a special case of the more general ambiguity issue, that we will address next.

In this particular case, ambiguities would be quite rare. If the other extension is imported with a using it would lose due to existing "closeness" rules. If it is defined on a base type, it would lose due to existing overload resolution rules.

If we still think it is a problem, we could consider an additional closeness rule that an extension is closer than others inside its own declaration.

public extension E1 for string
{
    public void M() => ...;
}
extension E2 for string
{
    public void M() => ...;
    public void P => ... M() ...; // Ambiguity? Closeness rule?
}

It would likely be a breaking change if at a later point (when extensions were allowed as instance types) we changed the type of this to the extension type.

Disambiguation

The other place where the type-ness of extensions plays a part in the current design is in disambiguation between members of two imported extensions.

using E1; // Brings in void M() on string;
using E2; // Also brings in void M() on string;

"Hello".M(); // Ambiguous
((E1)"Hello").M(); // No longer allowed

Given current rules, ambiguities like this would have to be handled by playing tricks with using clauses. E.g. putting using inside vs outside of the namespace (the one inside is nearer). Or defining a helper extension member in a separate file that only imports one of the extensions, and simply redirects to it:

// OtherFile.cs
using E1;

extension HelperExtensions for string
{
    public void E1_M() => M();
}

We know from extension methods that ambiguities are rare, but do occur. If the above is not satisfactory, we could recognize very specific patterns in code (such as ((E1)"Hello").M()) or consider new syntax for disambiguation.

Alternatives

These possible supplementary mitigating features were mentioned in the Drawbacks section:

  • A warning when an extension declares a member that would be hidden by a member of the underlying type
  • An additional closeness rule preferring members of a given extension within the declaration of that extension
  • Syntax or special rules to help with disambiguation between extension members

Unresolved questions

Design meetings

@BlinD-HuNTeR
Copy link

If we can live without those aspects of the extension feature, we can ship it faster

Please ship it already in C# 13!!! Or in the first preview of C# 14, if possible 🙂

@julealgon
Copy link

If we can live without those aspects of the extension feature, we can ship it faster and with less implementation complexity and risk. This does not prevent us from adding roles later, and in the process "upgrading" extensions to also be instance types. It allows us to further stratify the work across multiple waves of effort.

As long as it doesn't compromise future designs (which it doesn't see to, considering it can be "expanded" to non-static later), I'm all for this as well.

In a sense, this is similar to "sealed by default, open as needed" best practice. The sealed is an extra constraint that can be removed later, same as static in this context.

@333fred
Copy link
Member

333fred commented Oct 2, 2024

If we can live without those aspects of the extension feature, we can ship it faster

Please ship it already in C# 13!!! Or in the first preview of C# 14, if possible 🙂

Neither of those things are likely to happen.

@HaloFour
Copy link
Contributor

HaloFour commented Oct 3, 2024

My only question might be how this would play for helper methods. Could a private extension method be in scope within the extension that could be called from other extension members? e.g.:

public extension StringExtensions for string {
    public void M() {
        this.HelperMethod();
    }

    private void HelperMethod() { }
}

or would you be expected to declare that helper method as a static method, like we do with extension methods today?

public extension StringExtensions for string {
    public void M() {
        HelperMethod(this);
    }

    private static void HelperMethod(string value) { }
}

The former would be nice...

@TahirAhmadov
Copy link

I I'm not sure why the prohibition of:

Disallow use of an extension as an instance type. Just like static classes this means that it cannot be the type of a value or variable, and cannot be a type parameter.

Necessitates the change to how this is treated. I think we can have our cake and eat it, too, in this case.

@333fred
Copy link
Member

333fred commented Oct 3, 2024

this is an instance variable. Definitionally, if the type of an instance variable cannot be an extension, then this cannot be that extension either.

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

No branches or pull requests

6 participants