Skip to content
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: field keyword in properties #140

Open
lachbaer opened this issue Feb 17, 2017 · 394 comments
Open

Proposal: field keyword in properties #140

lachbaer opened this issue Feb 17, 2017 · 394 comments

Comments

@lachbaer
Copy link
Contributor

lachbaer commented Feb 17, 2017

Proposal: field keyword in properties

(Ported from dotnet/roslyn#8364)

Specification: https://github.com/dotnet/csharplang/blob/main/proposals/field-keyword.md

LDM history:

@scottdorman
Copy link
Contributor

This could also be useful/part of the implementation for #133.

@HaloFour
Copy link
Contributor

My opinion is that this proposal makes auto-implemented properties more difficult to read by introducing these silent backing fields despite the use of logic. With expression-bodied accessor members a chunk of the boilerplate for standard properties already disappears. The IDE eliminates much of the rest via standard code snippets.

@lachbaer
Copy link
Contributor Author

Suggestion:

  • only allow setters to be defined in auto-properties (making them semi-auto)
  • this setter expects the value to be assigned as a return value
  • to set it in the middle of the block, yield return is used.
int ASemiAutoProp
{
    get;
    set
    {
        var pcea = new PropertyChangeEventArgs(this.ASemiAutoProp, value));
        OnPropertyChanging( pcea );
        yield return value;
        OnPropertyChanged( pcea );
    }
}

Then there is no need for a new field keyword/variable.

And the following really looks better than with #133

public T PropertyInitialized
{
    get;
    set => value ?? new ArgumentNullException();
} = default(T);

instead of (with #133) - a bit much of backingField.

public T PropertyInitialized
{
    T backingField = default(T);
    get => backingField;
    set => backingField = value ?? new ArgumentNullException();
}

@lachbaer
Copy link
Contributor Author

lachbaer commented Mar 7, 2017

Instead of using a field keyword, I also suggest following syntax borrowed from C struct:

public string FirstName
{
    get;
    set => _name = value;
} _name;

_name is by definition private to it's type (maybe unless directly prepended by an access modifier?).
OR just scoped to the corresponding property, what would even be better.

In case expression-bodied set of semi-auto-properties expect a value being returnd to assign to the backing-field (here with additional initializer):

public string FirstName
{
    get;
    set => value;
} _name = "(not set)";

This completely eradicates the need for a field keyword or yield return as suggested above, because now the backing field's identifier can be used

public string FirstName
{
    get;
    set {
        var pcea = new PropertyChangeEventArgs(_name, value));
        OnPropertyChanging( pcea );
        _name = value;
        OnPropertyChanged( pcea );
    }
} _name = "(not set)";

This would also go hand in hand with #133.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 7, 2017

@lachbaer

With the declaration of the field being outside of the property block that would imply that the scope of that field would be for the entire class, not for just that property. That's also consistent with the scoping of that C syntax.

@lachbaer
Copy link
Contributor Author

lachbaer commented Mar 7, 2017

@HaloFour
Sure, but in this case we could probably rely on IntelliSense not to spoil the rest of the module.
Also without any prepending modifier it would imply internal access. (corrected, of course class members are private by default) 🙄
But nevertheless, I think that this is a neat way to achieve at least some of the goals.
Also the costs of implementing it could be reasonable.

@lachbaer
Copy link
Contributor Author

lachbaer commented Mar 7, 2017

Of course specifying a getter only works, too:

public string FirstName
{
    get {
        Debug.Assert( _name != null, "Getting name before setting it?");
        return _name;
    }
    set;
} _name = null;

or combined:

public string FirstName
{
    get {
        Debug.Assert( _name != null, "Getting name before setting it?");
        return _name;
    }
    set => value ?? throw new ArgumentNullException();
} _name;

In the latter example, just specifying the _name identifier qualifies for a semi-auto-property and therefore for set => value .

@jnm2
Copy link
Contributor

jnm2 commented Mar 7, 2017

@HaloFour

My opinion is that this proposal makes auto-implemented properties more difficult to read by introducing these silent backing fields despite the use of logic.

I don't disagree with your whole comment, but I wanted to point out that the difficulty reading is only if you're accustomed to thinking of logic and automatic backing fields as mutually exclusive. I don't think they should be. It's not hard to learn to look for a field keyword. It'll be right where you'll look if you're looking for the backing field anyway.

The reason I really like this proposal is not because it enables me to write less code, but because it allows me to scope a field to a property. A top source of bugs in the past has been inadequate control over direct field access and enforcing consistent property access. Refactoring into multiple types to add that desirable scope is quite often not worth it.

To that end, a field keyword makes perfect sense. I should not care or have to care what the backing field's name is. Ideally, it doesn't have one. This adds the convenience that renaming properties gets a lot easier.

Not infrequently I'm renaming constructs such as this, where Set handles INotifyPropertyChanged.PropertyChanged:

private bool someProperty;
public bool SomeProperty { get { return someProperty; } private set { Set(ref someProperty, value); } }

This kills two birds with one stone: 1) scope safety, 2) more DRY, less maintenence:

public bool SomeProperty { get; private set { Set(ref field, value); } }

@gafter
Copy link
Member

gafter commented Mar 24, 2017

/cc @CyrusNajmabadi

@lachbaer
Copy link
Contributor Author

lachbaer commented Apr 27, 2017

Meanwhile some time went by. I'd like to state my opionion on the questions from the inital post.

Allow both accessors to be defined simultaneously?

Yes, definitely. A nice example is the sample at the end of this comment. A semi-auto-property with an implicit field should be constructed under either or both these circumstances:

  • both, getter and setter are declared, but either is get; or set; the other having a statement/block
  • an initializer is declared and the property is not an auto-property

Assing expression bodied setters? and Assign block body with return?

I'd like to have that, because simply it looks nice and would totally fit into how "assign-to"-return expressions look.

But introducing to much new behaviour and especially having the compiler and reading user to differentiate between return and non-returning bodies can be confusing. Therefore I'd go with "no" on this currently.

Prohibit field keyword if not semi-auto?

No, but it must not be highlighted by the IDE in that case, because it that context it is no keyword anymore. I think it is very unlikely that somebody converts a semi-auto-property to an ordinary property and simultaneously has a 'field' named field in scope.

If property-scoped fields become availble shall that feature be available for semi-auto-properties as well?

Yes, if any possible. SAPs allow both, getter and setter, to be defined. It would make sense to make no difference versus normal properties to restrict that feature.

@jamesqo
Copy link
Contributor

jamesqo commented Jun 14, 2017

Taken from @quinmars at #681 (comment), this feature would allow devs to write a 1-liner to implement lazy initialization:

public T Foo => LazyInitializer.EnsureInitialized(ref field, InitializeFoo);

private T InitializeFoo() { ... }

@mattwar
Copy link
Contributor

mattwar commented Jun 14, 2017

@jamesqo Except using this LazyInitializer method has a downside (unless the initializer method is static) because you'll be creating a delegate instance each time the property is accessed.

What you want to write is:

public T Foo => 
{
   if (field == null)
   {
        System.Threading.Interlocked.CompareExchange(ref field, InitializeFoo(), null);
   }

   return field;
};

And now its not really a one-liner.

@jamesqo
Copy link
Contributor

jamesqo commented Jun 15, 2017

@mattwar Well they said they intend to cache point-free delegates eventually. Also what if people don't care about extra allocations because the initializer is doing something like network I/O which completely dwarfs allocating a few extra objects?

@lachbaer
Copy link
Contributor Author

public T Foo => LazyInitializer.EnsureInitialized(ref field, InitializeFoo);

However the field keyword should not be available to every property per se. It then should be decorated with a modifier, like e.g. public field T Foo ....

@yaakov-h
Copy link
Member

@lachbaer why?

@lachbaer
Copy link
Contributor Author

@yaakov-h For two reasons. First, so that you can see from the signature whether a (hidden) backing field is produced by the property. Second, to help the compiler finding out whether an automatic backing field shall be produced or not.

@yaakov-h
Copy link
Member

First reason... I can see the value on a getter-only autoproperty like above, but on a get/set one I see little point.

Second reason I don't think is necessary. If the property's syntax tree contains any reference to an undefined variable named field, then you know it needs to emit an automatic backing field.

@lachbaer
Copy link
Contributor Author

The alternative is to start thinking about properties a bit differently. Every property has an explicit (synthesized) backing field, unless it is opt'ed out, because it is not needed (no get; or set; and no field used). This little change in philosophy is even backwards compatible in (emmited) code.

@jnm2
Copy link
Contributor

jnm2 commented Jun 15, 2017

I think a field property modifier is unnecessary verbosity. The keyword should just be available for use the same place you look at to see the backing field anyway.

@mattwar
Copy link
Contributor

mattwar commented Jun 15, 2017

@jamesqo That delegate caching is for static delegates where they are not being cached in some circumstances today. It is unlikely that the InitializeFoo method is static.

@jamesqo
Copy link
Contributor

jamesqo commented Jun 16, 2017

@mattwar Then we can simply open a corefx proposal to add EnsureInitialized overloads that accept state? e.g.

public class LazyInitializer
{
    public static T EnsureInitialized<T, TState>(ref T value, TState state, Func<TState, T> factory);
}

public T Foo => EnsureInitialized(ref field, this, self => self.InitializeFoo());

And again, the extra allocations may be dwarfed by the initialization logic in some cases.

@sonnemaf
Copy link

Instead of introducing a new 'field' keyword you could maybe use the _ (discard) symbol. This would avoid conflicts in old code.

class Employee {

    public string Name {
        get { return _; }
        set { _ = value?.ToUpper(); }
    }

    public decimal Salary {
        get;
        set { _ = value >= 0 ? value : throw new ArgumentOutOfRangeException(); }
    } = 100;
}

I think this could work.

@lachbaer
Copy link
Contributor Author

@sonnemaf _ = value; already is valid code (it evaluates the right hand expression and discards its result). Therefore it does not work.

@lachbaer
Copy link
Contributor Author

Still there are some more questions to answer.

  1. What happens when an [inherited] field with the name field exists?
  2. What if Proposal: Property-Scoped Fields #133 arrives and a property-scoped variable named field is declared?
  3. What if field was a delegate and a member method named field is available?
  4. If a local named field is declared, what about highlighting that keyword within the accessor?

When thinking about it some time, you can come up with quite a long ruleset that must be followed. By always adding the modifier field to the property, this feature is switched on by demand and any occurance of the identifier field within the accessors references the backing-field, shadowing all other members with that name.

@yaakov-h
Copy link
Member

@lachbaer: I'd expect an existing variable, field or property named field would take precedence, otherwise backwards compatibility dies. field as a keyword would have to be conditional on nothing else having that name.

@KennethHoff
Copy link

Out of curiosity, what was the outcome of the breaking changes discussion that blocked this?

#7918

@jnm2
Copy link
Contributor

jnm2 commented Jul 4, 2024

@KieranDevvs #7964 is going forward (https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-15.md#field-and-value-as-contextual-keywords).

@DeimoSVK
Copy link

Am I only one who is extremely sceptical this will finally make it this year?

@luislhg
Copy link

luislhg commented Aug 3, 2024

I just want to point out that MVVMToolkit has a number of features in its generator that you are probably interested in, including notifying dependent properties, commands, running custom code in the setter, and validation. It can't solve the F12 pain, and I have that pain myself, but several of your complaints have features for them.

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/observableproperty

Just wanted to point out an open source extension that is supposed to easy the pain for Go to Definition (F12) with MVVM Toolkit [RelayCommand] (among some other small quality of life improvements).

The extension will automatically identify if VS opened a tab for the source generator file from MVVM Toolkit, if so it finds and opens the correct method and file (e.g. ViewModel) and closes the source generator tab.

It happens very quickly as you can see here

GoToDefinition-Simple01

BrightXaml Extension (github/VS marketplace): https://github.com/luislhg/BrightExtensions?tab=readme-ov-file#bright-xaml

Hopefully the new field should help a lot with properties/[ObservableProperty] but I think [RelayCommand] will still be very useful.

@jnm2
Copy link
Contributor

jnm2 commented Aug 3, 2024

Out of curiosity, what was the outcome of the breaking changes discussion that blocked this?

@KieranDevvs #7964 is going forward (https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-15.md#field-and-value-as-contextual-keywords).

The LDM revisited this and decided not to make changes to value, and to make field a keyword only in primary expressions. (field is a primary expression in M(field) but not a primary expression in this.field.)

https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-15.md#field-keyword

@glen-84
Copy link

glen-84 commented Aug 4, 2024

Would it still make sense to have an analyzer that suggests avoiding the use of value in primary expressions for other purposes, so that this change could possibly be made in the future?

@jnm2
Copy link
Contributor

jnm2 commented Aug 4, 2024

@glen-84 At this point there was no sense of "maybe we'll do this later," and while anything is possible, the weight of the breaking changes was a significant enough factor that I wouldn't expect it.

@jnm2
Copy link
Contributor

jnm2 commented Aug 8, 2024

Working group meeting notes for Aug 7 are available!
Agenda: Nullability analysis with the field keyword
Discussion thread: #8347

@seanblue
Copy link

seanblue commented Aug 8, 2024

@jnm2 From the notes it looks like the discussion is largely around when the nullability feature is enabled, so I wanted to clarify something for when it's disabled. Would the proposal mean that for any value type it would still be possible for the backing field to be the nullable version? Basically, allowing the following:

public int Prop1 => field ??= GetDefault1();

public SomeStruct Prop2 => field ??= GetDefault2();

It's unclear to me if those AllowNull and MaybeNull properties are just relevant when the nullability feature is enabled or if they're still needed when it's disabled (to allow the above example), just only for types that can't inherently be null.

Hopefully my question makes sense. This is the first I'm hearing of these attributes in relation to this feature, so I'm still catching up.

@jnm2
Copy link
Contributor

jnm2 commented Aug 8, 2024

@seanblue Unfortunately this clarification was buried within the proposal rather than being stated up front. The proposal only applies to properties whose type is both:

  1. Non-nullable (no ? annotation)
  2. Not a value type (either a reference type, or a generic type with no value constraint)

We don't plan to have a provision for the runtime type of the field type to differ, e.g. System.Nullable<int> versus int. This is all about the static analysis and annotations that are part of the Nullable Reference Types C# feature.

The potential of doing #133 is usually the fallback we think of for cases like yours. You declare the field yourself, but still have the benefit of scoping it to the property. It might look like this:

public int Prop1 { int? field; get => field ??= GetDefault(); }

However, consider that even without #133, you can still do lazy loading for value types if there are sentinel values that are palatable for your purposes:

public ImmutableArray<string> Values => field.IsDefault ? (field = LoadValues()) : field;
public int Prop1 { get => field == int.MinValue ? (field = GetDefault()) : field; } = int.MinValue;

@seanblue
Copy link

seanblue commented Aug 9, 2024

@jnm2 That's a shame, but understandable. Thankfully even without this, the field keyword covers 90% of my use cases.

@jnm2
Copy link
Contributor

jnm2 commented Aug 9, 2024

Finally, this option is available, where you access Prop1.Value each time the property is used rather than simply Prop1.

[NotNull]
public int? Prop1 => field ??= GetDefault1();

@jnm2
Copy link
Contributor

jnm2 commented Aug 26, 2024

The proposal is updated with the working group's accepted proposal for nullability analysis of field.

LDM 1: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-08-14.md
LDM 2: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-08-21.md#field-keyword-nullability

Updated spec section: https://github.com/dotnet/csharplang/blob/main/proposals/field-keyword.md#nullability

@WeihanLi
Copy link
Contributor

Would this come in .NET 9 RC 2 release?

@Aniobodo
Copy link

Would this come in .NET 9 RC 2 release?

Can somebody provide an answer to this question? ... As bad as it could sound, please let us hear it :)

@jnm2
Copy link
Contributor

jnm2 commented Sep 19, 2024

These kinds of questions can be as hard to answer as when any software feature will be done.

@WeihanLi
Copy link
Contributor

WeihanLi commented Sep 20, 2024

These kinds of questions can be as hard to answer as when any software feature will be done.

@jnm2 since the .NET 9 RC1 had been released, have the C# 13 features been frozen? If so, we may should drop the hope for the feature in C# 13, if not, is there a timeline for the C# feature freeze?

@jnm2
Copy link
Contributor

jnm2 commented Sep 20, 2024

Sorry, I wish I could help answer that. I share your enthusiasm for the feature!

@WeihanLi
Copy link
Contributor

the current plan is that field will be under langversion preview at .NET 9 RTM. These projects build with langversion preview and hence will hit the new behavior during source build.

seeing this from @jaredpar @cston on dotnet/runtime#108219 (comment)

@ebekker
Copy link

ebekker commented Oct 10, 2024

I know this is kinda late in the process already, but I was curious if an alternate field alias syntax was already considered (I couldn't readily find it in this ticket or the new keyword proposal so sorry if this has already been considered and rejected). I'm suggesting something like:

int PropertyName as fieldAlias
{
    get => fieldAlias;
    set => fieldAlias = value;
}

This would allow you to support all of the same scenarios and features that already described in the proposal but without the complications of adding a new keyword field. In this scenario, fieldAlias can be any valid identifier, it's only scoped to the get and set (and init) bodies, and it just aliases to the auto-generated backing field.

Compared to the field keyword proposal, it only adds a little bit of additional ceremony, and can actually be more succinct when used in small code fragments.

public string? CanonName as f
{
    get;
    set => f = value?.ToUpper();
}

@MarkusRodler
Copy link

@ebekker I'm fine, es long as C# keeps on evolving.

@bjbr-dev
Copy link

@ebekker I proposed a while ago that we allow function arguments to the get and set, which is basically the same as providing an alias.
#140 (comment)

Unfortunately it didn't seem to gain traction :(

@chucker
Copy link

chucker commented Oct 11, 2024

@bjbr-dev

@ebekker I proposed a while ago that we allow function arguments to the get and set, which is basically the same as providing an alias. #140 (comment)

Unfortunately it didn't seem to gain traction :(

Incidentally, VB.NET does this.

    Property Foo As String
        Get
            Return _foo
        End Get
        Set(value As String)
            _foo = value
        End Set
    End Property

Here, value is suggested by the IDE for the setter, but unlike C#, it isn't a keyword, nor does it get special treatment.

@eidylon
Copy link

eidylon commented Oct 11, 2024

@bjbr-dev

Incidentally, VB.NET does this.

Property Foo As String
    Get
        Return _foo
    End Get
    Set(value As String)
        _foo = value
    End Set
End Property

Here, value is suggested by the IDE for the setter, but unlike C#, it isn't a keyword, nor does it get special treatment.

One additional nice feature with VB was being able to add additional parameters to get/set (that needed to match).
It wasn't something that I used a LOT, but from time to time, there were specific instances where it made for a much more natural and cleaner class. Every now and then, I run into one of those, and do miss that feature in C# .

@HaloFour
Copy link
Contributor

@eidylon

That would be "indexed properties": #471

@jnm2
Copy link
Contributor

jnm2 commented Oct 11, 2024

@ebekker It's been a while, but to my memory we did consider an ability to use an alias for the field name. One of the goals of the feature was to not have to think about a name for each property's backing field. f is an awesome alias, but probably only for a minority of people, and so the length overall increases dramatically if you have to add as field to each property. We preferred for this common scenario to have less ceremony. That's not to say that the language would not evolve further, but the current proposal seems like a great and simple starting point for a wide range of users and uses.

@FrancisHogle
Copy link

I've been playing with the new preview bits and ran into some quirks. In particular, I have a property of the form:

private IEnumerable<thing> _myField = [];

public IEnumerable<thing> Foo
{
    get => _myField;
    private set
    {
        // pre-assignment logic
        _myfield = value;
        // post-assignment logic and side-effects
    }
}

public void Dispose() => Foo = [];

When converting this to use field, I tried:

public IEnumerable<thing> Foo
{
    get;
    private set
    {
        // pre-assignment logic
        field = value;
        // post-assignment logic and side-effects
    }
} = [];

...but this contains some errors:

  1. It complained about the auto getter with a manual setter (though I thought I saw Mads demo mixing and matching in a recent standup)

  2. I ran afoul of the "no initializers when you have a setter" rule - can that be revisited with the field keyword?

  3. (unrelated but worth a mention) It seemed like the incremental compiles in the editor (VsCode) recognized the new syntax after updating the SDK but "dotnet build" still did not. I have marked the csproj with both LangVersion="preview" and EnablePreviewFeatures=True.

Stepping back a bit, I would really like to have BOTH a setter AND direct access to 'field' - If my property is non-nullable reference type, I will still see a null prior value when assigning from a constructor. Perhaps "PropertyName.field" would be a reasonable way to access this? I would not restrict it to constructors only. While finalizers are rare and discouraged, they are supported and need similar access to what Constructors get. Further, a case can be made for similar access in Dispose. I know I can make my non-nullable setter handle nulls, but I'm complicating the "normal case" setter to handle "abnormal case" initialization and shutdown. While I have always wanted a bevy of other property accessors, I think the variety of ways storage can be used justified some sort of direct-but-cumbersome syntax that only makes sense for constructors / initializers and the like. I did read the other proposal about full-fledged declarations in the property scope and maybe if it covers rare access to field "outside the property" that would do it,

@DavidArno
Copy link

@FrancisHogle,

How are you trying to test that code? I'm running VS 17.12.0 Preview 3.0, created a .NET 9 project and set the language version to preview and that code (with thing changed to eg int) compiles just fine.

@jnm2
Copy link
Contributor

jnm2 commented Oct 18, 2024

@FrancisHogle Everything you're asking about is approved, implemented, and shipped in Visual Studio 17.12 Preview 3.0. My VS Code is also happy with the sample you provided (C# Dev Kit v1.11.14), but my dotnet build with 9.0.100-rc.2 does fail with the errors you mention. I'm not sure if RC2 can be used to build with this feature yet.

Adding #error version to the code shows:

  • Visual Studio 1712-p3 (✔️ working): Compiler version: '4.12.0-3.24509.5 (bde21ee2)'. Language version: preview.
  • VS Code, C# Dev Kit v1.11.14 (✔️ working): Compiler version: '4.13.0-1.24477.2 (3da6b6d8)'. Language version: preview.
  • dotnet build, SDK 9.0.100-rc.2 (❌ not working): Compiler version: '4.12.0-3.24473.3 (5ef52ae3)'. Language version: preview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests