-
Notifications
You must be signed in to change notification settings - Fork 92
Add rules for readonly members
#1497
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
base: draft-v8
Are you sure you want to change the base?
Add rules for readonly members
#1497
Conversation
Nigel-Ecma
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing definition(s)?
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.
4f351d6 to
8249812
Compare
Respond to remaining feedback from the latest meeting.
BillWagner
left a comment
There was a problem hiding this 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
| - 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.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnm2 & @BillWagner why not just:
| - 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; however no modifications to the receiver are observable after the readonly member returns. |
Which just sticks to the required language semantics – which implementations are required to provide.
Note: I followed @BillWagner’s suggestion for the semantics, which are presumably this way as there is no concern over with possible observation between the call & return – if that presumption is incorrect then the wording will need modifying appropriately, e.g. by dropping “after the readonly member returns”.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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--;
}
}Co-authored-by: Joseph Musser <me@jnm2.com>
Co-authored-by: Joseph Musser <me@jnm2.com>
| - The member shall not reassign the value of `this` or 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just remembered refs!
| - The member shall not reassign the value of `this` or 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. | |
| - The member shall not reassign the value of or take a writable reference to `this`, to an instance field of the receiver, or to an instance field-like event (§15.8.2) of the receiver. |
Example (and same for field or field-like event as for this itself):
struct S
{
readonly void M()
{
ref readonly S s = ref this; // Allowed
ref S s = ref this; // Not allowed
M2(ref this); // Not allowed
M3(in this); // Allowed
}
void M2(ref S p) { }
void M3(in S p) { }
}| <!-- 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.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| - The member shall not reassign the value of `this` or 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this slight modification to Nigel's suggestion:
| - 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; however no modifications to the receiver are observable outside the scope of the readonly member. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels a little odd in terms of scope usually being about the meaning of names, but I'm ambivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing @jskeet’s above comment in my email I spotted that all three of the original lines 147-149 appear to have the wrong actor. Back here on github I see proposed modifications to 149 do not, but that leaves 147 & 148. It is not “The member shall not reassign” – the member is not the actor, the language semantics are by preventing such an assignment. So all three lines need rewording, I have not checked whether the same issue appears elsewhere in the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not mention observability because I think that is a thorny topic (see #1497 (comment)). I believe the fact that a write has happened can be observable through other effects besides reading the eventual value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, we can be far more definite. A readonly member is passed a ref readonly this, and therefore it cannot write to fields of this, nor can it call a non-readonly member on this because that would require passing ref this which it does not have. It must create a copy in order to do so, and this is exactly the concept of "defensive copy" which is demanded by the fact that a readonly member never gains a writeable reference to this.
I have more details now too on how any mutation that a readonly member somehow managed to do could cause access violations, since the this reference may be within readonly memory when calling a readonly member, but luckily we don't have to discuss that (and other side effects of mutations) because mutations are not legal through a readonly ref this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided that we should say a copy is taken, and the non-readonly member is invoked on the copy. We don't need to call it "defensive" and we don't need to explicitly say that an implementation can optimize the copy away.
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.