Proposal: Immutable Types #8763
Replies: 68 comments
-
General idea is a good one. I believe it will require CLR support to enforce across languages. Can we rework how "unsafe" works? This keyword has specific connotations around memory safety that I'm not sure I like diluting. I also think it may be safer and better self-documenting if it is specified on specific fields. Something like:
Though a "readonly mutable" sounds a little funny. |
Beta Was this translation helpful? Give feedback.
-
I would prefer a requirement that fields be marked as
I would prefer this be a warning. While unlikely and generally not recommended, it's hard to state deterministically that no one will need to be able to write code like this. |
Beta Was this translation helpful? Give feedback.
-
A Roslyn-based analyzer and a new The biggest concern I would like to see resolved prior to implementing this is how we handle the builder pattern as cleanly (in my opinion) as the sharwell/openstack.net@a09fc52 For this implementation (specifically for |
Beta Was this translation helpful? Give feedback.
-
Maybe if you marked the constructor as |
Beta Was this translation helpful? Give feedback.
-
public class Person
{
public Person(string firstName, string lastName, DateTimeOffset birthDay)
{
FirstName = firstName;
LastName = lastName;
BirthDay = birthDay;
}
public string FirstName { get; } = firstName;
public string LastName { get; } = lastName;
public DateTime BirthDay { get; set; } = birthDay; // Oops!
public string FullName => "\{FirstName} \{LastName}";
public TimeSpan Age => DateTime.UtcNow – BirthDay;
} In this and the following examples, you're using property initializers. I think they shouldn't be there. |
Beta Was this translation helpful? Give feedback.
-
Oops, thanks, @svick. Fixed. |
Beta Was this translation helpful? Give feedback.
-
@sharwell, that's true, but I'd previously written these examples using primary constructors, and without primary constructors, what I'd written didn't make sense. Instead I'm initializing those fields in the regular constructor. |
Beta Was this translation helpful? Give feedback.
-
@sharwell, strange, I could have sworn I was responding to a comment you'd written... it's almost as if it was there and then someone deleted it ;) Oh well. |
Beta Was this translation helpful? Give feedback.
-
@stephentoub I'd love to get your feedback regarding the commit I mentioned above. In particular, the changes to StorageMetadata.cs and the new file StorageMetadataExtensions.cs. The intent is for a user to be able to go |
Beta Was this translation helpful? Give feedback.
-
@stephentoub I currently believe all of your proposed functionality can be provided by a combination of the following items:
Since the attribute is trivial, I'll explain my point regarding the analyzer.
The fun part is generic type constraints. As it turns out, you probably don't actually need them.
|
Beta Was this translation helpful? Give feedback.
-
Sure, an analyzer could absolutely be used here to enforce these rules. In fact it can even be done as a unit test with reflection. I've actually written such code in past projects. I don't think an analyzer is the right solution here though. Analyzers are great at enforcing a set of rules, or even to a degree a dialect of C#, within a single C# project. I control the compilation I can pick what analyzers I want to use. Analyzers are less effective when there is a need to enforce rules across projects. In particular when those projects are owned by different people. There is no mechanism for enforcing that a given project reference was scanned by a particular analyzer. The only enforcement that exists is a hand shake agreement. Immutable types is a feature though that requires cross project communication. The immutability of my type is predicated on the immutable of the type you control that I am embedding. If you break immutability in the next version you have broken my code. In my opinion hat kind of dependency is best done directly in the language. |
Beta Was this translation helpful? Give feedback.
-
Currently, when you access a
...which means currently implementing an "immutable" type is more subtle and fraught with danger than people may be aware (heisenbugs are likely to arise in client applications if they aren't aware of this issue). I don't care too much about the particular end syntax used (whether we get a new keyword, or redefine the semantics of
|
Beta Was this translation helpful? Give feedback.
-
@stephentoub Have you seen Neal's proposal for pattern matching and records in C#? I think it addresses a lot of your concerns here. |
Beta Was this translation helpful? Give feedback.
-
@MgSam, thanks, yes, I have seen it. |
Beta Was this translation helpful? Give feedback.
-
@jaredpar Overall I do agree with your comments. However, I also believe that some burden is placed on development teams to incorporate best practices described by libraries they depend on. There are all sorts of ways they can violate preconditions of libaries. One obvious example is
While the first item would be challenging to prevent, it would be easy to address the concerns for the second point by synchronizing concurrent access to the object. If immutable types were provided via optional static analysis, it is conceivable that many users would be able consume them even without an analyzer because they are only prone to failure in very specific ways:
There are other ways to improve overall reliability, such as declaring a dependency on the analyzer package when creating a NuGet package for a library which defines immutable types. It would also be possible to package the For those wondering why I would push for an analyzer instead of a language change:Concurrency remains a major challenge for modern application development. Synchronization constructs such as For better or worse, these libraries do not provide out-of-the-box support for every scenario an application developer might encounter. Improving the ability of developers to extend these concepts will go a long way towards improving overall developer efficiency when creating reliable, scalable applications intended for concurrent environments. In my opinion, the best approach would be to first implement this as an analyzer so people can start using it, and later consider incorporating it into the language and/or runtime. When you consider that several parts of the C# syntax, such as the |
Beta Was this translation helpful? Give feedback.
-
I was surprised to find this issue has existed for more than four years and I still couldn't find any Roslyn analyzer for immutable types, based on attributes or otherwise. Newer projects I work on use immutable types more and more, and while so far immutability by convention only has worked, it would be nice to have an analyzer to be sure you don't miss anything or otherwise accidentally fail to make a type truly immutable when you intended to. So I went ahead and made a basic implementation based on attributes - you can find the code here if you want to try it out. I tried applying it to one of the projects I'm working on and it seemed to work out... it actually ended up finding some types near the bottom of an immutable type tree that were mutable, but fortunately nothing ever actually modified the instances of those types after construction. I think I prefer the analyzer approach to a language feature because you can always disable the analyzer for the few cases where you need mutability (for performance or other reasons), but everywhere else still can have the constraints of immutable types applied. |
Beta Was this translation helpful? Give feedback.
-
Possibly related to #776? |
Beta Was this translation helpful? Give feedback.
-
I guess they refuse to add it to force us to use F# :) |
Beta Was this translation helpful? Give feedback.
-
I'm keen for this feature. From an architectural perspective, when i receive an object from an API/interface I would ideally, optionally, like to have an immutable constraint, meaning that there is nothing the consumer can do to update that object without going through the interface again. The primary drive for this is that, if a developer returns a non-immutable object from the interface, this can effectively serve as a back door into the originating module - circumnavigating the interface - which may not always be desirable. Having types with deep immutability would ensure strict enforcement of the interface (no back doors), which will ultimately ensure there are fewer surprises when rearchitecting the way the module is used - for example, if i want to move my module so it is now exposed through a web service (as any back doors into returned objects are not an option). Having this constraint ensures that we can identify risks at compile-time, to prevent any mistakes in development, and ensure interface-consistency regardless of application. As discussed above, the would mean deep immutability - types that satisfy the immutable constraint can only contain other immutables, and all functions are pure. |
Beta Was this translation helpful? Give feedback.
-
This is public const class A
{
public int Property {get;}
}
public class B : A
{
public int Property {get; set;}
}
var b = new B();
var a = (A)b; according to google in c++...
|
Beta Was this translation helpful? Give feedback.
-
Because, as your definition says, |
Beta Was this translation helpful? Give feedback.
-
its about immutability no? a compile time constant is immutable. |
Beta Was this translation helpful? Give feedback.
-
But being immutable doesn't make it a compile time constant. These types can be evaluated/initialized at runtime and still be immutable, so this proposal is not related to compile time constants. |
Beta Was this translation helpful? Give feedback.
-
A bit adjacent, I'm wondering if there is a C# equivalent for https://immutables.github.io/ ? |
Beta Was this translation helpful? Give feedback.
-
C# has source generators, which can be combined with partial types to emit the boilerplate code for such a type. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Got it -- is there a source generator that the community generally uses to build immutable types? |
Beta Was this translation helpful? Give feedback.
-
Would the Immutable* and Frozen* classes be marked as immutable? It seems that doing so would imply that the elements within them are immutable, but this is not necessarily the case unless we also add We'd need something like We'd also need a way to tell at runtime if the type is immutable |
Beta Was this translation helpful? Give feedback.
-
This is records is it not? |
Beta Was this translation helpful? Give feedback.
-
I think the problem here is the absence of a clear concept explanation of what is:
And what is the relation of "record" with these concepts. And I'm not referring to class/struct fields "private readonly int...". I'm referring to create a documentation and an article where the csharplang team explains with maximum detail what they want to be each of that concepts. And therefore, the conventions on deep/shallow immutability. My point of view consists in something very similar to the current state of things:
This have a big problem right now, and it is the (IMHO) low penetration of ReadOnlyMemory/ImmutableArray (thinking in reference types) as the way to expose readonly arrays. Because right now trying to expose a indexed collection of items hinders you to use IReadOnlyList, yes, or yes. If I have the example that @stephentoub uses public immutable class Person
{
public Person(string firstName, string lastName, DateTimeOffset birthDay, Person[] ancestors)
{
FirstName = firstName;
LastName = lastName;
BirthDay = birthDay;
Ancestors = ancestors;
}
public string FirstName { get; }
public string LastName { get; }
public DateTime BirthDay { get; }
public Person[] Ancestors { get; } // Compiler blows up
public ReadOnlyMemory<Person> Ancestors { get; } // The correct way to do it
public ReadOnlyMemory<PersonalBox> PersonalBoxes { get; } // Compiler must throw an error, PersonalBox is not immutable
public string FullName => $"{FirstName} {LastName}";
public TimeSpan Age => DateTime.UtcNow – BirthDay;
}
public class PersonalBox {
public int Value { get; set; }
} If there is some problem with the use of ReadOnlyMemory (like "it's more perfomance/low level oriented), then ImmutableArray must be used. And must be encouraged. Another thing is that by default every struct/class that by the content is immutable (even if not specified) should be marked by default with the immutable keyword during the compilation. |
Beta Was this translation helpful? Give feedback.
-
How about using a concept of "freezable" types under the hood for immutability? Proposal "Drafts"This proposal uses generic Draft types to allow modification of immutable objects during creation, while ensuring that they are fully immutable afterwards. Terminology
(Optional) CLR adaptionsFor immutable types to ensure total immutability, the CLR would need to add support for frozen types (e.g. objects have a frozen flag and if it is set, changes to the value are not allowed)
New language featuresTwo new keywords would be necessary:
Further there are some restrictions introduced:
As mentioned, a new type
Further all draft fields (within the draft-scope and on Problems/DrawbacksThe biggest drawback I could not figure out a solution for are protected draft-scoped members, as protected members cannot be added as an extension. For this problem, i found two approaches:
This approach also has some performance disadvantages, as a lot of theoretically unnecessary object allocations and method invoications are unavoidable. This could only be cirumvented by making the Sample (not all mentioned proposed features shown)
SummaryThis approach allows full control of the immutable types (cyclic references, etc.) while still ensuring full immutability (if supported by CLR). It is a big feature and I am sure that I have missed many things, but maybe it is a starting point for a new discussion. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Problem
One of the uses of 'readonly' fields is in defining immutable types, types that once constructed cannot be visibly changed in any way. Such types often require significant diligence to implement, both from the developer and from code reviewers, because beyond 'readonly' there’s little assistance provided by the compiler in ensuring that a type is actually immutable. Additionally, there’s no way in the language (other than in type naming) for a developer to convey that a type was meant to be immutable, which can significantly impact how it’s consumed, e.g. whether a developer can freely share an instance of the type between multiple threads without concern for race conditions.
Consider this type:
Writing this type requires relatively minimal boilerplate. It also happens to be immutable: there are no exposed fields, there are no setters on any properties (the get-only auto-props will be backed by 'readonly' fields), all of the fields are of immutable types, etc. However, there is no way for the developer to actually express the intent to the compiler that an immutable type was desired here and thus get compiler checking to enforce this. At some point in the future, a developer could add a setter not realizing this type was meant to be immutable, and all of a sudden consumers of this type that were expecting full immutability (e.g. they'd avoiding making defensive copies) will now be very surprised:
Similarly, the class could be augmented with an additional 'readonly' property but of a non-immutable type:
And so on. The developer has tried to design an immutable type, but without a way to declare that fact, and without compiler verification of that declaration, it is easy for bugs to slip in.
Solution: Immutable Types
We can introduce the notion of immutable types to C#. A type, either a class or a struct, can be annotated as "immutable":
When such an annotation is applied, the compiler validates that the type is indeed immutable. All fields are made implicitly readonly (though it’s ok for a developer to explicitly state the ‘readonly’ keyword if desired) and be of immutable types (all of the core types like Int32, Double, TimeSpan, String, and so on in the .NET Framework would be annotated as immutable). Additionally, the constructor of the type would be restricted in what it can do with the 'this' reference, limited only to directly reading and writing fields on the instance, e.g. it can’t call methods on 'this' (which could read the state of the immutable object before it was fully constructed and thus later perceive the immutable type as having changed), and it can’t pass 'this' out to other code (which could similar perceive the object changing). This includes being prohibited from capturing 'this' into an anonymous method in the ctor. A type being 'immutable' doesn't mean that its operations are pure, just that the state within the object can't observably change; an immutable type would still be able to access statics, could still mutate mutable objects passed into its methods, etc.
The 'immutable' keyword would also work as an annotation on generic types.
Applying 'immutable' to a type with generic parameters would enforce all of the aforementioned rules, except that the generic type parameters wouldn't be enforced to be immutable: after all, without constraints on the generic type parameters, there’d be no way for the implementation of the open generic to validate that the type parameters are immutable. As such, a generic type annotated as 'immutable' can be used to create both mutable and immutable instances: a generic instantiation is only considered to be immutable if it’s constructed with known immutable types:
Such concrete instantiations could be used as fields of other immutable types iff they're immutable. But whether a generic instantiation is considered to be immutable or not has other effects on consumers of the type, for example being able to know that an instance is immutable and thus can be shared between threads freely without concern for race conditions. As such, the IDE should do the leg work for the developer and highlight whether a given generic instantiation is considered to be immutable or mutable (or unknown, in the case of open generics).
However, the immutability question also affects other places where the compiler needs to confirm that a type is in fact immutable. One such place would be with a new immutable generic constraint added to the language (there are conceivably additional places in the future that the language could depend on the immutability of a type). Consider this variation on the tuple type previously shown:
The only difference from the previous version (other than a name change for clarity) is that we’ve constrained both generic type parameters to be 'immutable'. With that, the compiler would enforce that all types used in generic instantiations of this type are 'immutable' and satisfy all of the aforementioned constraints.
With such constraints, it’s possible to create deeply immutable types, both non-generic and generic, and to have the compiler help fully validate the immutability.
However, there are times when you may want to cheat, where you want to be able to use the type to satisfy immutable constraints, and potentially have some of the type’s implementation checked for the rules of immutability, but where you need to break the rules in the implementation in a way that’s still observably immutable but not physically so. For example, consider building an ImmutableArray type that wraps an underlying array. As arrays are themselves mutable (code can freely write to an array’s elements), it’s not normally possible to store an array as a field of an immutable type:
To work around this, we can resort to unsafe code. Marking an immutable type as 'unsafe' would disable the rule checking for immutability in the entire type and put the onus back on the developer to ensure that the type really is observably immutable, while still allowing the type to be used in places that require immutable types, namely generic immutable constraints. Marking a field as unsafe would disable the rule checking only related to that field, and marking a method as unsafe would disable the rule checking only related to that method. A type that uses unsafe needs to ensure not only that it still puts forth an immutable facade, but that its internal implementation is safe to be used concurrently.
Delegates could also be marked as immutable, and a set of ImmutableAction and ImmutableFunc types would be included in the framework. As with other immutable types, all of the objects reachable from an immutable delegate instance would need to be immutable, which means that an immutable delegate could only bind to methods on immutable types. That in turn means that, when an anonymous method binds to an immutable delegate type, that anonymous method may only capture immutable state. Further, any locals captured into the lambda must either be from a 'readonly' value (#115) or must be captured by value (#117). This ensures that the fields of the display class can be 'readonly' and that the method which created the lambda can’t reassign the captured values after creating the lambda.
Alternatives
The 'immutable' attribution would be deep, meaning that an instance of an immutable type and all of the types it recursively references in its state would be immutable. In contrast, we could consider a shallow version, with a 'readonly' attribute that could be applied to types. As with 'immutable', this would enforce that all fields were readonly. Unlike 'immutable', it would place no constraints on the types of those fields also being immutable.
Beta Was this translation helpful? Give feedback.
All reactions