Add rules for readonly members#1497
Conversation
Nigel-Ecma
left a comment
There was a problem hiding this comment.
Missing definition(s)?
4f351d6 to
8249812
Compare
BillWagner
left a comment
There was a problem hiding this comment.
This is now ready for final review @jskeet @Nigel-Ecma
standard/structs.md
Outdated
|
|
||
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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--;
}
}There was a problem hiding this comment.
Much more clear language came from adding the change that a readonly member has a ref readonly this parameter.
standard/structs.md
Outdated
| <!-- 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I'll defer to @jskeet and @Nigel-Ecma Both thought the example demonstrated a practice they wanted "strongly discouraged".
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I replaced the sample with one that's more obvious and clear.
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.
1f494ec to
67d9b1c
Compare
|
@jskeet I've updated per comments and last month's discussion. This should be ready for the next meeting and merging. |
jskeet
left a comment
There was a problem hiding this comment.
I've somewhat forgotten the context of the discussion, but this looks good to me with one nit.
Requested changes now addressed one way or another
Fixes #1339
Add a new section for the rules on
readonlymembers of structs.I didn't write a specific rule that
readonlymembers can modifystaticfields. 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
readonlyproperty andreadonlymethod modify the members of a direct member, which is allowed.