Description
Readonly Instance Methods
- Prototype: DoneImplementation: DoneSpecification: Not Started
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
. A pure
instance member guarantees no state will be modified. A readonly
instance member only guarantees that instance state will not be modified.
All instance members on a readonly struct
could be considered implicitly readonly instance members
. Explicit readonly 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 as readonly
(it would be a breaking change).
This normally doesn't have much impact, except in the case of in
parameters. With in
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 sharplab
Some other scenarios where hidden copies can occur include static readonly fields
and literals
. 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 marked readonly
.
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:
public struct Vector2
{
public float x;
public float y;
public readonly float GetLengthReadonly()
{
return MathF.Sqrt(LengthSquared);
}
public float GetLength()
{
return MathF.Sqrt(LengthSquared);
}
public readonly float GetLengthIllegal()
{
var tmp = MathF.Sqrt(LengthSquared);
x = tmp; // Compiler error, cannot write x
y = tmp; // Compiler error, cannot write y
return tmp;
}
public float LengthSquared
{
readonly get
{
return (x * x) +
(y * y);
}
}
}
public static class MyClass
{
public static float ExistingBehavior(in Vector2 vector)
{
// This code causes a hidden copy, the compiler effectively emits:
// var tmpVector = vector;
// return tmpVector.GetLength();
//
// This is done because the compiler doesn't know that `GetLength()`
// won't mutate `vector`.
return vector.GetLength();
}
public static float ReadonlyBehavior(in Vector2 vector)
{
// This code is emitted exactly as listed. There are no hidden
// copies as the `readonly` modifier indicates that the method
// won't mutate `vector`.
return vector.GetLengthReadonly();
}
}
Readonly can be applied to property accessors to indicate that this
will not be mutated in the accessor.
public int Prop1
{
readonly get
{
return this._store["Prop1"];
}
readonly set
{
this._store["Prop1"] = value;
}
}
When readonly
is applied to the property syntax, it means that all accessors are readonly
.
public readonly int Prop2
{
get
{
return this._store["Prop2"];
}
set
{
this._store["Prop2"] = value;
}
}
Readonly can only be applied to accessors which do not mutate the containing type.
public int Prop3
{
readonly get
{
return this._prop3;
}
set
{
this._prop3 = value;
}
}
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.
// Allowed
public readonly int Prop4 { get; }
public int Prop5 { readonly get; }
public int Prop6 { readonly get; set; }
// Not allowed
public readonly int Prop7 { get; set; }
public int Prop8 { get; readonly set; }
Readonly can be applied to manually-implemented events, but not field-like events. Readonly cannot be applied to individual event accessors (add/remove).
// Allowed
public readonly event Action<EventArgs> Event1
{
add { }
remove { }
}
// Not allowed
public readonly event Action<EventArgs> Event2;
public event Action<EventArgs> Event3
{
readonly add { }
readonly remove { }
}
public static readonly event Event4
{
add { }
remove { }
}
Some other syntax examples:
- Expression bodied members:
public readonly float ExpressionBodiedMember => (x * x) + (y * y);
- Generic constraints:
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 become in T
instead of ref T
.
This would allow the user to safely call said instance method without the compiler needing to make a copy.
The restrictions would include:
- The
readonly
modifier cannot be applied to static methods, constructors or destructors. - The
readonly
modifier cannot be applied to delegates. - The
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/or constant expressions
, both of which have had some existing proposals.
LDM history:
Activity
tannergooding commentedon Jul 12, 2018
FYI. @jaredpar
tannergooding commentedon Jul 12, 2018
This would allow better use of
in
with things likeSystem.Numerics.Matrix4x4
, which is big enough that it is sometimes better to pass around usingin
(due to size -- 64 bytes), but which can't be marked readonly due to a design-decision to make the fields public and writeable (even if the majority of methods exposed don't modify state).tannergooding commentedon Jul 12, 2018
It isn't mentioned above, but it may be worth discussing if a
readonly
method should only be able to call other readonly methods...Today, with
readonly fields
of non-readonly structs, C# will make a copy of the struct. For example, the following prints0
twice, rather than0
,1
: https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgZgAQCcCmBDGA9gHYQCe6AzsJgK4DGw6AGgLABQA3u+j+mlnkIlyATXSkA3O269+yACzoAytmAiAFAEoZPLm14HxAOhVr1ARiMAGAGaap+3gF92Ltu35VaDdCPZ6DfhsIAlxGADdcCBpsBwMdPgwFZVUNYNCIqJjtR10Eg2AACzAKI0jo7ABedHKYuOdXaTY5cwA2PgAmPnMAdn8Elvbk5HMADnURqwBtAF10XEwAcwocgwDDHiZ0AA90aqJsAHdmLXqNkaMRgE51baNSMqzse3zeV5470w0X3MML69u90eFR+BjcTiAA=HaloFour commentedon Jul 12, 2018
Conceptually I like it, a lot. I do have some issues with it, though.
First, such an attribute already exists,
System.Diagnostics.Contracts.PureAttribute
, which is supposed to mean the same thing but isn't enforced by the compiler. Adding another attribute to support this feature would be noise.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
readonly
as a keyword would help to divorce this concept from "purity", although they do seem to cover the same bases.Lastly,
readonly
is already a valid modifier on the return value of a method. The position is different, but that might still lead to confusion, especially since most of the time modifier keywords can be placed in any order.tannergooding commentedon Jul 12, 2018
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
readonly
methods (#1710 (comment))There could be a difference between a
readonly
and apure
method. Apure
method, by definition, cannot modify any state. Areadonly
method (in order to satisfy thein
constraint and elide the copy) must only guarantee that it does not modify instance state (it could be allowed to do things like callConsole.WriteLine
, which would allow copy elision, but would not be functionally pure).It can be summed as
All pure methods are readonly, but not all readonly methods are pure
.Depending on what LDM thinls, it could go one way or the other. If we did differentiate, then
pure
functions would be a natural stepping stone from readonly (andconstexpr
would again be a natural stepping stone frompure
).Right, I called this out in the notes section.
theunrepentantgeek commentedon Jul 12, 2018
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 ... 😁
theunrepentantgeek commentedon Jul 12, 2018
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?
tannergooding commentedon Jul 12, 2018
@theunrepentantgeek, right.
pure
has a number of additional considerations. However, the original post just coversreadonly
methods, which would not have many of these considerations.At its root, a
readonly
method (as proposed) would only guarantee that instance state isn't modified (which would imply calls toSystem.Console.WriteLine
, while not pure, would be allowe).tannergooding commentedon Jul 12, 2018
Added a root note, at the top of the post, clarifying that
readonly method
!=pure method
jaredpar commentedon Jul 12, 2018
@tannergooding
Consider instead
[-]Provide a way to specify that methods do not modify state.[/-][+]Provide a way to specify individual methods on a struct do not modify state in the same way that readonly struct specifies no instance method modifies state.[/+][-]Provide a way to specify individual methods on a struct do not modify state in the same way that readonly struct specifies no instance method modifies state.[/-][+]Provide a way to specify individual methods on a struct do not modify state, in the same way that readonly struct specifies no instance method modifies state.[/+]jaredpar commentedon Jul 12, 2018
@tannergooding
It's not just that it's confusing, it's also ambiguous with other potential features. The compiler specifically chose to use
ref readonly
instead ofreadonly ref
for locals / returns because we wanted to allow for the future addition ofreadonly ref readonly
local variables oncereadonly
locals are introduced.The individual variations are the following for locals in that context:
ref
: a reference which can be assigned to and reassigned to a new locationref readonly
: a reference which cannot be assigned to but can be reassigned to a new locationreadonly ref
: a reference which can be assigned to but cannot be reassigned to a new locationreadonly ref readonly
: a reference which cannot be assigned to or reassigned to a new locationI wouldn't want to use
readonly
as proposed here because it would create a bit of confusion between the two cases: specifying areadonly
method andreadonly
locals.This feature was discussed in LDM when we discussed the
readonly struct
feature. At the time the syntax we were considering is the following:That is putting the
readonly
modifier after the method signature. This is unambiguous and fairly easy to read. This is how we actually implemented permissions in Midori and it works out fairly well. Admittedly it does take some getting used to though for anyone who doesn't have a C++ background.tannergooding commentedon Jul 12, 2018
After the method seems reasonable to me. Will update the proposal later tonight.
tannergooding commentedon Jul 13, 2018
@jaredpar, where would you envision the keyword for properties?
Is it before or after the
get
keyword (changed it to after in the OP):I would imagine that specifying it individually on the
get
/set
is preferred here, sinceset
(in most normal API designs) wouldn't qualify for the attribute 😄113 remaining items