Skip to content

Add support for readonly members in structs #673

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

Open
wants to merge 8 commits into
base: draft-v8
Choose a base branch
from

Conversation

RexJaeschke
Copy link
Contributor

@RexJaeschke RexJaeschke commented Nov 27, 2022

No description provided.

@RexJaeschke RexJaeschke added this to the C# 8.0 milestone Nov 27, 2022
@RexJaeschke RexJaeschke marked this pull request as draft November 27, 2022 16:44
@RexJaeschke RexJaeschke added the type: feature This issue describes a new feature label Jul 22, 2023
@KalleOlaviNiemitalo
Copy link
Contributor

KalleOlaviNiemitalo commented Aug 16, 2023

This will also need to update the defensive copying rule like in #894.

Tracked as #928

@@ -502,6 +505,50 @@ Automatically implemented properties ([§15.7.4](classes.md#1574-automatically-i

> *Note*: This access restriction means that constructors in structs containing automatically implemented properties often need an explicit constructor initializer where they would not otherwise need one, to satisfy the requirement of all fields being definitely assigned before any function member is invoked or the constructor returns. *end note*

A *property_declaration* ([§14.7.1](classes.md#1471-general)) for an instance property in a *struct_declaration* may contain the *property_modifier* `readonly`. However, a static property shall not contain that modifier.
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Sep 26, 2023

Choose a reason for hiding this comment

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

Does "However, a static property shall not contain that modifier" also cover the case where the readonly modifier is on an accessor of the static property?

struct S {
    static int P {
        readonly get => 0;
        set {}
    }
}

Comment on lines +530 to +531
Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.

A readonly method may call a sibling property or indexer set accessor that is readonly. If a sibling member’s accessor is not explicitly or implicitly readonly, a compile-error occurs.
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Sep 26, 2023

Choose a reason for hiding this comment

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

Does "sibling" specifically mean an instance member of this? The term is not used with that meaning anywhere else in the specification. It is not clear that it excludes static members, or instance members of the same type but used on a different instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another thing that could be defined in §16.3.x as per suggestion on line 509


It is a compile-time error to have a `readonly` modifier on a property itself as well as on either of its `get` and `set` accessors.

If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should disallow this as well:

struct S {
    int P { // error CS8664: 'S.P': 'readonly' can only be used on accessors if the property or indexer has both a get and a set accessor
        readonly set { }
    }
}

Perhaps:

If an accessor has a readonly modifier, the property shall also have an accessor that does not have that modifier.

Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Sep 26, 2023

Choose a reason for hiding this comment

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

Or merge with the earlier rule

It is a compile-time error for a property to have a readonly modifier on both the get and the set accessors.

resulting in:

It is a compile-time error for a property to have a readonly modifier on all of its accessors. Note: To correct the error, move the modifier from the accessors to the property itself. end note

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, assuming CS8664 is the intended semantics and not a Roslyn quirk.

IIRC conformance allows additional members other than get & set so “all of its accessors” would be invalid in such a case. I'll offer the following:

Suggested change
If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
If the `get` or `set` accessor has a `readonly` modifier then a sibling `set` or `get` accessor shall exist and shall not have that modifier.

Could use “corresponding” rather than “sibling”

Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming the implementation allows a property to have a nonstandard increment accessor in addition to the standard get and set:

struct S {
    private int i;
    public int I {
        readonly get => i;
        increment => ++i;
    }
}

I think "all of its accessors" would be a good rule here. If the developer wants all accessors to be readonly, then they should make the whole property readonly, regardless of whether the accessors are standard or nonstandard. And if the developer wants the get accessor to be readonly but wants some nonstandard accessor not to be readonly, then the standard should not require them to make the whole property readonly, as that change would also affect the nonstandard accessor (or require contortions in the specification of the extension).

Copy link
Member

Choose a reason for hiding this comment

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

Well, the grammar currently only allows get and set accessors. (See 15.7.3). Given that, I'd prefer the simpler "all accessors", but I'm not too concerned one way or the other


### §cands-diffs-events Events

An *event_declaration* ([§14.8.1](classes.md#1481-general)) for an instance, non-field-like event in a *struct_declaration* may contain the *event_modifier* `readonly`. However, a static event shall not contain that modifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

OK; accessors of an event cannot have the readonly modifier, but the grammar does not allow any other modifiers on them either, so it does not need to be restated here.


It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly method declared in that struct.

Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.
Copy link
Contributor

Choose a reason for hiding this comment

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

OK, demo:

struct S {
    int P => 0; // not readonly
    
    int this[string s] => 0; // not readonly

    readonly void M() {
        _ = P; // warning CS8656: Call to non-readonly member 'S.P.get' from a 'readonly' member results in an implicit copy of 'this'.
        _ = this[""]; // warning CS8656: Call to non-readonly member 'S.this[string].get' from a 'readonly' member results in an implicit copy of 'this'.
        N(); // warning CS8656: Call to non-readonly member 'S.N()' from a 'readonly' member results in an implicit copy of 'this'.
    }

    void N() {}
}

The standard does not require these warnings and I think that's OK.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another demo, also OK:

struct S {
    private static int r;
    
    public ref int P {
        get => ref r; // not readonly
    }
    
    readonly void M() {
        this.P = 0; // warning CS8656: Call to non-readonly member 'S.P.get' from a 'readonly' member results in an implicit copy of 'this'.
    }
}

this.P is at the left side of the assignment, but this calls the get accessor and so the wording is OK.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree in both cases, these warnings are up to a compiler. The first example in particular does however raise another potential issue – see #1053.

Copy link
Member

Choose a reason for hiding this comment

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

I made a note in #1053: I think we should defer this until we spec out ref readonly parameter in C# 12.


Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.

A readonly method may call a sibling property or indexer set accessor that is readonly. If a sibling member’s accessor is not explicitly or implicitly readonly, a compile-error occurs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there already a rule against C.N here:

struct S {
    public int P {
        set {} // not readonly
    }
    
    public int this[string s] {
        set {} // not readonly
    }

    readonly void M() {
        this.P = 0; // error CS1604: Cannot assign to 'this.P' because it is read-only
        this[""] = 0; // error CS1604: Cannot assign to 'this[""]' because it is read-only
    }
}

class C {
    static void N(in S that) {
        that.P = 0; // error CS8332: Cannot assign to a member of variable 'in S' or use it as the right hand side of a ref assignment because it is a readonly variable
        that[""] = 0; // error CS8332: Cannot assign to a member of variable 'in S' or use it as the right hand side of a ref assignment because it is a readonly variable
    }
}

Not clear to me why these are separate diagnostic codes. Can the C# standard cover both S.M and C.N with the same rule?

Copy link
Contributor

Choose a reason for hiding this comment

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

The separate diagnostic codes (and text) are not mandated, that is a compiler choice – these messages are just (in the view of the compiler writer at least, YMMV) better than “error: LHS is read-only”

@BillWagner
Copy link
Member

rebased on 09/26/2023, various links also fixed.

@RexJaeschke RexJaeschke added the Review: pending Proposal is available for review label Oct 13, 2023
@RexJaeschke RexJaeschke marked this pull request as ready for review January 11, 2024 14:11
@jskeet jskeet added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Feb 7, 2024
@BillWagner BillWagner self-assigned this Feb 8, 2024
@sequestor sequestor bot added seQUESTered and removed reQUEST labels Feb 9, 2024
Copy link
Contributor

@jskeet jskeet left a comment

Choose a reason for hiding this comment

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

Overall it feels like we may be missing a brief section explaining the purpose of readonly members on structs.

I'd also expect there to be a change somewhere to what happens when member access via a readonly field (in either a class or a struct) invokes a readonly member... shouldn't we be removing a defensive copying step there? Apologies if I've missed it... I look forward to it being pointed out :)

@@ -1984,7 +1985,7 @@ Grammar notes:

> *Note*: The overlapping of, and priority between, alternatives here is solely for descriptive convenience; the grammar rules could be elaborated to remove the overlap. ANTLR, and other grammar systems, adopt the same convenience and so *method_body* has the specified semantics automatically. *end note*

A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)) and `async` ([§15.15](classes.md#1515-async-functions)) modifiers.
A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)), `async` ([§15.15](classes.md#1515-async-functions)), and `readonly` modifiers. However, it is a compile-time error for the *method_modifier* `readonly` to be used in a *method_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-methods).
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is strictly correct due to specifying "the method_modifier readonly" - but it's still confusing, as at first glance this looks like it violates the rule... It's a method_declaration including readonly, after all. But it's not a "method_modifier readonly".

class Test
{
    private int x;
    public ref readonly int GetXRef() => ref x;
}

It's only when it's part of a method_modifier that it's prohibited in a class. So this is only valid when Test is a struct, for example:

struct Test
{
    private static int x;
    public readonly ref readonly int GetXRef() => ref x;
}

I'm not saying that's a good example of when readonly is usable, but it does highlight that readonly can occur twice in a single declaration with different meanings, which is a bit confusing. Perhaps that deserves a note as well?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe a note is correct here. In the first example, readonly modifies the int. In the second, readonly modifies the method_declaration.

Copy link
Contributor

Choose a reason for hiding this comment

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

A note sounds like the way forward to me, particularly given your comment above from December 2024.

@@ -154,6 +153,7 @@ Structs differ from classes in several important ways:
- Instance field declarations for a struct are not permitted to include variable initializers ([§16.4.8](structs.md#1648-field-initializers)).
- A struct is not permitted to declare a parameterless instance constructor ([§16.4.9](structs.md#1649-constructors)).
- A struct is not permitted to declare a finalizer.
- Some kinds of members in structs are permitted to have the modifier `readonly` while that is not generally permitted for those same member kinds in classes.
Copy link
Contributor

Choose a reason for hiding this comment

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

While I can appreciate the desire not to repeat things, I suspect it's worth being more specific about which members can have readonly in structs. The "some" here feels really woolly.

@@ -502,6 +504,50 @@ Automatically implemented properties ([§15.7.4](classes.md#1574-automatically-i

> *Note*: This access restriction means that constructors in structs containing automatically implemented properties often need an explicit constructor initializer where they would not otherwise need one, to satisfy the requirement of all fields being definitely assigned before any function member is invoked or the constructor returns. *end note*

A *property_declaration* ([§15.7.1](classes.md#1571-general)) for an instance property in a *struct_declaration* may contain the *property_modifier* `readonly`. However, a static property shall not contain that modifier.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly property declared in that struct.
Copy link
Contributor

Choose a reason for hiding this comment

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

"attempt" feels more human than spec-like to me. What rule are we stating here, and can we do so more precisely?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with @jskeet and additionally we have some repetition here which I think should be factored out as it is for classes: under classes modifiers are generally described in subclauses under §15.3 Class members.

That same pattern can be repeated here with the current content of §16.3 Struct members becoming §16.3.1 General and adding §16.3.2 The readonly modifier.

In §16.3.2 the rules for readonly members can be given (not proposed text):

This would provide a single place for defining what a readonly member. The additional per-member differences could follow in §16.3 subclauses or remain as they are in §16.4 subclauses.

Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

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

I gave this a second read today, and added more comments.

@@ -3155,7 +3157,7 @@ ref_property_body

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

There are two kinds of *property_declaration*:
A *property_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.7.2](classes.md#1572-static-and-instance-properties)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *property_modifier* `readonly` to be used in a *property_declaration* that is not contained directly by a *struct_declaration* (([§16.4.11](structs.md#16411-automatically-implemented-properties)).
Copy link
Member

Choose a reason for hiding this comment

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

We should specify that the readonly modifier can appear on an individual accessor (typically get) as well. The most common forms would be:

public readonly int P => p; // readonly property

public int P2
{
    readonly get { return p; }
    set { p = value; }
}

Copy link
Member

@BillWagner BillWagner Mar 1, 2024

Choose a reason for hiding this comment

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

~~Also, in this and several related bullets, I'm curious why the adverb "directly" is needed? ~~

Kalle is right. Ignore this comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious why the adverb "directly" is needed?

public struct S {
    public class C {
        public readonly int P => 0;
    }
}

The property_declaration of P is contained in the struct_declaration of S, but not directly, and readonly should not be allowed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Meeting of 2025-05-14 - we think we probably need to add this, yes - we can't see where else it's specified.

@@ -1984,7 +1985,7 @@ Grammar notes:

> *Note*: The overlapping of, and priority between, alternatives here is solely for descriptive convenience; the grammar rules could be elaborated to remove the overlap. ANTLR, and other grammar systems, adopt the same convenience and so *method_body* has the specified semantics automatically. *end note*

A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)) and `async` ([§15.15](classes.md#1515-async-functions)) modifiers.
A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)), `async` ([§15.15](classes.md#1515-async-functions)), and `readonly` modifiers. However, it is a compile-time error for the *method_modifier* `readonly` to be used in a *method_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-methods).
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a note is correct here. In the first example, readonly modifies the int. In the second, readonly modifies the method_declaration.

@@ -3246,6 +3249,7 @@ For a ref-valued property the *ref_get_accessor_declaration* consists optional a
The use of *accessor_modifier*s is governed by the following restrictions:

- An *accessor_modifier* shall not be used in an interface or in an explicit interface member implementation.
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
Copy link
Member

Choose a reason for hiding this comment

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

I'd include accessors on this restriction. Either in this bullet, or as a separate bullet item.

Copy link
Contributor

Choose a reason for hiding this comment

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

The restriction in this bullet is only for accessors; it applies to accessor_modifier, rather than property_modifier or indexer_modifier.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest:

Suggested change
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
- The *accessor_modifier* `readonly` may only be used in a *property_declaration* or *indexer_declaration* that is contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).

Copy link
Contributor

Choose a reason for hiding this comment

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

Possibly "is permitted only" to match the following line?

Copy link
Contributor

Choose a reason for hiding this comment

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

@jskeet Good idea:

Suggested change
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
- The *accessor_modifier* `readonly` is permitted only in a *property_declaration* or *indexer_declaration* that is contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).


It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly method declared in that struct.

Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.
Copy link
Member

Choose a reason for hiding this comment

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

We haven't defined "sibling" anywhere in the spec. I suggest replacing it with "may call a ... declared in the same struct declaration" (emphasis mine for suggested text.

Copy link
Contributor

Choose a reason for hiding this comment

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

The wording "declared in the same struct declaration" would incorrectly cause a copy here:

struct S {
    void M(in S s) {}
    readonly void N(S s) {
        s.M(this); // must not make an implicit copy of `this`
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

While I wrote a short while ago in #1053 that I wasn’t intending to get into the defensive copy issue at this time re-reading comments like the above makes me think we have little choice.

Last year I suggested the common aspects of readonly need to be defined in single place and that including covering that readonly members may call mutating members but any mutations must not be visible afterwards – i.e. specify what not how. Do this and we should find wording things like the above easier.

And I think we'll need to define sibling, at least for this topic if not for the wider Standard.

@Nigel-Ecma
Copy link
Contributor

Overall it feels like we may be missing a brief section explaining the purpose of readonly members on structs.

I'd also expect there to be a change somewhere to what happens when member access via a readonly field (in either a class or a struct) invokes a readonly member... shouldn't we be removing a defensive copying step there? Apologies if I've missed it... I look forward to it being pointed out :)

See #1053

Copy link
Contributor

@Nigel-Ecma Nigel-Ecma left a comment

Choose a reason for hiding this comment

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

I think we need to pull out bits into a common ”readonly members” section so we have a single place to define what being a readonly member means.

Though I kept it in my suggestion changes I think “is contained directly by a struct_declaration” is a bit unwieldly, can we not use something like “is a member of a struct_declaration” – I don't think membership in the general sense ("member of the judo club") is automatically read to include grandchildren. But as I say I left it as it…

@@ -3155,7 +3157,7 @@ ref_property_body

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

There are two kinds of *property_declaration*:
A *property_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.7.2](classes.md#1572-static-and-instance-properties)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *property_modifier* `readonly` to be used in a *property_declaration* that is not contained directly by a *struct_declaration* (([§16.4.11](structs.md#16411-automatically-implemented-properties)).
Copy link
Contributor

Choose a reason for hiding this comment

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

Same suggestion as for methods above:

Suggested change
A *property_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.7.2](classes.md#1572-static-and-instance-properties)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *property_modifier* `readonly` to be used in a *property_declaration* that is not contained directly by a *struct_declaration* (([§16.4.11](structs.md#16411-automatically-implemented-properties)).
A *property_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.7.2](classes.md#1572-static-and-instance-properties)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.7.6](classes.md#1576-virtual-sealed-override-and-abstract-accessors)) and `extern` ([§15.6.8](classes.md#1568-external-methods)). Additionally a *property_declaration* that is contained directly by a *struct_declaration* may include the `readonly` modifier (§cands-diffs-methods).

@@ -3246,6 +3249,7 @@ For a ref-valued property the *ref_get_accessor_declaration* consists optional a
The use of *accessor_modifier*s is governed by the following restrictions:

- An *accessor_modifier* shall not be used in an interface or in an explicit interface member implementation.
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest:

Suggested change
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
- The *accessor_modifier* `readonly` may only be used in a *property_declaration* or *indexer_declaration* that is contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).

@@ -3897,7 +3902,7 @@ remove_accessor_declaration

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), and `extern` ([§15.6.8](classes.md#1568-external-methods)) modifiers.
An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *event_modifier* `readonly` to be used in an *event_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-events).
Copy link
Contributor

Choose a reason for hiding this comment

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

Following my previous suggestions:

Suggested change
An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *event_modifier* `readonly` to be used in an *event_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-events).
An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)) and `extern` ([§15.6.8](classes.md#1568-external-methods)) modifiers. Additionally an *event_declaration* that is contained directly by a *struct_declaration* may include the `readonly` modifier (§cands-diffs-methods).

@@ -4179,7 +4185,7 @@ ref_indexer_body

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

There are two kinds of *indexer_declaration*:
An *indexer_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *indexer_modifier* `readonly` to be used in an *indexer_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-indexers).
Copy link
Contributor

Choose a reason for hiding this comment

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

As above:

Suggested change
An *indexer_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *indexer_modifier* `readonly` to be used in an *indexer_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-indexers).
An *indexer_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)) and `extern` ([§15.6.8](classes.md#1568-external-methods)) modifiers. Additionally an *indexer_declaration* that is contained directly by a *struct_declaration* may include the `readonly` modifier (§cands-diffs-methods).

@@ -502,6 +504,50 @@ Automatically implemented properties ([§15.7.4](classes.md#1574-automatically-i

> *Note*: This access restriction means that constructors in structs containing automatically implemented properties often need an explicit constructor initializer where they would not otherwise need one, to satisfy the requirement of all fields being definitely assigned before any function member is invoked or the constructor returns. *end note*

A *property_declaration* ([§15.7.1](classes.md#1571-general)) for an instance property in a *struct_declaration* may contain the *property_modifier* `readonly`. However, a static property shall not contain that modifier.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly property declared in that struct.
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with @jskeet and additionally we have some repetition here which I think should be factored out as it is for classes: under classes modifiers are generally described in subclauses under §15.3 Class members.

That same pattern can be repeated here with the current content of §16.3 Struct members becoming §16.3.1 General and adding §16.3.2 The readonly modifier.

In §16.3.2 the rules for readonly members can be given (not proposed text):

This would provide a single place for defining what a readonly member. The additional per-member differences could follow in §16.3 subclauses or remain as they are in §16.4 subclauses.


It is a compile-time error to have a `readonly` modifier on a property itself as well as on either of its `get` and `set` accessors.

If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, assuming CS8664 is the intended semantics and not a Roslyn quirk.

IIRC conformance allows additional members other than get & set so “all of its accessors” would be invalid in such a case. I'll offer the following:

Suggested change
If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
If the `get` or `set` accessor has a `readonly` modifier then a sibling `set` or `get` accessor shall exist and shall not have that modifier.

Could use “corresponding” rather than “sibling”

Comment on lines +530 to +531
Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.

A readonly method may call a sibling property or indexer set accessor that is readonly. If a sibling member’s accessor is not explicitly or implicitly readonly, a compile-error occurs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Another thing that could be defined in §16.3.x as per suggestion on line 509

@Nigel-Ecma
Copy link
Contributor

It’s the next day and I’m commenting on my own introductory comment on my review as an important issue probably doesn't come across clearly in the review itself:

I think we need to pull out bits into a common ”readonly members” section so we have a single place to define what being a read-only member means.

Unless I'm missing something the Standard is missing a clear explanation of semantics of read-only members, and those semantics some might find “surprising”.

It is reasonable to assume that a read-only member reports on the current state of an object and does not modify it. C# does the latter but not necessarily the former...

If a read-only member attempts to modify the state a reasonable assumption is that an error would be produced, the member is read-only after all.

C# chooses a different approach: it makes a copy of the object, mutates the state of that copy, and then discards the copy. The C# choice could be “surprising”, something Roslyn seems to acknowledge by issuing a warning, but I don’t think the Standard is clear as it could be on the semantics as it is preoccupied with the “how” a compiler might achieve this result, the so called “defensive copies”.

Such an description doesn’t need to be long and complicated, it doesn’t need to justify C#’s choice, it just needs to be clear.

Refs: #673 (comment) & #1053

@@ -502,6 +504,50 @@ Automatically implemented properties ([§15.7.4](classes.md#1574-automatically-i

> *Note*: This access restriction means that constructors in structs containing automatically implemented properties often need an explicit constructor initializer where they would not otherwise need one, to satisfy the requirement of all fields being definitely assigned before any function member is invoked or the constructor returns. *end note*

A *property_declaration* ([§15.7.1](classes.md#1571-general)) for an instance property in a *struct_declaration* may contain the *property_modifier* `readonly`. However, a static property shall not contain that modifier.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly property declared in that struct.
Copy link
Contributor

@Nigel-Ecma Nigel-Ecma Jul 9, 2024

Choose a reason for hiding this comment

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

From MS Learn:

Readonly can be applied to property accessors to indicate that this will not be mutated in the accessor. The following examples have readonly setters because those accessors modify the state of member field, but do not modify the value of that member field.

public readonly int Prop1
{
    get
    {
        return this._store["Prop1"];
    }
    set
    {
        this._store["Prop1"] = value;
    }
}

This example demonstrates a subtly that might easily be missed from the statement on line 589:

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly property declared in that struct.

An example like (maybe exactly) this one should be included in the Standard with explanatory text to cover:

  • The “indirect” state of a readonly only struct can be modified; and
  • That the somewhat oxymoronic read-only set makes sense in C#

Unless there are such explanations already here somewhere…

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of an example. The "read only set that changes the state of a member" is a clear extension of what C# already permits:

public struct S
{
    private List<int> sequence;

    public IList<int> Sequence
    {
        get
        {
            sequence ??= new List<int>();
            return sequence;
        }
    }
}

Callers can modify the returned sequence. The property above can't be marked readonly until a future version of C# allows field initializers in struct (10, if I recall correctly).

@jskeet jskeet removed the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Dec 11, 2024

It is a compile-time error for an automatically implemented property in a `readonly` struct to have a `set` accessor.

An automatically implemented property declared inside a `readonly` struct need not have a `readonly` modifier, as its `get` accessor is implicitly assumed to be readonly.
Copy link
Member

Choose a reason for hiding this comment

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

Roslyn also treats auto property getter of non-readonly structs as implicitly readonly. Should this be intended in spec?

Copy link
Contributor

Choose a reason for hiding this comment

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

This behaviour of Roslyn avoids copying the non-readonly struct value when reading an automatically implemented property of an in parameter:

struct S {
    int I { get; set; }
    int J => 42;

    static int M(in S s) {
        int i = s.I; // does not copy s
        int j = s.J; // defensive copy, as in ((S)s).J
        return i + j;
    }
}

However, I don't think a conforming C# program can detect whether this copying happens, because the get accessor of S.I does not contain any user code that could mutate the original instance via an alias and check whether the mutation also affected this. (For C# implementations that target the CLR, it can be still detected by disassembling the IL code.) Therefore, I think the C# standard does not have to specify it.

Copy link
Member

Choose a reason for hiding this comment

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

I think the standard can remain quiet on this behavior. A conforming compiler shouldn't be required to add the readonly modifier in this instance.

@BillWagner BillWagner added the meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting label Apr 30, 2025
Copy link
Contributor

@jskeet jskeet left a comment

Choose a reason for hiding this comment

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

I suspect we can end up merging this in the meeting, but I'd like us to fix up as many issues as possible during the meeting to avoid creating a huge raft of follow-on issues. This feels like a PR with "lots of nits" rather than either "mostly clean" or "a few big changes needed".

@@ -1984,7 +1985,7 @@ Grammar notes:

> *Note*: The overlapping of, and priority between, alternatives here is solely for descriptive convenience; the grammar rules could be elaborated to remove the overlap. ANTLR, and other grammar systems, adopt the same convenience and so *method_body* has the specified semantics automatically. *end note*

A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)) and `async` ([§15.15](classes.md#1515-async-functions)) modifiers.
A *method_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods)), `override` ([§15.6.5](classes.md#1565-override-methods)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods)), `extern` ([§15.6.8](classes.md#1568-external-methods)), `async` ([§15.15](classes.md#1515-async-functions)), and `readonly` modifiers. However, it is a compile-time error for the *method_modifier* `readonly` to be used in a *method_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-methods).
Copy link
Contributor

Choose a reason for hiding this comment

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

A note sounds like the way forward to me, particularly given your comment above from December 2024.

@@ -3246,6 +3249,7 @@ For a ref-valued property the *ref_get_accessor_declaration* consists optional a
The use of *accessor_modifier*s is governed by the following restrictions:

- An *accessor_modifier* shall not be used in an interface or in an explicit interface member implementation.
- It is a compile-time error for the *accessor_modifier* `readonly` to be used in a *property_declaration* or *indexer_declaration* that is not contained directly by a *struct_declaration* (§16.4.11, §cands-diffs-indexers).
Copy link
Contributor

Choose a reason for hiding this comment

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

Possibly "is permitted only" to match the following line?


A *method_declaration* ([§15.6.1](classes.md#1561-general)) for an instance method in a *struct_declaration* may contain the *method_modifier* `readonly`. However, a static method shall not contain that modifier.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly method declared in that struct.
Copy link
Contributor

Choose a reason for hiding this comment

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

Another case of "attempt".


An *indexer_declaration* ([§15.9](classes.md#159-indexers)) for an instance indexer in a *struct_declaration* may contain the *indexer_modifier* `readonly`.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly indexer declared in that struct.
Copy link
Contributor

Choose a reason for hiding this comment

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

Another case of "attempt"


It is a compile-time error to have a `readonly` modifier on an indexer itself as well as on either of its `get` or `set` accessors.

If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
If the `get` accessor has a `readonly` modifier, the `set` accessor shall exist and shall not have that modifier.

Copy link
Member

Choose a reason for hiding this comment

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

The resolution here should follow the same pattern chosen for the similar restriction on line 509 regarding properties.


All *method_declaration*s of a partial method shall have a `readonly` modifier, or none of them shall have it.

### §cands-diffs-indexers Indexers
Copy link
Contributor

Choose a reason for hiding this comment

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

So just to check, that means an indexer can have a readonly set (so long as it doesn't have a readonly get as well)?

I guess if it's modifying the state of something referenced via the struct, that's reasonable. Odd though.

Copy link
Member

Choose a reason for hiding this comment

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

Yes. That's exactly why. @jnm2 has a proposal to allow this in more places: https://github.com/dotnet/csharplang/blob/main/proposals/readonly-setter-calls-on-non-variables.md (Note: not currently in the working set)

@BillWagner
Copy link
Member

rebased on dotnet/draft-v8

@@ -4043,7 +4048,7 @@ remove_accessor_declaration

*unsafe_modifier* ([§23.2](unsafe-code.md#232-unsafe-contexts)) is only available in unsafe code ([§23](unsafe-code.md#23-unsafe-code)).

An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), and `extern` ([§15.6.8](classes.md#1568-external-methods)) modifiers.
An *event_declaration* may include a set of *attributes* ([§22](attributes.md#22-attributes)) and any one of the permitted kinds of declared accessibility ([§15.3.6](classes.md#1536-access-modifiers)), the `new` ([§15.3.5](classes.md#1535-the-new-modifier)), `static` ([§15.6.3](classes.md#1563-static-and-instance-methods), [§15.8.4](classes.md#1584-static-and-instance-events)), `virtual` ([§15.6.4](classes.md#1564-virtual-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `override` ([§15.6.5](classes.md#1565-override-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `sealed` ([§15.6.6](classes.md#1566-sealed-methods)), `abstract` ([§15.6.7](classes.md#1567-abstract-methods), [§15.8.5](classes.md#1585-virtual-sealed-override-and-abstract-accessors)), `extern` ([§15.6.8](classes.md#1568-external-methods)), and `readonly` modifiers. However, it is a compile-time error for the *event_modifier* `readonly` to be used in an *event_declaration* that is not contained directly by a *struct_declaration* (§cands-diffs-events).
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo May 8, 2025

Choose a reason for hiding this comment

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

By the way, an extern event cannot work in .NET because its add and remove accessors cannot be explicitly declared and it is then not possible to decorate them with DllImportAttribute. This however is not a defect of the C# standard because DllImportAttribute is not specified there.

Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

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

This is a great example PR for our new process.

I didn't find any new issues in this review. The additional comments should provide a clear path to completion, or a couple possible paths.

I see either of these as possible:

  1. Review and build the list in the meeting. Merge, and create issues to address identified issues.
  2. Leave open at the meeting. Assign one person for edits, and a second for review and merge prior to the next meeting. Add one or more comments that note the expected plan for each open discussion offline. Edit and merge offline before the June meeting.

@@ -502,6 +504,50 @@ Automatically implemented properties ([§15.7.4](classes.md#1574-automatically-i

> *Note*: This access restriction means that constructors in structs containing automatically implemented properties often need an explicit constructor initializer where they would not otherwise need one, to satisfy the requirement of all fields being definitely assigned before any function member is invoked or the constructor returns. *end note*

A *property_declaration* ([§15.7.1](classes.md#1571-general)) for an instance property in a *struct_declaration* may contain the *property_modifier* `readonly`. However, a static property shall not contain that modifier.

It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly property declared in that struct.
Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of an example. The "read only set that changes the state of a member" is a clear extension of what C# already permits:

public struct S
{
    private List<int> sequence;

    public IList<int> Sequence
    {
        get
        {
            sequence ??= new List<int>();
            return sequence;
        }
    }
}

Callers can modify the returned sequence. The property above can't be marked readonly until a future version of C# allows field initializers in struct (10, if I recall correctly).


It is a compile-time error for an automatically implemented property in a `readonly` struct to have a `set` accessor.

An automatically implemented property declared inside a `readonly` struct need not have a `readonly` modifier, as its `get` accessor is implicitly assumed to be readonly.
Copy link
Member

Choose a reason for hiding this comment

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

I think the standard can remain quiet on this behavior. A conforming compiler shouldn't be required to add the readonly modifier in this instance.


It is a compile-time error to have a `readonly` modifier on a property itself as well as on either of its `get` and `set` accessors.

If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
Copy link
Member

Choose a reason for hiding this comment

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

Well, the grammar currently only allows get and set accessors. (See 15.7.3). Given that, I'd prefer the simpler "all accessors", but I'm not too concerned one way or the other


It is a compile-time error to attempt to modify the state of an instance struct variable via a readonly method declared in that struct.

Although a readonly method may call a sibling, non-readonly method, or property or indexer get accessor, doing so results in the creation of an implicit copy of `this` as a defensive measure.
Copy link
Member

Choose a reason for hiding this comment

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

I made a note in #1053: I think we should defer this until we spec out ref readonly parameter in C# 12.


All *method_declaration*s of a partial method shall have a `readonly` modifier, or none of them shall have it.

### §cands-diffs-indexers Indexers
Copy link
Member

Choose a reason for hiding this comment

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

Yes. That's exactly why. @jnm2 has a proposal to allow this in more places: https://github.com/dotnet/csharplang/blob/main/proposals/readonly-setter-calls-on-non-variables.md (Note: not currently in the working set)


It is a compile-time error to have a `readonly` modifier on an indexer itself as well as on either of its `get` or `set` accessors.

If the `get` accessor has a `readonly` modifier, the `set` shall exist and shall not have that modifier.
Copy link
Member

Choose a reason for hiding this comment

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

The resolution here should follow the same pattern chosen for the similar restriction on line 509 regarding properties.

@jnm2
Copy link
Contributor

jnm2 commented May 14, 2025

Could there be value in describing what a "readonly struct member" is, and apply that to each instance member type (method, property, indexer, event) without repeating the whole definition?

jskeet and others added 2 commits May 14, 2025 21:29
@RexJaeschke RexJaeschke removed the meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting label May 14, 2025
@RexJaeschke
Copy link
Contributor Author

On the 2025-05-14 call: After 20 minutes, and accepting/tweaking some changes, we decided this PR is not yet ready to be merged.

Action: Jon and Bill will review the remaining issues offline. For each remaining comment, they will add a new comment suggesting the disposition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Review: pending Proposal is available for review type: feature This issue describes a new feature
Projects
No open projects
Status: Slipped
Status: Slipped
Status: 👀 In review
Status: 👀 In review
Status: Slipped
Development

Successfully merging this pull request may close these issues.

7 participants