csharplang
csharplang copied to clipboard
Champion "readonly for locals and parameters"
- [ ] Proposal added
- [ ] Discussed in LDM
- [ ] Decision in LDM
- [ ] Finalized (done, rejected, inactive)
- [ ] Spec'ed
See also https://github.com/dotnet/roslyn/issues/115
Design review
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-27.md#readonly-modifiers-for-primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-07-17.md#readonly-parameters
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-07-26.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-07-31.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-08.md#readonly-for-primary-constructor-parameters
Any plan for readonly types(classes and structs)?
should support
readonly i = 0; // shorthand for readonly var
const j = 0; // shorthand for const var
Wasn't val
going to be short for readonly var
?
@jnm2 I just don't like the idea of adding new keyword. Especially it is already have keyword in the language that has the same meaning
readonly
might be a bit longer but we already preserved it from the start. We should reuse it. And to be shorter, just let we use readonly
without var
At least I have seen some suggestion to use let
that would still better than val
because we already have it as keyword, even for linq scope
Especially because val
was not keyword. I really have my code var val = 0;
and I bet there are many people have val
as variable or field name in their code like me. I think val
is a bad choice
@Thaina Yes, I'm inclined to agree.
@Thaina
Especially because val was not keyword. I really have my code var val = 0; and I bet there are many people have val as variable or field name in their code like me. I think val is a bad choice
And you could continue to. Like var
, val
would be a contextual keyword, in that it only behaves like the keyword when it doesn't make sense for it to behave like anything else. So var val = 0;
would remain perfectly legal.
Although I do prefer let
to val
, mostly because I think it looks sufficiently different.
Oh yes! let
was the one I liked. Thanks!
@HaloFour It not breaking change I understand but it still ambiguous
BTW I still don't like let
. I prefer readonly
. But at least let
is better than val
let
is a bit early basic; also was read write. Not sure how let
implies readonly?
@benaadams
let
is a bit early basic; also was read write. Not sure howlet
implies readonly?
let
is also F# where it is the readonly (by default) binding of an identifier. let
is also C# LINQ where it is the declaration of a readonly range variable.
Personally the latter reason is enough for me. It's already a contextual keyword, and in that existing context it creates a readonly identifier.
let is also C# LINQ where it is the declaration of a readonly range variable.
Fair enough 😄
@gulshan
Any plan for readonly types(classes and structs)?
Do you mean immutable types? If so, that's a completely separate proposal (I'm pretty sure it's been made before).
ITNOA
@Richiban where I can found it?
@soroshsabz
ITNOA
That's a new one!
https://github.com/dotnet/roslyn/issues/7626 and https://github.com/dotnet/roslyn/issues/159 are probably what you're looking for.
Something I like about final
locals in Java is that it's possible to declare one as not assigned and then have branching logic to assign it. The Java compiler uses flow analysis to ensure that for each branch that the local is assigned exactly once.
final String name;
if (entity instanceof Person) {
Person person = (Person)person;
name = person.getFirstName() + " " + person.getLastName();
}
else if (entity instanceof Company) {
Company company = (Company)company;
name = company.getFirmName();
}
This can be useful in those scenarios where you want the local to be readonly, the expression to calculate it can throw and you want the scope of the local to exist beyond a try
block.
@Richiban thanks, but I hope to see comprehensive proposal about immutable object in csharplang project.
@HaloFour Is conditional operator ( ?: ) not sufficient for this purpose?
@soroshsabz
Sometimes not. I amended my comment to mention try
/catch
scenarios where C# offers no single expression. You could extract the logic to a separate function but that's more verbose ceremony. Even if it could be expressed as a single conditional expression sometimes it's more readable expanded out into multiple statements.
Either way, Java supports this, and I make use of it frequently enough that I think it would be useful here.
I think simple rule like "All local readonly
variables must be initializing immediately after declaration." cause to improve simplicity and readability for programmers. In Java case some programmers maybe surprise to final
variable has not initialize and must be track code to find the where is this variable initialize?
@HaloFour,
That's yet another use-case for match
:
let name = entity match (
case Person person : $"{person.FirstName} {person.LastName}",
case Company company : company.FirmName
);
@soroshsabz
We may differ on opinion there. If the expression has to be overly complex in order to satisfy an overly strict language feature that only decreases overall maintainability and readability. I'd rather the flow be logical and the compiler enforce readonly-ness where appropriate.
And as a Java programmer who works directly with hundreds of other Java programmers I can say that this has never been a source of confusion. It's a pattern used fairly frequently across the Java ecosystem. If anything I think I would find it much more annoying that I couldn't declare and assign a readonly
variable like this.
@DavidArno
It's just one exceptionally simple case. match
won't handle the exception handling scenario. And again, forcing the developer to try to pack it all into a one-liner, or to extract that local logic elsewhere, does not improve the readability or maintainability of the program.
In C# we actually have elaborate flow analysis for struct
that it must be set all field properly if we not call the constructor, else it will cause compile error
I agree that readonly
should do the same
I've always liked the idea that readonly
can be applied to local and parameter declarations, e.g.:
public void MyMethod(readonly int arg) //...
and
void Main()
{
readonly string[] items = new [] {"one", "two"};
}
with the added feature that let
can be used a shorthand for readonly var
:
void Main()
{
let items = new [] {"one", "two"};
}
However, you could allow the splitting of declaration and assignment if you write it out in full. The existing definite assignment analysis can be used to make sure that the variable is never overwritten:
void Main()
{
readonly string name;
try {
name = MakeWebRequest("http://my.resource/name");
}
catch (Exception) {
name = null;
}
// Here name is definitely assigned, and now cannot not be written to (Compiler error)
}
But what happens if the variable is perhaps not assigned? Can we still write to it?
void Main()
{
readonly string name;
if(person != null)
{
name = person.Name
}
name = "Bob"; // Is this allowed?
}
@Richiban No you must
if(person != null)
name = person.Name;
else name = "Bob"; // must else
instead
By the way, I was prefer my idea of returnable block #249 instead of using flow analysis as that
local readonly
is one of my reason to make #249 but I forgot to mention it
@Richiban It is unlikely that we would consider adopting something analogous to Java's "blank final" variables or definite unassignment rules. A readonly local would have to be initialized at the point where it is declared.
I understand the desire to support try/catch, but I'm having a hard time envisioning how that would work. Consider:
readonly int x;
try {
DoSomething();
x = 1;
DoSomethingElse();
}
catch {
x = 0;
}
We can see that x
is definitely assigned. However, the value of x
could be assigned twice depending on where the exception occurs.
@Grauenwolf I agree. It makes much more sense that readonly locals insist on being declared and assigned in the same statement, much like var is today.
To be clear, I would prefer that readonly int x
doesn't require immediate assignment. I just don't know if it is possible for this use case.
@Grauenwolf
In Java that code would be a compiler error, specifically because x
isn't definitely assigned exactly once. The following would be legal:
final int x;
try {
x = CalculateX();
}
catch (Throwable e) {
x = 123;
}
The following would not:
final int x;
try {
x = CalculateX();
DoSomethingElse();
}
catch (Throwable e) {
x = 123;
}