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

Add support for init accessors #978

Draft
wants to merge 11 commits into
base: draft-v9
Choose a base branch
from

Conversation

RexJaeschke
Copy link
Contributor

Notes:

  1. New section "§init-accessors Init accessors" contains the place-holder link ([§xxx](plug in here link to v9 records grammar)), which will need to be replaced by the corresponding grammar section number that results from the addition of the records feature (also in V9).
  2. The edits to the final para of "15.9.2 Indexer and Property Differences" rely on the corresponding text being changed by PR Reverse some V7 edits w.r.t Indexers #970, which at the time of this writing had not been merged.

@RexJaeschke RexJaeschke added the type: feature This issue describes a new feature label Oct 31, 2023
@RexJaeschke RexJaeschke added this to the C# 9.0 milestone Oct 31, 2023
@RexJaeschke RexJaeschke marked this pull request as draft October 31, 2023 23:48
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo left a comment

Choose a reason for hiding this comment

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

Should this say something about referencing an init accessor in an expression tree (System.Linq.Expressions)?

standard/classes.md Outdated Show resolved Hide resolved
Comment on lines +3661 to +3664
At the point an init accessor is invoked, the instance is known to be in the construction phase. Hence an init accessor may take the following actions in addition to what a set accessor can do:

1. Call other init accessors available through `this` or `base`
1. Assign `readonly` fields declared on the same type through `this`
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Nov 1, 2023

Choose a reason for hiding this comment

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

Apparently, it can also make a writable reference to a readonly field, but I'm not sure whether that is a C# 9 feature. However, unlike a constructor, it cannot assign to a get-only property. SharpLab:

public class C {
    private readonly int x;
    
    public int Y { get; }

    public int Z {
        get {
            // error CS0192: A readonly field cannot be used as a ref or out value (except in a constructor)
            ref int r = ref this.x;
            return 0;
        }
        init {
            // OK, despite readonly
            ref int r = ref this.x;
            
            // error CS0200: Property or indexer 'C.Y' cannot be assigned to -- it is read only
            this.Y = 0; 
        }
    }
}

- A property that has only a get accessor is said to be a ***read-only property***. It is a compile-time error for a read-only property to be the target of an assignment.
- A property that has only a set accessor is said to be a ***write-only property***. Except as the target of an assignment, it is a compile-time error to reference a write-only property in an expression.
- A property that has only an init accessor is said to be an ***init-only property***. Except as the target of an assignment during the construction phase of an object, it is a compile-time error to reference an init-only property in an expression.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does nameof count as referencing a property in an expression?

@KalleOlaviNiemitalo
Copy link
Contributor

Expression tree seems to be working as expected. SharpLab:

using System;
using System.Linq.Expressions;

public class C {
    public int M { get; init; }
}

public static class D {
    public static void Main()
    {
        Expression<Func<C>> expression1 = () => new C { M = 1 }; // OK
        ////Expression<Action<C>> expression2 = (c) => { c.M = 2; }; // error CS8852
        C c = expression1.Compile().Invoke();
        Console.WriteLine(c.M); // outputs 1
    }
}

@KalleOlaviNiemitalo
Copy link
Contributor

KalleOlaviNiemitalo commented Nov 1, 2023

Please add init to the interface_accessors grammar rule. Although an interface cannot be named in a new expression, the init accessor can be called via a type parameter constrained to that interface. SharpLab:

public interface I {
    int P { get; init; }
    int this[int index] { get; init; }
}

public class C : I {
    public int P { get; init; }
    public int this[int index] { get => 0; init {} }
}

static class Program
{
    static void M<T>() where T : I, new() {
        T t = new T() { P = 1, [5] = 6 };
    }
    
    static void Main()
    {
        M<C>();
    }
}

@RexJaeschke
Copy link
Contributor Author

Re @KalleOlaviNiemitalo's comment #978 (comment):

This suggests the following changes to the grammar:

interface_accessors
    : attributes? 'get' ';'
    | attributes? 'set' ';'
    | attributes? 'init' ';'
    | attributes? 'get' ';' attributes? ('set'|'init') ';'
    | attributes? ('set'|'init') ';' attributes? 'get' ';'
    ;

However, that will be superseded by changes to this same grammar by V8's PR #681, which adds support for default interface function members. We should revisit this once that PR has been merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature This issue describes a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants