-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion: Readonly members on structs (16.3, Core 3) #1710
Comments
FYI. @jaredpar |
This would allow better use of |
It isn't mentioned above, but it may be worth discussing if a Today, with |
Conceptually I like it, a lot. I do have some issues with it, though. First, such an attribute already exists, There's also the chicken&egg problem in that the BCL and ecosystem would have to be updated for this to be particularly useful otherwise these methods are stuck not being able to call out to other methods. Next there is the issue of what defines "pure". In this case we could keep it to situations where the compiler can safely not make defensive copies, but philosophically it extends beyond that. Perhaps Lastly, |
We would have to add another attribute, however. The existing attribute has no enforcement, as you indicated, and as such would be a breaking change to begin enforcing it now.
This is only applicable if we restrict the ability to only call other There could be a difference between a It can be summed as Depending on what LDM thinls, it could go one way or the other. If we did differentiate, then
Right, I called this out in the notes section. |
I'm 85% sure this has all been thrashed out before - but I'm unable to find it with a quick search. One of the real problems is that different developers will have different expectations. Some will expect this to mean that literally no changes are made, none at all, by the method. Others will expect this to mean that no externally visible changes are made. The former is rigorous and useful from both theoretical and practical perspectives. The later permits result caching (which can be really useful for performance) and other internal changes. Many of the original decisions in the design of C# were driven by a desire to avoid versioning issues as code evolves - this is, for example, one of the reasons why checked exceptions were not included. What restrictions on the evolution of my code do I have if I have a property declared like this:
My own opinion is that I don't think that adding internal caching should have any effect on the external interface; if I need to remove the readonly modifier to add caching, that could have very large cascading effects - and if it's a published API (e.g. via NuGet) then I might not be able to do it at all without breaking people. That said, I know smart developers who disagree with me fervently on this topic ... 😁 |
Another scenario to consider ... if you have something with a pure API, and then you modify it to generate audit logs (for legal compliance reasons), should you need to change the declaration of the API? |
@theunrepentantgeek, right. At its root, a |
Added a root note, at the top of the post, clarifying that |
Consider instead
|
It's not just that it's confusing, it's also ambiguous with other potential features. The compiler specifically chose to use The individual variations are the following for locals in that context:
I wouldn't want to use This feature was discussed in LDM when we discussed the struct S1 {
int i;
public void M() readonly { ... }
} That is putting the |
After the method seems reasonable to me. Will update the proposal later tonight. |
@jaredpar, where would you envision the keyword for properties? Is it before or after the public float LengthSquared
{
get readonly
{
return (x * x) +
(y * y);
}
}
// vs
public float LengthSquared
{
readonly get
{
return (x * x) +
(y * y);
}
} I would imagine that specifying it individually on the |
@tannergooding indeed it would be on the There is no ambiguity here hence
It actually applies in more cases than you would expect. There are a non-trivial number of structs which are just wrappers around reference types. Such structs could themselves be easily marked as |
I really hope this is considered. I was excited about ref-readonly/in...until I ran into the defensive copy problem. An all-or-nothing approach (declaring the struct readonly) makes using the ref-readonly feature difficult in my context (game development / graphics). I have a math library that is similar as the examples discussed - low level and mutable math types. Historically, a source of bugs have been when a function takes a math type as a ref, but the parameter is supposed to be readonly (basically have always used the honor system...). Also, while the types are mutable, more often than not the context they are used are in readonly collections (e.g. rendering that acts on the data, the data can only change during a simulation step...so its a "readonly view of the data"); having those collections return a writable reference breaks the design. So I was excited about having more options to express design intent and reduce errors with ref-readonly. To get around the defensive copies, however, has become something of a pain. I'm considering using extension methods to "fake" instance methods, but then there aren't options for instance properties/indexers.
I would argue most developers who would be in need of this feature are already pretty familiar with const methods in C++. |
Thanks for the examples provided here. This matches up with the other asks we've seen for this. One of the biggest items that held us from doing this at the same time we did |
While struct S1 {
int i;
public void M() const { ... }
} Otherwise proposal seems to be very useful in multiple scenarios and it would be very nice to have it available sooner rather than later. |
IMHO I'd rather save |
@HaloFour IMHO it could be reconciled with |
I don't like using The reason for using
There is a good case for |
I'm all in for this feature. Without it, the And, in C++, if you call a mutable method on a const object, you get an error. In current C#, you get a silent copy. This contradicts with the goal, if I understand correctly, the feature is all for performance.
In case the struct just wraps a reference, it does not need |
What about an expression-bodied getter/method ? |
I'm really glad
The simple case where the struct wraps a single reference type, sure. Might be an issue when the struct holds a number of references. But that case is probably exceedingly rare. @theunrepentantgeek, @jaredpar
qrli's comment about C++ const correctness made me think about this. Annotating ready-only methods to avoid silent copies is really a struct-only feature, and would basically be useless on a method for a reference type (at least I think so?). So the worry about caching in a reference type would be moot in my opinion, but for structs it certainly has interesting implications. The case where a struct has to cache some state in a read-only method would probably be rare, but I can see it - maybe a geometry intersection algorithm, or the like where you might need to keep track of some state incrementally as data is processed. And I really like the idea of giving more flexibility to using structs everywhere and in more contexts, e.g. low-allocation apps that manages data using large arrays of structs passed around by ref or ref-readonly. Not common in business apps, but certainly common for games. C++ of course has a solution to this scenario, the mutable keyword. When a field is marked as mutable, it can be modified in a const method. What are your thoughts in bringing this concept to C#? |
There is already an API available to do this hence I wouldn't want to add a language concept. void Example(in T value) {
ref T refValue = Unsafe.AsRef(in value);
...
} The Unsafe Type. There are lots of dangerous / powerful helpers there that let you subvert C# type system rules. Note though that it's the safety equivalent of using |
Yes that's the likely syntax. |
@jaredpar isn’t that a Core-only API? |
I have some reservations about allowing this specific option: It looks very non-obvious here that I just feel that this one is much more likely to trap beginners rather than help them. |
@Joe4evr I think that's because our brains are used to the C++ rules, where the position of the modifier matters a lot. Beginners do not have this. It is just readonly property or readonly method, in addition to readonly fields. |
Can add readonly modifier for readonly struct's member? That's definitely no effect so should not allowed. Proposal doc's restrictions section does not mention it. |
That is allowed. There are cases in the language where redundant modifiers exist and are allowed or even required. The application of |
I'm trying to understand exactly what the language should do when a The strictest approach would be to disallow calling any non-readonly methods on Another possibility is to have the language permit such calls by implicitly creating a local copy before invoking the non-readonly member. Optionally a compiler warning could be given. Now consider a program like the following: public struct S
{
public int i;
public readonly void M1()
{
// should create local copy
M2();
System.Console.Write(i);
}
void M2()
{
i = 42;
}
static void Main()
{
var s = new S { i = 1 };
s.M1();
}
} In this case, the expected output is Wondering if you have any thoughts on this @jaredpar @tannergooding. |
My opinion is that the most correct behavior is to disallow calling any non-readonly methods on However, I don't think we can necessarily do that give the way the existing The next best thing would be to give a warning. I expect this is what the I think that ultimately, it will come down to an LDM decision and @jaredpar's weigh in is likely much more important than mine 😄 |
Hindsight is 20-20. If I'd spent more time thinking about the
That would've caught cases like the following:
There is likely an unintended copy of
The language of my rule probably needs a bit of tweaking to avoid all the cases where you'd insert a warning for existing code. But hopefully it gets the basic point across. It would've also paved the way to have better warning support in |
This seems to predate the readonly structs feature. public struct S1
{
public readonly S2 s2;
public void M1()
{
s2.M2();
}
}
public struct S2
{
public int i;
public void M2()
{
i = 42;
}
static void Main()
{
var s1 = new S1();
s1.M1();
System.Console.Write(s1.s2.i);
}
} Just removing the It seems like the value proposition for |
This should be usable on members of interfaces (and why not on classes) as well: interface INumber<TNumber> where TNumber : struct, INumber<TNumber>
{
readonly TNumber Add(in TNumber other);
}
TNumber Add(in TNumber a, in TNumber b) where TNumber : struct, INumber<TNumber>
{
return a.Add(b); // shouldn't cause copying
} Calling |
@IllidanS4 I would suggest making a separate proposal. This was already implemented and the issue is only open to track the changes that need to be made to the spec. |
@333fred Alright, thanks, I was confused by the "Not Started" in the initial post. |
Fixed. |
What's the pure instance member meaning? |
It cannot mutate the containing struct. |
Readonly Instance Methods
Summary
Provide a way to specify individual instance members on a struct do not modify state, in the same way that
readonly struct
specifies no instance members modify state.It is worth noting that
readonly instance member
!=pure instance member
. Apure
instance member guarantees no state will be modified. Areadonly
instance member only guarantees that instance state will not be modified.All instance members on a
readonly struct
could be considered implicitlyreadonly instance members
. Explicitreadonly instance members
declared on non-readonly structs would behave in the same manner. For example, they would still create hidden copies if you called an instance member (on the current instance or on a field of the instance) which was itself not-readonly.Motivation
Today, users have the ability to create
readonly struct
types which the compiler enforces that all fields are readonly (and by extension, that no instance members modify the state). However, there are some scenarios where you have an existing API that exposes accessible fields or that has a mix of mutating and non-mutating members. Under these circumstances, you cannot mark the type asreadonly
(it would be a breaking change).This normally doesn't have much impact, except in the case of
in
parameters. Within
parameters for non-readonly structs, the compiler will make a copy of the parameter for each instance member invocation, since it cannot guarantee that the invocation does not modify internal state. This can lead to a multitude of copies and worse overall performance than if you had just passed the struct directly by value. For an example, see this code on sharplabSome other scenarios where hidden copies can occur include
static readonly fields
andliterals
. If they are supported in the future,blittable constants
would end up in the same boat; that is they all currently necessitate a full copy (on instance member invocation) if the struct is not markedreadonly
.Design
Allow a user to specify that an instance member is, itself,
readonly
and does not modify the state of the instance (with all the appropriate verification done by the compiler, of course). For example:Readonly can be applied to property accessors to indicate that
this
will not be mutated in the accessor.When
readonly
is applied to the property syntax, it means that all accessors arereadonly
.Readonly can only be applied to accessors which do not mutate the containing type.
Readonly can be applied to some auto-implemented properties, but it won't have a meaningful effect. The compiler will treat all auto-implemented getters as readonly whether or not the
readonly
keyword is present.Readonly can be applied to manually-implemented events, but not field-like events. Readonly cannot be applied to individual event accessors (add/remove).
Some other syntax examples:
public readonly float ExpressionBodiedMember => (x * x) + (y * y);
public static readonly void GenericMethod<T>(T value) where T : struct { }
The compiler would emit the instance member, as usual, and would additionally emit a compiler recognized attribute indicating that the instance member does not modify state. This effectively causes the hidden
this
parameter to becomein T
instead ofref T
.This would allow the user to safely call said instance method without the compiler needing to make a copy.
The restrictions would include:
readonly
modifier cannot be applied to static methods, constructors or destructors.readonly
modifier cannot be applied to delegates.readonly
modifier cannot be applied to members of class or interface.Drawbacks
Same drawbacks as exist with
readonly struct
methods today. Certain code may still cause hidden copies.Notes
Using an attribute or another keyword may also be possible.
This proposal is somewhat related to (but is more a subset of)
functional purity
and/orconstant expressions
, both of which have had some existing proposals.LDM history:
The text was updated successfully, but these errors were encountered: