Proposal: immutable fields and properties #4699
Replies: 11 comments
-
Is
This means that
I don't think that would be safe. Consider: void Sneaky(immutable Foo foo)
{
List<Foo> listOfMutable = new List<Foo>();
List<immutable Foo> listOfImmutable = listOfMutable;
listOfImmutable.Add(foo);
listOfMutable[0].Property = 42;
} Thanks to that cast from
I think this could be a trap for libraries. If the compiler infers that a method is
What does it mean for
This raises the question of how An alternative would be to use modopt or modreq, which I think would allow this kind of overloading. But then, what would happen if I wrote code like the above and then attempted to use it from a language that didn't understand |
Beta Was this translation helpful? Give feedback.
-
Good question. In my question, immutable should be widespread as it communicates clear intent to observe and not modify. I don't personally consider it too verbose, but that's opinion-based. The most use I see in method signatures. I'm open to suggestions on shorter ways of saying "only observation allowed".
I personally value thread-safety a lot, so I am open to support that
Correct and as intended. static void Thread1(Foo foo)
{
foo.Modify();
}
static void Thread2(immutable Foo foo)
{
foo.Observe();
}
void Start(Foo foo)
{
new Thread(() => Thread1(foo).)Start();
new Thread(() => Thread2(foo).)Start();
}
Good point about breaking changes. Potentially, the static code analysis for immutability should only used for tooling that facilitates adding
In this example, the data of a list will not be considered changed when an element is changed. The modifier here refers to the state of the class (this), not the returned object (T).
C++ name mangling comes to mind, but doesn't feel right. Definitely something to be discussed. Thanks for all the points you raised @svick! |
Beta Was this translation helpful? Give feedback.
-
I understand that this kind of feature can't guarantee much. But I think that if I a method receives an
You also said that But that's not really what I was asking. As I understand it, if I have an |
Beta Was this translation helpful? Give feedback.
-
I feel like this could largely be solved today with a couple attributes and an extension method or two. However, we already have Your underlying problem seems to be mutability of the element type, but why are current solutions such as |
Beta Was this translation helpful? Give feedback.
-
I'd also really appreciate a feature like this, to support writing good code. Current solutions usually involve having to make a copy of a class, or adding a read-only facade. In terms of |
Beta Was this translation helpful? Give feedback.
-
In response to @svick:
I'm afraid you're mistaken: even in your code,
Talking about collections, I see four kinds of options: void ResetCounters(immutable List<Counter> counters)
{
foreach (var counter in counters)
{
counter.Reset(); // modification of element of list
}
}
You are talking here about an immutable collection of mutable objects, which in my opinion is perfectly valid, including the replacement of an element. Note that I'm not proposing to implement In response to @yaakov-h:
I don't see how the transitivity aspect of In response to @Nition:
I considered |
Beta Was this translation helpful? Give feedback.
-
@stefanloerwald static void Modify(Bar bar)
{
Bar.Content = “”;
}
static void Test(immutable Foo foo)
{
MutableImmutable<Foo> mutable = new MutableImmutable<Foo>();
MutableImmutable<immutable Foo> immutable= mutable;
immutable.Item = foo;
Modify(foo.Bar); // illegal, as Modify expects a mutable Bar
Modify(mutable.Item.Bar); // legal for some reason
} Doesn't that make the whole thing worthless as the immutability is trivially avoided? |
Beta Was this translation helpful? Give feedback.
-
Good point. |
Beta Was this translation helpful? Give feedback.
-
private class Wrapper<T>
{
public T Value { get; set; }
}
public static T GetMutableReference<T>(immutable T value)
{
var wrapper = new Wrapper<T>();
var immutable_wrapper = wrapper as Wrapper<immutable T>;
immutable_wrapper.Value = value;
return wrapper.Value;
}
public void CallSite(immutable Foo foo)
{
var mutable_reference_to_foo = GetMutableReference(foo);
mutable_reference_to_foo.Modify();
} This is trickery to achieve the same as public void CallSite(immutable Foo foo)
{
var mutable_reference_to_foo = mutable! foo;
mutable_reference_to_foo.Modify();
} So yep, this code is possible and it's not ideal: you can shoot yourself in the foot, but regardless of whether you use the trickery boilerplate code or |
Beta Was this translation helpful? Give feedback.
-
My point of view on this topic is that, while I'm very happy to see others asking for language-level immutability controls to be added to the language, I'm not in favour of being able to tag individual references as immutable or not; as we've seen from the comments it's rife with loopholes and inconsistencies. Another thing: if we're going to do immutability properly I don't think we should be using the same mutable types; a read/write list and a read-only list will probably be implemented completely differently, especially since a read-only list is going to want to have some kind of copy-and-update functionality. |
Beta Was this translation helpful? Give feedback.
-
The one loophole that was discovered (let's call it "promotion of
With this proposed feature, you'd get an implementation of a read-only type for free, including casts, that is a reduction (talking about the types' interface) of the original type. Nothing stops you from writing explicitely different types, should functionality need to be different. I don't see a good argument to implement things like list completely different |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Rationale
Programmers make implicit assumptions about the state of objects. For example, the implicit assumption about a function to calculate the total of a list of integers, is that neither the list, nor the values in the list, will be changed by that function:
However, by the method signature alone, we cannot infer either property. A non-intuitive implementation might modify elements, add, or remove entries:
These implementations are equally valid in terms of the returned value, but will modify the list. Given the standard C# libraries, we can use
IReadOnlyList<T>
to avoid the modification of the list itself, but we still lack one crucial thing, which is the prevention of modification of values. In case of integers, this is already sufficient byIReadOnlyList<int>[int index]
as a get-only property, however for more even slightly more complexIReadOnlyList<T>
, this is not guaranteed:What we actually want is something like
static int Total(IReadOnlyList<IReadOnlyInt> values)
, however, that is not possible without writing a lot of boilerplate code. This proposal is to add two new keywordsimmutable
andmutable
. The method declaration then becomesstatic int Total(immutable List<immutable int> values)
andint Total(immutable List<immutable IntWrapper> values)
.Total2
fails to compile atvalues[0] += values.Last()
, as modification ofvalues[0]
is not permitted (as the+=
operator onint
is notimmutable
:Total2a
fails to compile atvalues.Remove(values.Last())
, as modification ofvalues
is not permitted (asRemove
is not declaredimmutable
:Total3
fails to compile atvalues[i].Value = 0
, as modification ofvalues[0].Value
is not permitted (as the=
operator onint
is notimmutable
:Changes to the language
The keywords
immutable
and its antonymmutable
can be used in several contexts.The effects are on compile time only.
immutable and mutable fields and properties
Immutability is all about preservation of logical state, therefore, in an
immutable
context, all modifications to fields and auto-properties are disallowed, i.e. fields are implicitlyimmutable
, andimmutable
applies transitively. Some fields are used to track internal state, but not observable logical state. Such fields (e.g. a version property such as in CoreLib'sSystem.Collections.Generic.List<T>
) can be markedmutable
.immutable parameter
A parameter can be declared
immutable
. The effects are that the body of the function may not modify the state of the parameter in any way. It is equivalent to replacing the type of theimmutable
parameter with one, that only implements the non-modifying methods (property setters are therefore also excluded, unless they don’t modify any field), and has all fields declared by their respectiveimmutable
types.immutable method
A method can be declared
immutable
to disallow modification of any field of the class with the exception of fields with themutable
modifier.Non-
immutable
methods may not be called from a method declaredimmutable
, including by transitivity all non-immutable
methods on anyimmutable
field. Methods aremutable
unless otherwise determined, but can be optionally markedmutable
.immutable return values
Note that
public immutable T Method()
is different topublic T immutable Method()
. The former returns an object of typeT
that cannot be modified, whereas the latter returns a modifiable object of typeT
, but the method itself does not modify the state of the class.public immutable T immutable Method()
does not modify its classes state, and the returned object isimmutable
.immutable at call-site
To highlight the expectation of immutability of an object at the call-site, one can add
immutable
to the passed object.immutable generics type parameters
On instantiation of a generic type or method, type parameters may be declared
immutable
.Any
Type<T>
can be cast to aType<immutable T>
, but not vice-versa.immutable arrays
An
immutable
array must be distinguishable from an array ofimmutable
objects. Inimmutable T[]
,immutable
applies toT
, inT immutable[]
, it applies to the array.Type casting
The keyword
immutable
may not be cast away, i.e. if an object of typeT
is declaredimmutable
andT
is type-convertible toS
, then the object may be cast toimmutable S
, but notS
.Explicit override to non-immutable
For some corner cases, the override of the immutability of an object may be necessary, e.g. to use legacy libraries. To pass an
immutable
object to a function that doesn't promiseimmutable
, the object must explicitly be modified to bemutable
. To highlight the danger of such an operation, this proposal argues for the use ofmutable!
instead of justmutable
.Some methods must be able to modify state while appearing
immutable
. Similarly to a function call,mutable!
can be used to override immutability in an expression or a code block.Why existing keywords are not reused
There are two keywords in the language that can be associated with immutability:
const
(coming from a C++ background) andreadonly
. However, the existing usage is distinctly different to what is proposed here (no transitivity inreadonly
, compile time constant forconst
) and not compatible in declarations, e.g.private readonly immutable T value;
-- here, the writer intends initialization in the constructor only, hence readonly. In the class, value can be observed, but not modified.Inferred immutable
The
immutable
modifier to methods can be inferred through static code analysis. A pessimistic analysis seems most appropriate for this language feature.Example: Effects on
List<T>
Current definition
The definition of
List<T>
is:New definition
The definition of
List<T>
can communicate intent of mutability / immutability much clearer:Reduced interface when observed as
immutable List<T>
An
immutable List<T>
will expose the subsetReduced interface when observed as
List<immutable T>
A
List<immutable T>
will exposeReduced interface when observed as immutable List
An
immutable List<immutable T>
will exposeDifference to existing language features
Difference to in parameters
in
parameters disallow the change of references, they do not disallow modification of the object state itself:An
immutable
parameter on the other hand disallows modification transitively, but not itself:Difference to get-only properties
A
get
-only property disallows the modification of the field itself, but as within
parameters, they do not disallow modification of the object:Difference to implementation with interfaces / inheritance
The safety against unintentional modification can be implemented via interfaces, e.g.
IReadOnlyList
versusList
. While it is technically possible to writeIReadOnlyT
interfaces for allT
, it is an undesirable amount of boilerplate code. Furthermore, nothing prevents the inclusion of state-modifying methods in such aIReadOnlyT
interface, or the implementation of the interface to violate the assumption. Lastly, the transitivity ofimmutable
cannot be implemented via interfaces / inheritance:Comparison to C++ const
The proposed
immutable
keyword behaves in many ways similar to C++const
, i.e. immutability is transitive and applies to fields and methods. The key difference lies in their behavior in generics/templates. In C++, avector<int>
cannot be cast tovector<const int>
, because the vector template gets compiled to fundamentally different types. Allowing a conversion ofIEnumerable<T>
toIEnumerable<immutable T>
- but not vice-versa – enables writing safer code.Future
I would like to invite any person interested to challenge, add, correct, and build upon this initial proposal.
Beta Was this translation helpful? Give feedback.
All reactions