Champion issue: #9031
Champion issue for field
keyword: #8635
Related discussion: #8704
Allow direct assignment and use of a property's backing field during construction, without having to invoke the setter, via a new fieldof(Prop)
expression.
class C
{
public C(DataStore store)
{
this.store = store;
fieldof(this.Prop) = store.ReadPropFromDisk();
M(ref fieldof(this.Prop));
}
void Method()
{
// error: 'fieldof' can only be used during initialization (see also Alternatives)
fieldof(this.Prop) = "a";
}
private DataStore store;
public string Prop
{
get => field;
set
{
if (value != field)
{
field = value;
store.WritePropToDisk(value);
}
}
}
}
We are seeing prominent source generators such as MVVM Toolkit and ComputeSharp making heavy use of partial field-backed properties. Using a backing field for a partial property implementation comes with a number of experience improvements, including:
- avoiding the need for either generator or user to introduce an additional member with a distinct name (and in practice, often the generator needs to introduce it, which is bad for discoverability).
- avoiding the need for the generator to "wire up" the relationship between the field and property, in order to make it clear to user and compiler (e.g. for nullable constructor analysis).
- allowing user to put
[field: Attr]
on the definition part of the property, and have the attributes just go where they're supposed to, without any hacky workarounds from the generator itself. - allowing user to put a property initializer on the definition part, letting the user initialize the field during construction without invoking the setter logic.
Users are hitting limitations related to (4). Specifically, by only allowing the property initializer itself to assign the backing field, we are imposing an inconvenient limitation on what values are allowed to "bypass the setter" during construction:
class C1
{
// "stuff available in a static context" can be used:
public partial string Prop { get; set; } = ValueFactory.GetValue();
}
class C2(string prop)
{
// primary constructor parameters can be used:
public partial string Prop { get; set; } = prop;
}
public class C3
{
// but non-primary constructors must go through setter logic.
internal C3()
{
// even though users may have reasons that a primary constructor isn't suitable.
// e.g. in this case, even if we figure out how to make things work with a primary constructor,
// we may not want that constructor to have the same accessibility as the containing type.
var (first, second) = GetValues();
Prop1 = first;
Prop2 = second;
}
public partial string Prop1 { get; set; }
public partial string Prop2 { get; set; }
}
See also field-keyword.md#property-initializers. It's fairly easy to imagine the bool IsActive
example from that proposal, which motivated the property initializer behavior we have today, where the initial value doesn't simply come from a constant or a static, but needs to be passed in through a constructor.
class SomeViewModel
{
public SomeViewModel(bool isActive)
{
// without a way to assign the field directly,
// 'HasPendingChanges' is set to true, only when 'isActive' is true.
// But all we're trying to do is rehydrate state from a previous session/user setting/etc..
IsActive = isActive;
}
public bool HasPendingChanges { get; private set; }
public bool IsActive { get; set => Set(ref field, value); }
private bool Set<T>(ref T location, T value)
{
if (RuntimeHelpers.Equals(location, value))
return false;
location = value;
HasPendingChanges = true;
return true;
}
}
We believe that solutions involving explicitly declaring the backing field will significantly degrade the end user experience in source generator scenarios. Essentially, hand-rolled substitutes for the benefits outlined in Motivation are unlikely to be uniform and fully correct across various generators. Users would have to get oriented with different solutions across different generators for associating the field and property, locating the related declarations in user code and generated code, applying attributes independently to the field and property, and applying field initializers. See also Alternate generator patterns.
One purported benefit of the field
keyword feature is that it is only usable from within the property accessors. It may seem questionable that this proposal is to seemingly change that, and allow the field
to also be used in constructors.
However, this encapsulation has never been as complete as the above statement implies. Today, a type's constructors need to be concerned with which properties are field-backed, because it is directly related to nullable constructor analysis--forgetting to assign or check a field-backed property can result in a warning, while doing the same on a non-field-backed property will not.
class C
{
public string Prop1 { get => ValueStore.Get(); set => ValueStore.Set(value); }
public string Prop2 { get => field; set => field = value; }
// warning for Prop2, but not for Prop1
public C() { }
}
The fact that a property initializer (and by extension, a primary constructor) is permitted to "bypass" the setter logic is necessary and useful. We think that allowing such "bypass" to occur in ordinary constructors of the same type is useful for the same reasons. Because the capability remains limited to construction-time, we believe it preserves and reinforces the benefits of using the field
keyword.
The grammar is updated as follows:
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
(...)
| nameof_expression
+ | fieldof_expression
(...)
;
+fieldof_expression
+ : 'fieldof' '(' expression ')'
+ ;
A fieldof_expression
of the form fieldof(P)
is evaluated and classified as follows:
- If the containing member of the expression is not a constructor or
init
accessor, a compile-time error occurs. - If
P
is not classified as a property access of a field-backed property, a compile-time error occurs. - If
P
is classified as a property access of a field-backed property, thenfieldof(P)
is classified as a variable, specifically the backing field ofP
. - If
P
is static and the containing constructor is not static, or vice-versa, a compile-time error occurs. - If
P
is not declared in the containing type, a compile-time error occurs. (fieldof()
does not work with a property declared on a base type.) - Otherwise,
fieldof(P)
denotes the backing field ofP
.
A fieldof_expression
is subject to limitations on the receiver of its property access, similar to an assignment to a readonly
field. Specifically, the receiver must be the instance being initialized by the containing constructor, i.e. explicit or implicit this
. Otherwise, a compile-time error occurs.
The ref-safe-context (§9.7.2.4) for an expression of the form fieldof(e.P)
is determined as follows:
- If
e
is of a value type, then the ref-safe-context offieldof(e.P)
is the same as the ref-safe-context ofe
. - Otherwise, its ref-safe-context is caller-context.
The safe-context (§16.4.12.4) for an expression of the form fieldof(e.P)
is determined as follows:
- If
e.P
is of ref struct type, then the safe-context offieldof(e.P)
is the same as the safe-context ofe
. - Otherwise, its safe-context is caller-context.
The above ref safety rules are strongly analogous to the existing, linked rules which apply to ordinary field accesses.
This design makes no concession to preserving existing fieldof(P)
behavior when a symbol fieldof
is already in scope. This is a divergence from existing nameof
behavior, but aligns with the existing breaking change design of the field
keyword itself.
This proposal also reserves fieldof(P)
in expression contexts generally, rather than reserving it only in constructors and init
accessors. Existing code containing calls like fieldof(P)
would need to be changed to @fieldof(P)
in order to avoid breaks. We could consider instead limiting the break to only apply within constructors and init
accessors.
Depending on feedback, we could also adjust the design so that fieldof
works more like nameof
, and simply becomes unavailable when a symbol fieldof
is in scope.
To give a sense of relative risk of the break with field
, versus fieldof
, there are about ~86k results for field
in C# source and comments on GitHub, and about 5 of the same for fieldof
, at time of writing.
The motivating scenarios may not rise to the level of justifying a new contextual keyword or specialized syntax form in the language.
Except for encapsulation, there isn't a specific reason we need to limit use of fieldof()
to initialization. We could instead allow it anywhere in the same type if we wanted, which would effectively make field
itself just a shorthand for fieldof()
for the current property.
class C
{
string P { get => fieldof(P); set => field = value; }
void M0()
{
M1(ref fieldof(P));
}
void M1(ref string s) { }
}
The "encapsulation" behavior, as it currently exists in absence of fieldof()
, seems appealing, as prevents misuse of the field outside the policy of the associated property. However, since fieldof()
is always a more nested expression than a property access, it seems like users will tend to use the property anyway unless they have a specific reason for needing to use the field.
If we think that there are justified construction-specific cases for using the backing field directly, then perhaps there are also valid post-construction cases as well, that we may not know about yet, and it's not justified to put in a cliff, saying: sorry, only during initialization or in the accessors. Instead, we could simply see what the user is trying to do, and get out of their way.
At the same time, field
was thought to be a stepping stone toward a more general "property scoped fields" feature--where it seems much harder to justify accessing the fields outside of the property. The just get out of the user's way line of reasoning also seems to lead to letting people specify any accessibility for the backing field, which somehow seems a little too far.
Essentially, the field
feature is providing both encapsulation and association benefits. The question is whether to allow users to drop the encapsulation part, if they wish, and keep the association part.
We could consider alternative syntax for doing the same thing, such as an init prefix:
public C(string prop)
{
// 'init' prefix appearing before an assignment means assign a backing field.
init Prop = prop;
}
Arguably, fieldof(Prop)
is more clear than init Prop
. The latter is more of a "knowledge check" that a property initializer assigns the field without calling the setter, and that we are using init
to "simulate" such an initializer outside of the property declaration. While fieldof(Prop)
more directly states "we are using the field here".
Also, due to reusing an accessor keyword, it may be confusing to unfamiliar users:
class C
{
public string Prop { get; init { SideEffect(); field = value; } }
public C(string prop)
{
// wait.. putting 'init' here means "don't use the init accessor"?
init Prop = prop;
}
}
Generator authors could come up with a pattern where the field is explicitly declared by either generator or user, and associated to the property in a way that generator and compiler can understand (e.g., nullability attributes, naming conventions, and/or generator-specific attributes to associate members by name). Then, user can simply refer to the explicit field in a constructor.
We think this is a bad solution, because it requires generator authors to solve all the bullet points mentioned in Motivation, and necessarily results in a compromised end user experience.
We could advise users in this situation to introduce a flag which is set at the end of construction:
class C
{
private readonly bool _initialized;
public C(string prop)
{
Prop = prop;
_initialized = true;
}
public string Prop
{
get => field;
set
{
if (!_initialized)
{
field = value;
return;
}
if (value != field)
{
field = value;
OnPropertyChanged();
}
}
}
}
We think this is not a palatable solution compared to simply being able to set the field, due to increasing memory usage and complicating setter and constructor logic.
See Compat.