Skip to content

Conversation

@RexJaeschke
Copy link
Contributor

@RexJaeschke RexJaeschke commented Oct 31, 2023

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

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

Comment on lines +3661 to +3846
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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I cloned the new para from the one preceding it (re a write-only property).

VS doesn't complain about nameof(P) where P is an init-only (or write-only) property.

According to §12.8.23, "Nameof expressions,"

A nameof_expression is a constant expression of type string, and has no effect at runtime. Specifically, its named_entity is not evaluated, and is ignored for the purposes of definite assignment analysis (§9.4.4.22).

This suggests that such a usage of the property name is not "a reference to a property."

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

An instance property containing an *init_accessor_declaration* is considered settable during the construction phase of the object, except when in a local function or lambda. The ***construction phase of an object*** includes the following:
- During execution of an *object_initializer* ([§12.8.17.2.2](expressions.md#1281722-object-initializers))
Copy link
Contributor

Choose a reason for hiding this comment

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

This should exclude a nested object initializer, like { I = 1 } in the following.

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

class D {
    public D(C c) { C = c; }
    public C C { get; }
}

class E {
    static void M() {
        new D(new C()) {
            C = { I = 1 } // error CS8852
        };
    }
}

#### §init-accessors Init accessors
An instance property containing an *init_accessor_declaration* is considered settable during the construction phase of the object, except when in a local function or lambda. The ***construction phase of an object*** includes the following:
Copy link
Contributor

Choose a reason for hiding this comment

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

"except when in a local function or lambda" can be understood as incorrectly disallowing the following.

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

class D {
    System.Func<C> Method() {
        C Local() {
            return new C { P = 1 };
        }
        return Local;
    }
}

An automatically implemented property (or auto-property for short), is a non-abstract, non-extern, non-ref-valued property with semicolon-only *accessor_body*s. An auto-property shall have a get accessor and may optionally have a set or init accessor.
When a property is specified as an automatically implemented property, a hidden backing field is automatically available for the property, and the accessors are implemented to read from and write to that backing field. The hidden backing field is inaccessible, it can be read and written only through the automatically implemented property accessors, even within the containing type. If the auto-property has no set accessor, the backing field is considered `readonly` ([§15.5.3](classes.md#1553-readonly-fields)). Just like a `readonly` field, a read-only auto-property may also be assigned to in the body of a constructor of the enclosing class. Such an assignment assigns directly to the read-only backing field of the property.
When a property is specified as an automatically implemented property, a hidden backing field is automatically available for the property, and the accessors are implemented to read from and write to that backing field. The hidden backing field is inaccessible, it can be read and written only through the automatically implemented property accessors, even within the containing type. If the auto-property has no set or init accessor, the backing field is considered `readonly` ([§15.5.3](classes.md#1553-readonly-fields)). Just like a `readonly` field, a read-only auto-property may also be assigned to in the body of a constructor of the enclosing class. Such an assignment assigns directly to the read-only backing field of the property. If the auto-property has an init accessor, the backing field may be assigned to during the construction phase of an object (§init-accessors).
Copy link
Contributor

Choose a reason for hiding this comment

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

The backing field should be considered readonly also when the auto-property has an init accessor. Otherwise, this would lead to a readonly struct having a non-readonly field.

readonly struct S {
    int I { get; init; }
}

@BillWagner BillWagner changed the base branch from draft-v9 to v9-init-accessors November 5, 2025 18:26
@BillWagner BillWagner force-pushed the Add-support-for-init-accessors branch 2 times, most recently from d829839 to 4916995 Compare November 5, 2025 20:27
@BillWagner BillWagner force-pushed the Add-support-for-init-accessors branch from 8fa2054 to f5cde7d Compare November 5, 2025 21:37
@BillWagner BillWagner marked this pull request as ready for review November 5, 2025 21:47
@BillWagner
Copy link
Member

@jskeet @RexJaeschke

This is the first C# 9 feature PR that I targeted for the updated V9 plan. Note that the base branch is now v9-init-accessors, a feature branch I created based on the updated draft-v9.

Note that there are failures at the moment:

  • Grammar validation fails because the init keyword isn't expected.
  • Test fixes are in Update test templates for V9 #1451, enabling C# 9 in our test templates.
  • Renumbering is because this feature depends on primary constructors in records.
  • Word converter fails because of new sections and anchors.

Before moving forward in either of the two directions, I'd like @RexJaeschke to give it a look and see if I missed anything.

I see two procedural ways to move forward (with options):

  • I can merge the PR into the feature branch.
  • I can push the changes directly to the feature branch, and add a comment pointing to this PR to preserve links to the existing comments.

Is there a preference?

In both plans, I could either squash the commits into one, or keep separate.

In either case, I'd open a fresh PR from v9-init-accessors to draft-v9. That can stay in draft mode. That's where I'd add pointers back to this PR.

My first thought is to push a single squashed commit into the feature branch. Open a new PR, and point back to this one with a summary of the comments.

Thoughts?

@Nigel-Ecma
Copy link
Contributor

Nigel-Ecma commented Nov 5, 2025

Hi @BillWagner

  • Grammar validation fails because the init keyword isn't expected.

I was curious so I took a peek. A failure is a failure so this doesn’t make any real difference to you, but the error isn’t down to the init keyword per se. The test that is failing is designed to fail – it is testing that right shift is not erroneously recognised – and the checker passes the test if the error message is what it expected. Antlr writes a long informative error message listing all the tokens it might have expected to find at the failing point of the code and that list now has init in it. Maybe I could look at supporting matching error messages (actually any stderr output) with a R.E. to avoid such false negatives… no promises! ;-)

@BillWagner
Copy link
Member

@Nigel-Ecma

No need to make corrections right now. This PR shows how our tools help us as we move to C# 9:

  • The grammar validator shows where we need updates to reflect the updated grammar in C# 9.
  • The test examples runner validates that our samples don't use newer features while we're still working on older standards. See Update test templates for V9 #1451 for updates in this branch to fix the failures in this PRs.
  • The renumbering tool highlights that this PR / features depends on records.

All told, this is good.

@BillWagner
Copy link
Member

Closing in favor of #1452

@BillWagner BillWagner closed this Nov 6, 2025
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.

4 participants