dotnet / csharplang

The official repo for the design of the C# programming language
10.92k stars 994 forks source link

Champion "readonly for locals and parameters" #188

Open gafter opened 7 years ago

gafter commented 7 years ago

See also https://github.com/dotnet/roslyn/issues/115

Design review

gulshan commented 7 years ago

Any plan for readonly types(classes and structs)?

Thaina commented 7 years ago

should support

readonly i = 0; // shorthand for readonly var
const j = 0; // shorthand for const var
jnm2 commented 7 years ago

Wasn't val going to be short for readonly var?

Thaina commented 7 years ago

@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

jnm2 commented 7 years ago

@Thaina Yes, I'm inclined to agree.

HaloFour commented 7 years ago

@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.

jnm2 commented 7 years ago

Oh yes! let was the one I liked. Thanks!

Thaina commented 7 years ago

@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

benaadams commented 7 years ago

let is a bit early basic; also was read write. Not sure how let implies readonly?

HaloFour commented 7 years ago

@benaadams

let is a bit early basic; also was read write. Not sure how let 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.

benaadams commented 7 years ago

let is also C# LINQ where it is the declaration of a readonly range variable.

Fair enough πŸ˜„

Richiban commented 7 years ago

@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).

soroshsabz commented 7 years ago

ITNOA

@Richiban where I can found it?

Richiban commented 7 years ago

@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.

HaloFour commented 7 years ago

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.

soroshsabz commented 7 years ago

@Richiban thanks, but I hope to see comprehensive proposal about immutable object in csharplang project.

soroshsabz commented 7 years ago

@HaloFour Is conditional operator ( ?: ) not sufficient for this purpose?

HaloFour commented 7 years ago

@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.

soroshsabz commented 7 years ago

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?

DavidArno commented 7 years ago

@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
);
HaloFour commented 7 years ago

@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.

Thaina commented 7 years ago

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

Richiban commented 7 years ago

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?
}
Thaina commented 7 years ago

@Richiban No you must

if(person != null)
    name = person.Name;
else name = "Bob"; // must else

instead

Thaina commented 7 years ago

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

gafter commented 7 years ago

@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.

Grauenwolf commented 7 years ago

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.

Richiban commented 7 years ago

@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.

Grauenwolf commented 7 years ago

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.

HaloFour commented 7 years ago

@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;
}
Grauenwolf commented 7 years ago

Technically that works, but I don't know how I would be able to explain to someone why DoSomethingElse(); makes x=123 illegal.

HaloFour commented 7 years ago

@Grauenwolf

Java programmers seem to understand it just fine. Java has always been strict about this single assignment rule with final, even with fields.

Even if it's not added at this time I don't see why it couldn't be considered again in the future. The declaration readonly int x; would simply be a compiler error now.

Grauenwolf commented 7 years ago

At the end of the day it probably won't be used much. So while I would like delayed assignment, I could live without it.

-----Original Message----- From: "HaloFour" notifications@github.com Sent: β€Ž4/β€Ž7/β€Ž2017 3:33 AM To: "dotnet/csharplang" csharplang@noreply.github.com Cc: "Jonathan Allen" grauenwolf@gmail.com; "Mention" mention@noreply.github.com Subject: Re: [dotnet/csharplang] Champion "readonly for locals and parameters"(#188)

@Grauenwolf Java programmers seem to understand it just fine. Java has always been strict about this single assignment rule with final, even with fields. Even if it's not added at this time I don't see why it couldn't be considered again in the future. The declaration readonly int x; would simply be a compiler error now. β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

alrz commented 7 years ago

Link to the porposal: https://github.com/dotnet/csharplang/blob/master/proposals/readonly_locals.md

alrz commented 7 years ago

"immutable objects" are also related here: https://github.com/dotnet/csharplang/issues/421.

Eirenarch commented 6 years ago

While I like immutable values in F# has anyone considered the legacy code issues here (maybe I missed a comment?)

Specifically the greatest value in this feature will be indicating to the reader that a value is not supposed to be changed. However we have a great body of code where variables were declared mutable so there is no way to know the intent behind these. It makes legacy code misleading because why would variables which are not mutated not declared as such? It seems to me that this issue offsets the benefit that the proposal would provide information about read only variables.

Also note that being explicit here does not provide any new information to the compiler. Java developers often talk about "final or effectively final" the compiler has the full information to know if the variable is changed and therefore can perform any optimizations on readonly variables without the need to declare them explicitly.

Grauenwolf commented 6 years ago

It makes legacy code misleading because why would variables which are not mutated not declared as such?

Make a refactoring command to globally change any local variable that's never mutated into an immutable variable. Poof, no more legacy code.

Might even make it an informational warning like we have for member variables that are not readonly but never changed outside of a constructor.

-----Original Message----- From: "Stilgar" notifications@github.com Sent: β€Ž6/β€Ž3/β€Ž2017 3:23 AM To: "dotnet/csharplang" csharplang@noreply.github.com Cc: "Jonathan Allen" grauenwolf@gmail.com; "Mention" mention@noreply.github.com Subject: Re: [dotnet/csharplang] Champion "readonly for locals and parameters"(#188)

While I like immutable values in F# has anyone considered the legacy code issues here (maybe I missed a comment?) Specifically the greatest value in this feature will be indicating to the reader that a value is not supposed to be changed. However we have a great body of code where variables were declared mutable so there is no way to know the intent behind these. It makes legacy code misleading because why would variables which are not mutated not declared as such? It seems to me that this issue offsets the benefit that the proposal would provide information about read only variables. Also note that being explicit here does not provide any new information to the compiler. Java developers often talk about "final or effectively final" the compiler has the full information to know if the variable is changed and therefore can perform any optimizations on readonly variables without the need to declare them explicitly. β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

Eirenarch commented 6 years ago

Refactoring tool is not a bad solution. Still the syntax should be as short as the mutable syntax or people will fight it instinctively.

Grauenwolf commented 6 years ago

Definitely. That’s why I like β€œset x = 5” or β€œlet x = 5”.

From: Stilgar Sent: Saturday, June 3, 2017 12:01 PM To: dotnet/csharplang Cc: Jonathan Allen; Mention Subject: Re: [dotnet/csharplang] Champion "readonly for locals and parameters"(#188)

Refactoring tool is not a bad solution. Still the syntax should be as short as the mutable syntax or people will fight it instinctively. β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

marinasundstrom commented 6 years ago

The keywords const and readonly are all about how to bind to a local or field.

Both share the trait that they can only be assigned to once, at the time they are being declared. However, they are used in different contexts with slightly different semantics.

As this proposed feature only has meaning at compile-time, it seems logical that const should be the keyword to use. Something unchangeable is intuitively referred to as a "constant".

Syntax proposal

const modifies a declaration so that it is only allowed to assign to the name one once, and only when it is being declared.

const Foo x = ...;

Shorter, simplified and probably preferred way with type inference:

const foo = ...

This mirrors var but with the inability to assign to the variable more than once.

HaloFour commented 6 years ago

@robertsundstrom

I disagree. The behavior of these parameters/locals much more closely resembles that of readonly fields. It's true that the behavior is entirely enforced by the compiler and not by the CLR, but the compiler also enforces readonly fields today as well.

The big differences are that const members need to be fully evaluated and bound at compile time. The value must be a literal constant (or in a very narrow set of expressions that the compiler is willing to evaluate at compile time). It's not legal to assign them the result of an arbitrary expression, as is the case with readonly fields. const members also don't really exist. For fields the compiler will emit a literal field which serves only as metadata but that field doesn't exist and cannot be directly referenced; the compiler is required to actually use the literal value itself.

These locals and parameters will be bound at runtime to any arbitrary expression. The compiler is only enforcing that the local or parameter cannot be reassigned. With the exception of being allowed to assign/reassign a field in the constructor this behavior closely matches that of readonly fields. They'll also remain proper local slots and arguments and the IL will continue to reference them directly, also very much like readonly fields.

Also, const locals already exist and have very different semantics.

Pzixel commented 6 years ago

AFAIR local readonly wasn't introduced because scope of method is small enough to not enforce it by compiler. What is changed since we have decided it's not needed?

aluanhaddad commented 6 years ago

Just to bikeshed a little bit more about the syntax, in addition to my previously stated reasons for wanting val TL;DR: the relationships between verbs, nouns, and noun-modifiers, and the flow of grammar. And also recalling that in F#, let is shorthand for let identifier = value in expression and is used consistently.

I would like to add that, although there is a precedent in F# to use let, the reality is that more programmers are likely to be familiar with let as it is used in ECMAScript where it is the practical analog to C#'s var.

HaloFour commented 6 years ago

@aluanhaddad

I've mentioned it already but I think the more important precedent for let already exists in C#. Why would you use let to define a readonly variable in one context and val to define a readonly variable in another context? Why define yet another contextual keyword?

aluanhaddad commented 6 years ago

@HaloFour One might just as well ask why, given that query variables are inherently readonly just like foreach declarators, one would introduce let as a way to define a readonly local in LINQ expressions when var could have been reused?

Or for that matter, why introduce a where clause in LINQ when if could have been reused?

I believe it was because

from customer in customers
var address = customer.Address
if address.City == "London"
select address.PostCode

quite possibly because it doesn't read as well as

from customer in customers
let address = customer.Address
where address.City == "London"
select address.PostCode
HaloFour commented 6 years ago

@aluanhaddad

Perhaps. But let means the same thing in both cases whereas var is only readonly in that one specific instance.

Apart from that I prefer let because it visually stands out compared to var. It's also pronounced quite differently and wouldn't be a homophone in some Asian languages like Japanese (yeah, I'm reaching here).

Richiban commented 6 years ago

As a number of people have pointed out, let reads nicely in local declarations e.g. let message = "Hello world", but poorly in foreach statements. Since the loop variables in foreach statements are inherently readonly we've already proposed not allowing the let keyword but instead sticking with var. This leaves us in the slightly unsatisfactory position of having the meaning of var for 'mutable' and let for 'immutable' except in a foreach statement which has var for 'immutable' and nothing for 'mutable'.

To solve this confusion I recommend that we... remove the keyword entirely from foreach loops. Controversial, I know! But, hear me out.

Given that this:

var i = 0;

foreach (i in new[] { 1, 2, 3 })
{
}

is currently illegal and specifically gives a compiler error "Type and identifier are both required in a foreach statement", can't we just drop the keyword completely in the foreach declaration? Since the contents of the foreach declaration must contain a variable declaration I don't see the problem. The type of the variable (or var keyword would become optional), just how it is in Linq:

var _ =
    from i in new[] { 1, 2, 3 }
    select i;

var _ =
    from int i in new[] { 1, 2, 3 }
    select i;

So, in my proposal, foreach loops would become:

foreach (i in new[] { 1, 2, 3 })
{
}
orthoxerox commented 6 years ago

This leaves us in the slightly unsatisfactory position of having the meaning of var for 'mutable' and let for 'immutable' except in a foreach statement which has var for 'immutable' and nothing for 'mutable'.

I agree. It's only slightly unsatisfactory. Iteration variables are immutable to avoid stupid errors, and I don't think anyone minds that. Yes, Captain Hindsight is right that they should've been prefixed with let for consistency, but he's never around when the decisions are made.

Mutable iteration variables (whenever they are supported) should probably be indicated as ref var or ref Type, as this will clearly indicate the expected behavior.

Grauenwolf commented 6 years ago

can't we just drop the keyword completely in the foreach declaration?

Yes please. Coming from a VB background where that wasn't necessary, I always found that to be an annoying bit of boilerplate.

-- Jonathan Allen Scholars of Alcala https://www.meetup.com/Scholars-of-Alcala 619-933-8527 <#UNIQUE_IDSafeHtmlFilter>

On Tue, Jul 25, 2017 at 1:42 AM, Richard Gibson notifications@github.com wrote:

As a number of people have pointed out, let reads nicely in local declarations e.g. let message = "Hello world", but poorly in foreach statements. Since the loop variables in foreach statements are inherently readonly we've already proposed not allowing the let keyword but instead sticking with var. This leaves us in the slightly unsatisfactory position of having the meaning of var for 'mutable' and let for 'immutable' except in a foreach statement which has var for 'immutable' and nothing for 'mutable'.

To solve this confusion I recommend that we... remove the keyword entirely from foreach loops. Contraversial, I know! But, hear me out.

Given that this:

var i = 0; foreach (i in new[] { 1, 2, 3 }) { }

is currently illegal and specifically gives a compiler error "Type and identifier are both required in a foreach statement", can't we just drop the keyword completely in the foreach declaration? Since the contents of the foreach declaration must contain a variable declaration I don't see the problem. The type of the variable (or var keyword would become optional), just how it is in Linq:

var = from i in new[] { 1, 2, 3 } select i; var = from int i in new[] { 1, 2, 3 } select i;

So, in my proposal, foreach loops would become:

foreach (i in new[] { 1, 2, 3 }) { }

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/csharplang/issues/188#issuecomment-317670653, or mute the thread https://github.com/notifications/unsubscribe-auth/AKIqTdRZzCfNsyMpVa1UNXpm7yz5tNzSks5sRap_gaJpZM4MMfi3 .

HaloFour commented 6 years ago

Conversely the C# team could always decide to reverse the decision that the iteration variable is immutable in that case. It wouldn't break any existing code to do so. But I am perfectly happy to have the C# language continue to enforce that iteration variables are always immutable but to allow for them to be explicitly declared as readonly or let/val:

foreach (var i in numbers) { ... }
foreach (readonly var in numbers) { ... }
foreach (let i in numbers) { ... }

As for the VB.NET case, variable declarations just work very differently in that language. As the type information is generally a suffix following a specific keyword (Dim) and an identifier it made sense for type inference to simply permit the omission of that type information. That creates a number of oddities in other parts of the language where the syntax allows for inline declaration of a variable when that type information is provided and a different behavior for when it is not. So now For Each can have multiple behaviors. Worse, because For Each could refer to an existing identifier, you could accidentally overwrite another variable or accessible field when your intention was simply to infer the type of a newly declared iteration variable.

C# can at least guard against that specific behavior as foreach has never permitted reusing an existing identifier as the iteration variable. But I don't think it makes sense to omit the declaration syntax entirely. It simply doesn't read as a declaration without it and could easily be mistaken as a reuse of some existing variable. And frankly, four characters is exceptionally far from an onerous amount of boilerplate.