- Proposed
- Prototype
- Implementation
- Specification
Allow locals and parameters to be annotated as readonly in order to prevent shallow mutation of those locals and parameters.
Today, the readonly
keyword can be applied to fields; this has the effect of ensuring that a field can only
be written to during construction (static construction in the case of a static field, or instance construction in the case of an instance field),
which helps developers avoid mistakes by accidentally overwriting state which should not be modified. But fields aren’t the only places developers
want to ensure that values aren’t mutated. In particular, it’s common to create a local variable to store temporary state, and accidentally updating
that temporary state can result in erroneous calculations and other such bugs, especially when such "locals" are captured in lambdas, at which point
they are lifted to fields, but there's no way today to mark such lifted fields as 'readonly`.
Locals will be annotatable as readonly
as well, with the compiler ensuring that they’re only set at the time of declaration (certain locals in C# are
already implicitly readonly, such as the iteration variable in a ‘foreach’ loop or the used variable in a ‘using’ block, but currently a developer has
no ability to mark other locals as readonly
). Such readonly
locals must have an initializer:
readonly long maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
...
maxBytesToDelete = 0; // Error: can’t assign to readonly locals outside of declaration
And as shorthand for readonly var
, the existing contextual keyword let
may be used, e.g.
let maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
...
maxBytesToDelete = 0; // Error: can’t assign to readonly locals outside of declaration
There are no special constraints for what the initializer can be, and can be anything currently valid as an initializer for locals, e.g.
readonly T data = arg1 ?? arg2;
readonly
on locals is particularly valuable when working with lambdas and closures. When an anonymous method or lambda accesses local state from the enclosing scope,
that state is captured into a closure by the compiler, which is represented by a “display class.” Each “local” that’s captured is a field in this class, yet
because the compiler is generating this field on your behalf, you have no opportunity to annotate it as readonly
in order to prevent the lambda from erroneously
writing to the “local” (in quotes because it’s really not a local, at least not in the resulting MSIL). With readonly
locals, the compiler can prevent the lambda
from writing to local, which is particularly valuable in scenarios involving multithreading where an erroneous write could result in a dangerous but rare and
hard-to-find concurrency bug.
readonly long index = ...;
Parallel.ForEach(data, item => {
T element = item[index];
index = 0; // Error: can’t assign to readonly locals outside of declaration
});
As a special form of local, parameters will also be annotatable as readonly
. This would have no effect on what the caller of the method is able to pass to the
parameter (just as there’s no constraint on what values may be stored into a readonly
field), but as with any readonly
local, the compiler would prohibit code
from writing to the parameter after declaration, which means the body of the method is prohibited from writing to the parameter.
public void Update(readonly int index = 0) // Default values are ok though not required
{
...
index = 0; // Error: can’t assign to readonly parameters
...
}
readonly
parameters do not affect the signature/metadata emitted by the compiler for that method, and simply affect how the compiler handles the compilation of
the method's body. Thus, for example, a base virtual method could have a readonly
parameter, and that parameter could be writable in an override.
As with fields, readonly
for locals and parameters is shallow, affecting the storage location but not transitively affecting the object graph. However, also
as with fields, calling a method on a readonly
local/parameter struct will actually make a copy of the struct and call the method on the copy, in order to avoid
internal mutation of this
.
readonly
locals and parameters can't be passed as ref
or out
arguments, unless/until ref readonly
is also supported.
val
could be used as an alternative shorthand tolet
.
readonly ref
/ref readonly
/readonly ref readonly
: I've left the question of how to handleref readonly
as separate from this proposal.- This proposal does not tackle readonly structs / immutable types. That is left for a separate proposal.
- Briefly discussed on Jan 21, 2015 (dotnet/roslyn#98)