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

Merged
merged 11 commits into from
Jun 4, 2025

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 {}
    }
}

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

This would be addressed by my suggestion below, assuming I haven't missed anything.

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.

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

@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

@BillWagner
Copy link
Member

rebased on dotnet/draft-v8

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.

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

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.

@BillWagner I've looked through this and commented or thumbs-upped each one that sounded potentially blocking. Please could you:

  • Commit suggestions that you're happy with
  • Comment on my suggestions for deferring with an issue
  • Reply on anything else that doesn't come under one of those

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

This would be addressed by my suggestion below, assuming I haven't missed anything.

BillWagner and others added 3 commits June 3, 2025 14:27
Co-authored-by: Nigel-Ecma <[email protected]>
Co-authored-by: Jon Skeet <[email protected]>
Apply suggestions and edit recommendations from the May meeting.
@BillWagner
Copy link
Member

@jskeet

I've looked through this and commented or thumbs-upped each one that sounded potentially blocking. Please could you:

  • Commit suggestions that you're happy with
  • Comment on my suggestions for deferring with an issue
  • Reply on anything else that doesn't come under one of those

I made three commits today (June 3)

  1. Accept suggestions where we had consensus.
  2. Make edits where comments suggested a solution, but there wasn't a suggestion.
  3. Clarify language and headers around readonly being allowed in struct member declarations, but not in class member declarations.

Other open issues are described in #1339

I think this is ready to merge.

@BillWagner BillWagner added the meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting label Jun 3, 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.

Agreed, I think this is ready to merge, too. Will do so.

@jskeet jskeet dismissed Nigel-Ecma’s stale review June 4, 2025 11:29

Lots has changed since Nigel left comments, and we believe they've all been addressed.

@jskeet
Copy link
Contributor

jskeet commented Jun 4, 2025

Test failure:

❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::  Mismatched errors: Expected CS1593, CS1661, CS1678, CS8030, CS1688, CS1661, CS1676, CS1643, CS0126, CS0029, CS1662, CS1670, CS0029, CS1662; Was CS1593, CS1661, CS1678, CS8030, CS1688, CS1676, CS1643, CS0126, CS0029, CS1662, CS1670, CS0029, CS1662
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::  Details of actual errors:
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 5: CS1593: Delegate 'Class1.D' does not take 0 arguments
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 6: CS1661: Cannot convert anonymous method to type 'Class1.D' because the parameter types do not match the delegate parameter types
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 6: CS1678: Parameter 1 is declared as type 'long' but should be 'int'
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 9: CS8030: Anonymous function converted to a void returning delegate cannot return a value
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 12: CS1688: Cannot convert anonymous method block without a parameter list to delegate type 'Class1.E' because it has one or more out parameters
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 14: CS1676: Parameter 1 must be declared with the 'out' keyword
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 17: CS1643: Not all code paths return a value in anonymous method of type 'Class1.P'
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 18: CS0126: An object of a type convertible to 'int' is required
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 20: CS0029: Cannot implicitly convert type 'string' to 'int'
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 20: CS1662: Cannot convert anonymous method to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 25: CS1670: params is not valid in this context
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 32: CS0029: Cannot implicitly convert type 'string' to 'int'
❌Example tester-ExampleTester::file=tools/conversions.md:771-809,line=771::    Line 32: CS1662: Cannot convert anonymous method to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type

@jskeet
Copy link
Contributor

jskeet commented Jun 4, 2025

But conversions.md isn't changed by this PR, so I'll merge this and we'll need to fix it separately.

@jskeet jskeet merged commit 064ce22 into dotnet:draft-v8 Jun 4, 2025
5 of 7 checks passed
@BillWagner BillWagner deleted the readonly-members branch June 4, 2025 15:12
BillWagner added a commit to BillWagner/docs that referenced this pull request Jun 4, 2025
The standard now includes the language for `readonly` instance members in `struct` types. See dotnet/csharpstandard#673.

Remove the feature speclet from the learn site.
BillWagner added a commit to dotnet/docs that referenced this pull request Jun 4, 2025
* remove readonly speclet

The standard now includes the language for `readonly` instance members in `struct` types. See dotnet/csharpstandard#673.

Remove the feature speclet from the learn site.

* build error

* warnings, part 2
@jnm2
Copy link
Contributor

jnm2 commented Jun 4, 2025

@jskeet That is the error caused by #1334 (review) and fixed by #1336.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting 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