-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
[Proposal]: Support readonly modifier for classes and records #3885
Comments
To add to the motivation (I've wanted
I think the answer has to be no. The real usefulness of |
This proposal essentially adds shallow immutability to I've considered a feature like this a few times and I remain a bit skeptical as to whether it's worth the added complexity to the language. The guarantee here is mostly beneficial to the type author(s) because it's a declarative reminder that they wanted to avoid direct mutations in this type. The guarantee doesn't have much benefit to the type consumers though because it does not mean the type is free of mutations. The type can freely mutate via nested data hence knowing a type is Compare this to Couple of other areas to consider or possibly explain why you discarded them
|
I'm not as skeptical as Jared here. I'll champion this. |
I agree with your skepticism, this proposal would not provide the degree of benefit that
This has come up before. IMO, an interface declares what a type can do, not what it can't. I've always argued that interfaces named
I wouldn't be opposed to |
This is a bad idea for classes, at least when it comes to using the
A keyword just to force you to add You could argue In a sense, all classes (and their methods) are implicitly
This is a bit misleading. The same for saying "interfaces don't declare what a type cannot do": public interface IMyCollection
{
int Count { readonly get; }
} Am I really the only one who thinks "I can provide you my Count without modifying the value you used." isn't completely unreasonable? This is something that is actually needed. And in other languages, interfaces absolutely can declare what a type "cannot" do: public interface java.io.Closeable {
void close() throws IOException;
}
public interface java.lang.AutoCloseable {
void close() throws Exception;
} The same for To sum things up, |
By the way, I think this technically *could* be a non-issue in the future: public readonly struct S
{
public int Value; // notice the lack of readonly
public override string ToString() => Value.ToString();
}
Just something to bear in mind. |
It empowers the author of the class by enforcing their intent. By that same token
Nor does it try, which is why I'm not suggesting that keyword here.
If that's the bar that needs to be solved then a solution will simply never exist in this language/runtime.
And something that enough people want to do that I feel it is worth being a first-class citizen.
The interface says, "I can provide you my Count," and that's all. How it provides that value isn't your concern, and if the collection uses lazy evaluation then it can (and should) mutate it's internal state.
Except that they can't. From Java you can choose to not throw an Exception, or you can throw any runtime exception. From every other JVM language you can throw whatever you want because none of them care about checked exceptions and the runtime can't provide any guarantees. If this proposal pushes conversation and effort on #2543 then I'll be happy, but I'd rather some solution now than a better solution never. |
So you can implement
I am not arguing about mutability here, only about the changes to the value. That is only limited to values of fields of structs. A struct can implement
Well nobody had to make a proposal for " Even |
This proposal does not preclude any of those other proposals. If you think it's analyzer territory you're welcome to your opinion. I do explicitly mention that above. But I stand by my position that a common request should be a first class citizen. If the conversation yields something more than this proposal then wonderful. I'd be happy if |
Whilst you are correct that read-only structs offer performance benefits, it would be wrong to suggest that is the only benefit to them. For those of us less worried about micro-managing performance and more interested in our code communicating ideas, such performance benefits are effectively an implementation detail of the feature. What's really important is that that keyword offers a read-only contract to the user of the type. It is directly comparable to read-only fields: they only offer "shallow immutability" too. yet people mark fields as read-only as it communicates intent, even if it cannot enforce true immutability. Read-only classes offer the same communication of intent. They do not guarantee immutability, just as read-only fields and structs do not guarantee immutability. It does though communicate an intent to the the user. And for many developers, that is a significant benefit, whether tangible or not. |
2cents: the fact that structs can be 'readonly' and have 'readonly' members, while the classes can't, already adds a bit of complexity for the user. I did find myself performing "remove readonly from all the methods" dance a couple of times. |
I believe it should be high priority because it blocks important scenarios and improvements. I am actively working on developing of PowerShell and one scnenario we have a persistent concern is a startup scenario. Historically PowerShell loaded some XML resource files at startup time and converted its in internal structures. It is slow, very slow. And we implemented an improvement - we converted the resource file to C# code at design time. We get a great speedup of the startup scenario. (See PowerShell/PowerShell#10898 for details) But let's start with more simpler example of code pattern that's used dozens of times in PowerShell code. private static readonly char[] s_pathSeparators = new char[] { '/', '\\' }; Design intention is obviously it is immutable - elements are never changed and never added or removed.
Today Roslyn does the optimization (remove extra copy) for follow: private static ReadOnlySpan<byte> AsciiCharInfo => new byte[] { ... }; See https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static/ for details. But why don't we generalize this to Next step would be to generalize this to all struct types. Next step, if we take a struct and replace Here we see that optimizations specific to structures only can be applied to simple classes and this can be very useful for real applications - see my example above about PowerShell XML resource files - yes, they could be converted to simple classes and yes, this could speed up PowerShell startup scenario. Let's take a step back to the example because we are talking about immutability: private static readonly char[] s_pathSeparators = new char[] { '/', '\\' }; We need the array being immutable and I can think about: private static readonly char[] s_pathSeparators = new readonly char[] { '/', '\\' };
// or the same in short form:
private static immutable char[] s_pathSeparators = new char[] { '/', '\\' }; Is it possible to implement? I think yes - if today we can set readonly to all struct members and get fully (I remeber about auto-implemented properties and hope they will be fixed) immutable structs why can't we set readonly to class members? It can be difficult in common but as we have saw above for simple classes we can. So we can implement Let's return to startup scenario. Converting XML to code obviously requires links between class instances. Yes, this is a special case, but my goal is to show that it is useful in real-world scenarios both to improve performance and to improve the facilities of the language itself. The last thing I would like to note is that the implementation of this proposal makes "copy-on-write" optimization possible. private static readonly char[] s_pathSeparators = new char[] { '/', '\\' }; Now let's consider it as mutable. |
@iSazonov This proposal is about a single quite simple thing: adding the What you're talking about sounds useful (though I have my doubts on whether it's feasible), but it's quite a lot bigger than this proposal. |
I really hope this at least gets considered together with #3055, which does actually bring performance benefits when you care about code principles. |
This proposal is solely about a shorthand way of stating your intent that all members of a class need to be readonly. |
Is there a parallel plan to keep working towards something like #2543 ? Being able to specify deep immutability seems like much greater value than shallow immutability discussed in this proposal, especially in light of the introduction of records. |
Consumers can't tell the difference whether a class is deeply immutable or whether it's just shallow - in both cases they have something that won't change state that can be safely shared. Shallow immutability gives me a lot more power as a type author - for example, I can defer an expensive computation for In my experience, languages that have enforcement of deep immutability tend to end up with multiple parallel type systems (a mutable list and an immutable list, a mutable string and an immutable string, a mutable vector and an immutable vector, a mutable set and an immutable set, and on and on and on) resulting in a lot of needless marshalling between otherwise equivalent types. |
@theunrepentantgeek That is a potential danger, but it is far from absolute. C++ has avoided it, for example, although only by having |
@IllidanS4 c++ const-ness is just a strong suggestion. Many complex 'const' classes end up having volatile members, or const cast away immutability as needed. (Some may argue those are indicators of poor design by specific standards, but that is a different discussion, as that is how that language is used. ) |
I suspect that people may find "top-level" immutability sufficient. I think a common pattern will be people going either "all in" on immutable records for object graphs, or barely at all. In the former case, top level immutability will be fine because every field will either be a primitive type or another record. I suspect that an analyser would then be sufficient to warn if you have an read-only record that contains a field that is eg a mutable class. Perhaps I'm totally wrong, but I'd be surprised to find mixing immutable records and mutable classes a common use case. |
I don't like this proposal because it uses up the readonly keyword for something that could be done with an attribute. Keywords are valuable, and this would kill the potential of a readonly class being something more significant, and useful in the future. |
@joshfisk It seems like it would not be ideal for the keyword to differ in meaning between classes and structs, so we should be able to already know the semantics and go ahead and add it now if ever. An attribute would look particularly odd and provoke people to wonder why things have to be the way they are. |
@isaacabraham I have a couple instances of trees of immutable state that have references to mutable classes (treated opaquely) by necessity, deep in the tree. I would not be able to use deep immutability for parts of the tree above this, but I would not stop wanting to make each part a single-level readonly class. |
@jnm2 Yeah, it should match what struct is doing. It makes it easier to learn, and reduces a step if you need to convert between a class, and a struct. I still feel like adding a keyword to the type should change the way the type works, perhaps instead of adding error messages, and making you add readonly to every field, it could implicitly add it to all the fields. |
This would deviate from how it works with structs. (Also from how the |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Readonly Modifier for Classes and Records
Summary
Support applying the
readonly
modifier to classes and records. This keyword will cause the compiler to enforce that all fields declared in the type are alsoreadonly
and result in a compiler error if any field is not readonly or any auto-property has a set accessor.Motivation
There seems to be a lot of confusion around the records feature regarding "immutability" as it is being designed for C# 9.0. This is exacerbated by the fact that the docs very clearly state that record types make it easier to declare "immutable" types and that they are "immutable" by default, neither of which is really true. There has been a bit of surprise expressed in a number of issues/discussions on this repo as well as on Gitter and Discord about the fact that auto-properties with setters are legal. In my opinion there would be a lot of value in being able to state that a record type is in fact "immutable" at least to the extent that the fields cannot be reassigned.
Further motivation is that it is likely that C# 10.0 will gain "struct records". There is already support for
readonly struct
in the C# language and I expect that areadonly struct
can also be a record by a combination of the modifiers. As such I think it makes sense to allow thereadonly
modifier to be applied universally to types with the same compiler enforcement.Detailed design
The
readonly
modifier can be applied to a type ofclass
orrecord
. The modifier can be applied in any other with other modifiers. The modifier by itself does nothing. However, when applied the modifier will enforce that the type can only havereadonly
fields and either readonly or init-only auto-properties:Drawbacks
This feature could be confused with immutable types which aim to enforce deep immutability but is much stricter in that every field must also be of an immutable type.
Alternatives
This could be supported today by an analyzer. In my opinion there is value in supporting this as a first-class citizen, especially since
readonly struct
already exists and I expect thatreadonly struct record
will be added in C# 10.0Unresolved questions
readonly
class or record inherit from a non-readonly
class or record?Design meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-26.md#readonly-classes-and-records
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#ungrouped
The text was updated successfully, but these errors were encountered: