Skip to content

Add rules for readonly members#1497

Merged
jskeet merged 7 commits intodotnet:draft-v8from
BillWagner:readonly-struct-member-rules
Feb 11, 2026
Merged

Add rules for readonly members#1497
jskeet merged 7 commits intodotnet:draft-v8from
BillWagner:readonly-struct-member-rules

Conversation

@BillWagner
Copy link
Member

Fixes #1339

Add a new section for the rules on readonly members of structs.

I didn't write a specific rule that readonly members can modify static fields. There's no rule prohibiting it, so it should be allowed.

I chose language consistent with the future work on #1053 to consider. One popular implementation creates a "defensive copy" of the receiver to invoke a non-readonly member.

Finally, add an example that demonstrates the "direct member of a struct" rule by having a readonly property and readonly method modify the members of a direct member, which is allowed.

@BillWagner BillWagner added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label Dec 4, 2025
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.

Missing definition(s)?

@BillWagner BillWagner force-pushed the readonly-struct-member-rules branch from 4f351d6 to 8249812 Compare December 18, 2025 16:04
Copy link
Member Author

@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 now ready for final review @jskeet @Nigel-Ecma


- The member shall not reassign the value of an instance field of the receiver.
- The member shall not reassign the value of an instance field-like event (§15.8.2) of the receiver.
- A readonly member may call a non-readonly member using the receiver instance only if it ensures no modifications to the receiver are observable after the readonly member returns.
Copy link
Contributor

Choose a reason for hiding this comment

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

What does a readonly member do to ensure this? I would have thought it was an error to call a non-readonly member using the receiver instance.

Copy link
Member Author

Choose a reason for hiding this comment

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

The roslyn implementation creates a defensive copy, and calls the non-readonly member using that copy as this.

There are also instances where static analysis determines that the call doesn't modify state, so the copy is elided.

That's the reason for the language that doesn't require an implementation choice.

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about having it spec clearly that the compiler must perform this defensive copy? As written it sounds ambiguous, as though there might be something the language allows the user to do in order to achieve this.

Copy link
Member Author

Choose a reason for hiding this comment

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

In the meeting, @Nigel-Ecma pointed out that saying it must be a defensive copy is standardizing one implementation choice. While it's an obvious choice, it's not the only possible implementation. I see your point. How about this alternative wording:

Suggested change
- A readonly member may call a non-readonly member using the receiver instance only if it ensures no modifications to the receiver are observable after the readonly member returns.
- A readonly member may call a non-readonly member using the receiver instance. A conforming implementation must ensure no modifications to the receiver are observable after the readonly member returns.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's the only possible implementation that does not result in a compiler error, isn't it? I would think for portability reasons, all compilers should agree on whether or not it's an error. If it's not, then defensive copy is the only possible approach.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I'm following and I agree. I like Nigel's suggestion. The main things I wanted the spec to be clear on is that it is never a compiler error to make that call, while simultaneously no writes are being made to the current instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I'm following and I agree. I like Nigel's suggestion. The main things I wanted the spec to be clear on is that it is never a compiler error to make that call, while simultaneously no writes are being made to the current instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

I do have one quibble about "after it returns"- I want a guarantee that there are no writes before it returns, either. I think I can make an example with a callback that shows the difference being observable.

Copy link
Contributor

@jnm2 jnm2 Jan 9, 2026

Choose a reason for hiding this comment

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

Here is the example. It's not sufficient to say that readonly means that no writes are observable after the method returns. No writes should happen at all, within the memory of the this instance, if the method is readonly. I believe "observable" is also tricky to define and I would leave it out. If we keep "observable," I'd want to carefully define it, since at the level of subtle threading behaviors and such, there can be observable differences if you do even a "no-op" write, differences which go beyond observing the eventual value.

using System;

class Program
{
    static void Main()
    {
        var storageLocation = new S();

        Console.WriteLine(storageLocation.SomeField); // 0

        storageLocation.M(() =>
        {
            Console.WriteLine(storageLocation.SomeField); // 1
        });

        Console.WriteLine(storageLocation.SomeField); // 0
    }
}

struct S
{
    public int SomeField;

    public void M(Action someCallback)
    {
        // No writes are observable *after* M returns, yet crucially,
        // writes are observable. Readonly should disallow this scenario.

        SomeField++;
        someCallback();
        SomeField--;
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Much more clear language came from adding the change that a readonly member has a ref readonly this parameter.

<!-- markdownlint-disable MD028 -->

<!-- markdownlint-enable MD028 -->
> *Example*: The following code demonstrates the reassigning and modifying an instance field. Obfuscating the intent of `readonly` in this manner is discouraged:
Copy link
Contributor

Choose a reason for hiding this comment

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

What does "obfuscating the intent" mean in this context, and what is being discouraged? Looking at this from my own context, both approaches seem (individually) appropriate to me. I know when I'd use one and I know when I'd use the other.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll defer to @jskeet and @Nigel-Ecma Both thought the example demonstrated a practice they wanted "strongly discouraged".

Copy link
Contributor

Choose a reason for hiding this comment

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

@jnm2 – I can’t speak for @jskeet, for myself:

This is very much subjective. The usual understanding of readonly is, unsurprisingly, that state is not altered – this example alters what is clearly private state. It would be better without the set, as it is the readonly feels a bit like a false flag operation 😉 (you may collectively groan)

It feels a bit worse here as, depending on language, arrays may be value or reference types but are often thought of as more like values regardless – here flags certainly has the flavour of local value state of the struct.

It’s a subjective boundary – if the struct stored a reference to, say, a GUI window then not being able to change what window was reference, but being able to change the window’s content would likely not be viewed negatively.

Copy link
Contributor

@jnm2 jnm2 Jan 9, 2026

Choose a reason for hiding this comment

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

A reader should never gather more from the readonly modifier than its technical meaning, which is "the struct instance will not have changed due to this call." It does not imply anything more broad, such as that no mutations are being made in private state through an indirection.

For example, CancellationToken.Register is readonly (the whole struct is readonly), but no one would imagine that this means that no mutations are being done at all within indirect private state during the Register call.

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 worth discussing more. I would be happy with softening it to something like "used with care and with clear documentation" rather than outright discouragement. CancellationToken is a good example to discuss.

Copy link
Member Author

Choose a reason for hiding this comment

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

I replaced the sample with one that's more obvious and clear.

BillWagner and others added 6 commits January 29, 2026 14:22
Fixes dotnet#1339

Add a new section for the rules on `readonly` members of structs.

I didn't write a specific rule that `readonly` members can modify `static` fields. There's no rule prohibiting it, so it should be allowed.

I chose language consistent with the future work on dotnet#1053 to consider. One popular implementation creates a "defensive copy" of the receiver to invoke a non-readonly member.

Finally, add an example that demonstrates the "direct member of a struct" rule by having a `readonly` property and `readonly` method modify the members of a direct member, which is allowed.
Respond to remaining feedback from the latest meeting.
Co-authored-by: Joseph Musser <me@jnm2.com>
Co-authored-by: Joseph Musser <me@jnm2.com>
Use a more reasonable example.
@BillWagner BillWagner force-pushed the readonly-struct-member-rules branch from 1f494ec to 67d9b1c Compare January 29, 2026 19:23
@BillWagner BillWagner added the meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting label Jan 29, 2026
@BillWagner
Copy link
Member Author

@jskeet I've updated per comments and last month's discussion. This should be ready for the next meeting and merging.

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've somewhat forgotten the context of the discussion, but this looks good to me with one nit.

@Nigel-Ecma Nigel-Ecma self-requested a review February 11, 2026 18:55
@Nigel-Ecma Nigel-Ecma dismissed their stale review February 11, 2026 18:56

Requested changes now addressed one way or another

@jskeet jskeet merged commit d38cbde into dotnet:draft-v8 Feb 11, 2026
6 checks passed
@BillWagner BillWagner deleted the readonly-struct-member-rules branch February 12, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

meeting: discuss This issue should be discussed at the next TC49-TG2 meeting meeting: priority Review before meeting. Merge, merge with issues, or reject at the next TC49-TC2 meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define rules for "readonly" in struct types

4 participants