Słowo kluczowe „field
” we właściwościach
Streszczenie
Rozszerz wszystkie właściwości, aby umożliwić im odwołanie się do automatycznie wygenerowanego pola zapasowego przy użyciu nowego słowa kluczowego w kontekście field
. Właściwości mogą teraz również zawierać akcesoriów bez ciała obok akcesoriów z ciała.
Motywacja
Właściwości automatyczne zezwalają tylko na bezpośrednie ustawienie lub pobieranie pola zapasowego, dając kontrolę tylko przez umieszczenie modyfikatorów dostępu na akcesoriach. Czasami konieczne jest posiadanie dodatkowej kontroli nad tym, co dzieje się w jednym lub obu akcesorach, ale użytkownicy napotykają na trudność związaną z deklarowaniem pola pomocniczego. Nazwa pola wspierającego musi być zsynchronizowana z właściwością, a pole wspierające ma zakres dla całej klasy, co może spowodować możliwość przypadkowego ominięcia metod dostępowych wewnątrz klasy.
Istnieje kilka typowych scenariuszy. W getterze istnieje leniwa inicjalizacja lub domyślne wartości, jeśli właściwość nigdy nie została podana. W obrębie setter stosuje się ograniczenie, aby zapewnić poprawność wartości lub wykrywać i propagować aktualizacje, na przykład poprzez wywołanie zdarzenia INotifyPropertyChanged.PropertyChanged
.
W takich przypadkach zawsze musisz utworzyć pole wystąpienia i napisać całą właściwość samodzielnie. To nie tylko dodaje sporo kodu, ale także ujawnia pole pomocnicze do pozostałej części zakresu tego typu, gdy często pożądane jest, aby były dostępne tylko dla ciał akcesorów.
Glosariusz
Właściwość Auto: skrót od "automatycznie zaimplementowana właściwość" (§15.7.4). Akcesory we właściwości automatycznej nie mają treści. Zarówno implementacja, jak i magazyn zapasowy są dostarczane przez kompilator. Właściwości automatyczne mają
{ get; }
,{ get; set; }
lub{ get; init; }
.automatyczne akcesorium: skrót od "automatycznie zaimplementowane akcesorium". Jest to akcesorium, które nie ma ciała. Zarówno implementacja, jak i magazyn zapasowy są dostarczane przez kompilator.
get;
,set;
iinit;
są akcesorami automatycznymi.Pełne akcesorium: To jest akcesor, który ma treść. Implementacja nie jest dostarczana przez kompilator, chociaż magazyn zapasowy może nadal być (jak w przykładzie
set => field = value;
).właściwości opartej na polu: jest to właściwość używająca słowa kluczowego
field
w ciele akcesora lub jako automatyczna właściwość.pole zapasowe: jest to zmienna określona przez słowo kluczowe
field
w akcesorze właściwości, która jest również niejawnie odczytywana lub zapisywana w automatycznie implementowanych akcesorach (get;
,set;
lubinit;
).
Szczegółowy projekt
W przypadku właściwości z akcesorem init
, wszystkie elementy, które mają zastosowanie poniżej do set
, zamiast tego mają zastosowanie do akcesora init
.
Istnieją dwie zmiany składni:
Istnieje nowe kontekstowe słowo kluczowe,
field
, które może być używane w organach dostępu do właściwości w celu uzyskania dostępu do pola zapasowego dla deklaracji właściwości (decyzji LDM).Właściwości mogą teraz mieszać i dopasowywać automatyczne metody dostępu z pełnymi metodami dostępu (decyzja LDM). "Właściwość automatyczna" będzie nadal oznaczać właściwość, której metody dostępu nie mają ciał. Żadne z poniższych przykładów nie zostanie uznane za właściwości automatyczne.
Przykłady:
{ get; set => Set(ref field, value); }
{ get => field ?? parent.AmbientValue; set; }
Obie metody dostępu mogą być pełnymi akcesorami, z tym że jeden lub oba mogą korzystać z field
.
{ get => field; set => field = value; }
{ get => field; set => throw new InvalidOperationException(); }
{ get => overriddenValue; set => field = value; }
{
get;
set
{
if (field == value) return;
field = value;
OnXyzChanged();
}
}
Właściwości z wyrażeniami i te z tylko akcesorem get
mogą również używać field
:
public string LazilyComputed => field ??= Compute();
public string LazilyComputed { get => field ??= Compute(); }
Właściwości tylko do ustawiania mogą także używać field
:
{
set
{
if (field == value) return;
field = value;
OnXyzChanged(new XyzEventArgs(value));
}
}
Zmiany powodujące niezgodność
Istnienie kontekstowego słowa kluczowego field
w ciałach akcesorów właściwości jest potencjalnie przełomową zmianą, proponowaną w ramach większej funkcji Zmiany wpływające na zgodność.
Ponieważ field
jest słowem kluczowym, a nie identyfikatorem, może być "przysłonięte" tylko za pomocą identyfikatora przy użyciu normalnej metody ucieczki słowa kluczowego: @field
. Wszystkie identyfikatory o nazwie field
zadeklarowane w jednostkach dostępu właściwości mogą chronić przed przerwami podczas uaktualniania z wersji języka C# przed 13 przez dodanie początkowej @
.
Atrybuty ukierunkowane na pola
Podobnie jak we właściwościach automatycznych, każda właściwość używająca pola zapasowego w jednym z jego metod dostępu będzie mogła używać atrybutów docelowych pól:
[field: Xyz]
public string Name => field ??= Compute();
[field: Xyz]
public string Name { get => field; set => field = value; }
Atrybut ukierunkowany na pole pozostanie nieprawidłowy, chyba że akcesor używa pola wspierającego.
// ❌ Error, will not compile
[field: Xyz]
public string Name => Compute();
Inicjatory właściwości
Właściwości z inicjatorami mogą używać field
. Pole pomocnicze jest inicjalizowane bezpośrednio zamiast wywołania metody ustawiającej (decyzja LDM).
Wywoływanie settera dla inicjatora nie jest opcją; inicjatory są przetwarzane przed wywołaniem konstruktorów podstawowych i nielegalne jest wywołanie dowolnej metody instancji przed wywołaniem konstruktora podstawowego. Jest to również ważne w przypadku domyślnego inicjowania/określonego przypisania struktur.
Daje to elastyczną kontrolę nad inicjowaniem. Jeśli chcesz zainicjować bez wywoływania inicjatora, użyj inicjatora właściwości. Jeśli chcesz zainicjować, wywołując metodę ustawiającą, przypisz właściwości wartość początkową w konstruktorze.
Oto przykład tego, gdzie jest to przydatne. Uważamy, że słowo kluczowe field
znajdzie szerokie zastosowanie w modelach widoku z powodu eleganckiego rozwiązania, jakie oferuje dla wzorca INotifyPropertyChanged
. Ustawienia właściwości modelu widoku prawdopodobnie są powiązane z danymi w interfejsie użytkownika i mogą powodować śledzenie zmian lub wyzwalać inne zachowania. Poniższy kod musi zainicjować wartość domyślną IsActive
bez ustawiania HasPendingChanges
na true
:
class SomeViewModel
{
public bool HasPendingChanges { get; private set; }
public bool IsActive { get; set => Set(ref field, value); } = true;
private bool Set<T>(ref T location, T value)
{
if (RuntimeHelpers.Equals(location, value))
return false;
location = value;
HasPendingChanges = true;
return true;
}
}
Różnica w zachowaniu między inicjalizatorem właściwości a przypisaniem w konstruktorze jest również widoczna w przypadku wirtualnych właściwości automatycznych we wcześniejszych wersjach języka.
using System;
// Nothing is printed; the property initializer is not
// equivalent to `this.IsActive = true`.
_ = new Derived();
class Base
{
public virtual bool IsActive { get; set; } = true;
}
class Derived : Base
{
public override bool IsActive
{
get => base.IsActive;
set
{
base.IsActive = value;
Console.WriteLine("This will not be reached");
}
}
}
Przypisanie konstruktora
Podobnie jak w przypadku właściwości automatycznych, przypisanie w konstruktorze wywołuje (potencjalnie wirtualny) setter, jeśli istnieje, a jeśli go brakuje, przechodzi do bezpośredniego przypisywania do pola pomocniczego.
class C
{
public C()
{
P1 = 1; // Assigns P1's backing field directly
P2 = 2; // Assigns P2's backing field directly
P3 = 3; // Calls P3's setter
P4 = 4; // Calls P4's setter
}
public int P1 => field;
public int P2 { get => field; }
public int P4 { get => field; set => field = value; }
public int P3 { get => field; set; }
}
Określone przypisanie w strukturach
Mimo że nie można się do nich odwoływać w konstruktorze, pola wspierające oznaczone słowem kluczowym field
podlegają domyślnej inicjalizacji i domyślnym ostrzeżeniom o wyłączeniu w tych samych warunkach, co wszystkie inne pola struktury (decyzja LDM 1, decyzja LDM 2).
Na przykład (te diagnostyki są domyślnie dyskretne):
public struct S
{
public S()
{
// CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
// assignments of 'default' to non-explicitly assigned fields.
_ = P1;
}
public int P1 { get => field; }
}
public struct S
{
public S()
{
// CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit
// assignments of 'default' to non-explicitly assigned fields.
P2 = 5;
}
public int P2 { get => field; set => field = value; }
}
Właściwości zwracające referencje
Podobnie jak we właściwościach automatycznych, słowo kluczowe field
nie będzie dostępne do użycia we właściwościach zwracanych przez odwołanie. Właściwości zwracane przez ref nie mogą mieć ustawionych metod dostępu, a bez zestawu akcesoriów, metodę get i inicjator właściwości byłyby jedynymi elementami, które mogą uzyskać dostęp do pola zapasowego. W przypadku braku przypadków użycia nie jest teraz czas na ponowne zwracanie właściwości, aby można było zapisywać je jako właściwości automatyczne.
Możliwość przypisania wartości null
Jedną z zasad funkcji Typy odwołań dopuszczających wartość null jest zrozumienie istniejących idiomatycznych wzorców kodowania w języku C# i wymaganie możliwie najmniejszej ilości formalności związanej z tymi wzorcami. Propozycja słowa kluczowego field
umożliwia proste, idiomatyczne wzorce do obsługi często poszukiwanych scenariuszy, takich jak właściwości inicjowane leniwie. Ważne jest, aby typy odwołań dopuszczane do wartości null dobrze współpracowały z tymi nowymi wzorcami kodowania.
Cele:
Należy zapewnić rozsądny poziom bezpieczeństwa wartości null dla różnych wzorców użycia funkcji słowa kluczowego
field
.Wzorce używające słowa kluczowego
field
powinny czuć się tak, jakby zawsze były częścią języka. Unikaj utrudniania użytkownikowi włączania typów referencyjnych dopuszczających null w kodzie, który jest całkowicie idiomatyczny dla funkcji słowa kluczowegofield
.
Jednym z kluczowych scenariuszy są właściwości inicjowane z opóźnieniem:
public class C
{
public C() { } // It would be undesirable to warn about 'Prop' being uninitialized here
string Prop => field ??= GetPropValue();
}
Następujące zasady dotyczące nullowalności będą stosowane nie tylko do właściwości używających słowa kluczowego field
, ale także do automatycznych właściwości.
Nullability pola zapasowego
Aby uzyskać definicje nowych terminów, zobacz Słownik.
Pole pomocnicze ma taki sam typ jak właściwość. Jednak jego adnotacja opcjonalności może różnić się od tej właściwości. Aby określić tę adnotację akceptującą wartości null, wprowadzamy koncepcję odporności na brak wartości null .
odporność na null oznacza intuicyjnie, że get
dostęp do właściwości zachowuje odporność na null, nawet gdy pole zawiera wartość default
dla swojego typu.
właściwości opartej na
- Dla celów tej analizy zakłada się, że
field
jest tymczasowo oznaczony jako posiadający adnotację zanulowania, np.string?
. Powoduje to, żefield
może mieć wartość null lub może być z domyślnym stanem początkowym w akcesorzeget
, w zależności od jego typu. - Następnie, jeśli analiza getterów dopuszczających wartość null nie zwraca żadnych ostrzeżeń o wartościach null, właściwość jest odporna na wartości null. W przeciwnym razie nie jest to odporne na null.
- Jeśli właściwość nie ma metody uzyskiwania dostępu, jest ona (pusto) odporna na wartości null.
- Jeśli akcesor pobierający jest implementowany automatycznie, właściwość nie jest odporna na wartość null.
Wartość null pola kopii zapasowej jest określana w następujący sposób:
- Jeśli pole ma atrybuty dopuszczania wartości null, takie jak
[field: MaybeNull]
,AllowNull
,NotNull
lubDisallowNull
, adnotacja dopuszczana do wartości null pola jest taka sama jak adnotacja dopuszczana do wartości null właściwości.- Dzieje się tak dlatego, że gdy użytkownik zacznie stosować atrybuty dopuszczalności null do pola, nie chcemy już wywnioskować niczego, chcemy, aby wartość null miała być tym, co użytkownik powiedział.
- Jeśli właściwość zawierająca ma nieświadomą lub adnotowaną nullowalność, to pole zapasowe ma taką samą nullowalność jak właściwość.
- Jeśli właściwość zawierająca ma nienotowane wartości null (np.
string
lubT
) lub ma atrybut[NotNull]
, a właściwość jest odporne na wartości null, pole zapasowe ma adnotację null. - Jeśli właściwość zawierająca ma nienotowane wartości null (np.
string
lubT
) lub ma atrybut[NotNull]
, a właściwość nie jest odporna na wartości null, pole zapasowe nie ma adnotacji null.
Analiza konstruktora
Obecnie właściwość automatyczna jest traktowana bardzo podobnie do zwykłego pola w analizie dopuszczalności wartości null konstruktora. Rozszerzamy to podejście na właściwości oparte na polach , traktując każdą właściwość oparte na polu jako proxy do jej pola bazowego.
Aktualizujemy następujący język specyfikacji z poprzedniego proponowanego podejścia, aby to osiągnąć:
Przy każdym jawnym lub niejawny "powrocie" w konstruktorze wystawiamy ostrzeżenie dla każdego członka, którego stan przepływu jest niezgodny z jego adnotacjami i atrybutami dotyczącymi dopuszczalności wartości null. Jeśli element członkowski jest właściwością opartą na polu, do tego sprawdzenia jest używana adnotacja nullable pola zapasowego. W przeciwnym razie jest używana adnotacja dopuszczająca wartość null samego elementu członkowskiego. Rozsądne przybliżenie tej zasady jest następujące: jeśli przypisanie elementu członkowskiego do siebie w punkcie zwrotnym wywoła ostrzeżenie dotyczące możliwości przyjmowania wartości null, to takie ostrzeżenie zostanie wygenerowane w punkcie zwrotnym.
Należy pamiętać, że jest to istotnie ograniczona analiza międzyproceduralna. Przewidujemy, że w celu przeanalizowania konstruktora konieczne będzie przeprowadzenie analizy powiązania i "odporności null" dla wszystkich odpowiednich metod dostępu get w tym samym typie, które używają słowa kluczowego kontekstowego field
i mają nienotowane wartości null. Spekulujemy, że nie jest to zbyt kosztowne, ponieważ ciała getter zwykle nie są bardzo złożone i że analiza "odporności null" musi być wykonywana tylko raz, niezależnie od liczby konstruktorów w typie.
Analiza ustawiająca
Dla uproszczenia używamy terminów "setter" i "set accessor", aby odwoływać się do set
lub init
akcesorium.
Należy sprawdzić, czy settery właściwości opartych na polach faktycznie inicjalizują pole wspierające.
class C
{
string Prop
{
get => field;
// getter is not null-resilient, so `field` is not-annotated.
// We should warn here that `field` may be null when exiting.
set { }
}
public C()
{
Prop = "a"; // ok
}
public static void Main()
{
new C().Prop.ToString(); // NRE at runtime
}
}
Początkowy stan przepływu pola zapasowego w zestawie właściwości opartej na jest określany w następujący sposób:
- Jeśli właściwość ma inicjalizator, stan początkowy przepływu jest taki sam jak stan przepływu właściwości po użyciu inicjalizatora.
- W przeciwnym razie początkowy stan przepływu jest taki sam jak stan przepływu podany przez
field = default;
.
W przypadku każdego jawnego lub niejawnego "return" w setterze zgłaszane jest ostrzeżenie, jeśli stan przepływu pola zapasowego jest niezgodny z jego adnotacjami i atrybutami dotyczącymi dopuszczalności wartości null.
Uwagi
Ta formulacja celowo przypomina zwykłe pola w konstruktorach. Zasadniczo, ponieważ tylko metody dostępu właściwości mogą rzeczywiście odwoływać się do pola zapasowego, setter jest traktowany jako "mini-konstruktor" dla pola zapasowego.
Podobnie jak w przypadku zwykłych pól, zazwyczaj wiemy, że właściwość została zainicjowana w konstruktorze, ponieważ ją ustawiono, choć nie zawsze. Po prostu powrócenie w obrębie gałęzi, w której Prop != null
było prawdziwe, jest również wystarczające dla naszej analizy konstruktora, ponieważ rozumiemy, że mechanizmy, których nie śledzimy, mogły zostać użyte do ustawienia właściwości.
Rozważano alternatywy; zobacz sekcję Alternatywy dotyczące nullowalności.
nameof
W miejscach, w których field
jest słowem kluczowym, nameof(field)
nie będzie się kompilować (decyzja LDM), na przykład nameof(nint)
. Nie jest to takie jak nameof(value)
, które należy używać, gdy metody ustawiające właściwości zgłaszają wyjątek ArgumentException, jak to robią niektóre z bibliotek platformy .NET Core. Natomiast nameof(field)
nie ma oczekiwanych przypadków użycia.
Zastępuje
Zastępowanie właściwości może używać field
. Takie użycia field
odwołują się do pola zapasowego dla właściwości nadpisującej, niezależnie od pola zapasowego właściwości podstawowej, jeśli takie pole istnieje. Nie ma interfejsu ABI dla uwidaczniania pola pomocniczego właściwości bazowej w klasach nadpisujących, ponieważ spowodowałoby to przerwanie hermetyzacji.
Podobnie jak w przypadku właściwości automatycznych, właściwości, które używają słowa kluczowego field
i przesłaniają właściwość bazową, muszą przesłonić wszystkie akcesory (decyzja LDM).
Zrzuty ekranowe
field
powinno być w stanie być przechwytywane w funkcjach lokalnych i lambdach, a odwołania do field
z funkcji lokalnych i lambd są dozwolone, nawet jeśli nie ma innych odwołań (decyzja LDM 1, decyzja LDM 2):
public class C
{
public static int P
{
get
{
Func<int> f = static () => field;
return f();
}
}
}
Ostrzeżenia dotyczące użycia pól
Gdy słowo kluczowe field
jest używane w akcesorze, istniejąca analiza kompilatora dotycząca nieprzypisanych lub nieprzeczytanych pól będzie obejmować to pole.
- CS0414: Pole zapasowe właściwości "Xyz" jest przypisane, ale jego wartość nigdy nie jest używana
- CS0649: Pole zapasowe właściwości "Xyz" nigdy nie jest przypisane i zawsze będzie miało wartość domyślną
Zmiany specyfikacji
Składnia
Podczas kompilowania z wersją języka 13 lub nowszą, field
jest uznawane za słowo kluczowe, gdy jest używane jako wyrażenie podstawowe (decyzja LDM) w następujących lokalizacjach (decyzja LDM):
- W ciałach metod
get
,set
iinit
akcesorów we właściwościach , ale nie indeksatorach - W atrybutach zastosowanych do tych akcesorów
- W zagnieżdżonych wyrażeniach lambda i funkcjach lokalnych oraz w wyrażeniach LINQ w tych akcesorach
We wszystkich innych przypadkach, w tym podczas kompilowania z językiem w wersji 12 lub starszej, field
jest uważany za identyfikator.
primary_no_array_creation_expression
: literal
+ | 'field'
| interpolated_string_expression
| ...
;
Właściwości
Właściwości §15.7.1— Ogólne
property_initializer można podać tylko dla
automatycznie zaimplementowanej właściwości, awłaściwość, która ma pole zapasowe, które będzie emitowane. property_initializer powoduje zainicjowanie pola bazowego takich właściwości z wartością podaną przez wyrażenie .
§15.7.4Automatycznie implementowane właściwości
Automatycznie zaimplementowana właściwość (lub właściwość automatyczna w skrócie) jest nie abstrakcyjną, nie-extern, niekorzystaną z właściwością niekorzystaną z
ciałami pomocniczymi tylko średnikami. Właściwości automatyczne mają dostęp do metody dostępu i mogą opcjonalnie mieć zestaw akcesoriów.jednej lub obu następujących
- akcesor z ciałem zawierającym tylko średnik
- użycie kontekstowego słowa kluczowego
field
w akcesorach lub treści wyrażeniawłaściwościGdy właściwość jest określona jako automatycznie zaimplementowana właściwość, ukryte bez nazwy pole zapasowe jest automatycznie dostępne dla właściwości
, a metody dostępu są implementowane do odczytu i zapisu do tego pola zapasowego. W przypadku właściwości automatycznych wszystkie akcesory wyłącznie ze średnikamiget
są implementowane do odczytu z, a akcesory wyłącznie ze średnikamiset
do zapisu w polu pomocniczym.
Ukryte pole zapasowe jest niedostępne, można je odczytywać i zapisywać tylko za pośrednictwem automatycznie zaimplementowanych akcesorów właściwości, nawet w obrębie typu zawierającego.Pole zapasowe można odwołać się bezpośrednio przy użyciu słowa kluczowegofield
we wszystkich akcesorach i w ciału wyrażenia właściwości. Ponieważ pole nie jest nazwane, nie można go używać w wyrażeniunameof
.Jeśli właściwość automatyczna nie ma
ustawić metody dostęputylko średnik uzyskaćdostępu, pole zapasowe jest uznawane zareadonly
(§15.5.3). Podobnie jak w przypadku polareadonly
, właściwość automatyczna tylko do odczytu (bez akcesora set lub akcesora init) może być również przypisana w treści konstruktora otaczającej klasy. Takie przypisanie dokonuje się bezpośrednio wtylko do odczytupolu pomocniczym właściwości.Właściwość automatyczna nie może mieć tylko jednego
set
set
akcesorium bezget
akcesorium.Automatyczna właściwość może opcjonalnie mieć property_initializer, która jest stosowana bezpośrednio do pola wspierającego jako variable_initializer (§17.7).
Poniższy przykład:
// No 'field' symbol in scope.
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
jest odpowiednikiem następującej deklaracji:
// No 'field' symbol in scope.
public class Point
{
public int X { get { return field; } set { field = value; } }
public int Y { get { return field; } set { field = value; } }
}
co jest równoważne:
// No 'field' symbol in scope.
public class Point
{
private int __x;
private int __y;
public int X { get { return __x; } set { __x = value; } }
public int Y { get { return __y; } set { __y = value; } }
}
Poniższy przykład:
// No 'field' symbol in scope.
public class LazyInit
{
public string Value => field ??= ComputeValue();
private static string ComputeValue() { /*...*/ }
}
jest odpowiednikiem następującej deklaracji:
// No 'field' symbol in scope.
public class Point
{
private string __value;
public string Value { get { return __value ??= ComputeValue(); } }
private static string ComputeValue() { /*...*/ }
}
Alternatywy
Alternatywy dotyczące wartości null
Oprócz podejścia odporności na null
Nic nie rób
Nie moglibyśmy w ogóle wprowadzać żadnych specjalnych zachowań. W efekcie:
- Traktuj właściwość wspieraną przez pole w taki sam sposób, w jaki właściwości automatyczne są traktowane dzisiaj — właściwość musi być zainicjowana w konstruktorze, chyba że oznaczono jako wymagana itp.
- Nie ma specjalnego traktowania zmiennej pola podczas analizowania metod dostępu do właściwości. Jest to po prostu zmienna o tym samym typie i wartości null co właściwość.
Należy pamiętać, że spowodowałoby to uciążliwe ostrzeżenia dotyczące scenariuszy "leniwej właściwości", w takim przypadku użytkownicy prawdopodobnie będą musieli przypisać null!
lub podobne, aby uciszyć ostrzeżenia konstruktora.
Jako "alternatywne podejście" można rozważyć również całkowite ignorowanie właściwości przy użyciu słowa kluczowego field
do analizy konstruktorów obsługujących wartości nullable. W takim przypadku nie będzie żadnych ostrzeżeń informujących użytkownika o konieczności zainicjowania czegokolwiek, ale również brak uciążliwości dla użytkownika, niezależnie od tego, jakiego wzorca inicjowania mogą używać.
Ponieważ planujemy jedynie wprowadzenie funkcji słowa kluczowego field
w wersji zapoznawczej LangVersion na platformie .NET 9, spodziewamy się możliwości zmiany zachowania nullowalnego tej funkcji w .NET 10. Dlatego możemy rozważyć przyjęcie rozwiązania "niższego kosztu", takiego jak ten w krótkim okresie, i dorastanie do jednego z bardziej złożonych rozwiązań w dłuższej perspektywie.
field
- docelowe atrybuty dopuszczania wartości null
Moglibyśmy wprowadzić następujące wartości domyślne, osiągając rozsądny poziom bezpieczeństwa wartości null, bez konieczności wykonywania jakiejkolwiek analizy międzyproceduralnej.
- Zmienna
field
zawsze ma tę samą adnotację dopuszczającą wartość null co właściwość. - Atrybuty zerowalności
[field: MaybeNull, AllowNull]
itp. mogą służyć do dostosowywania zerowalności pola pomocniczego. - Właściwości oparte na polach są sprawdzane pod kątem inicjowania w konstruktorach na podstawie adnotacji i atrybutów pola dopuszczanych do wartości null.
- metody ustawiania we właściwościach opartych na polu sprawdzają inicjowanie
field
podobnie jak konstruktory.
Oznaczałoby to, że "little-l leniwy scenariusz" wyglądałby następująco:
class C
{
public C() { } // no need to warn about initializing C.Prop, as the backing field is marked nullable using attributes.
[field: AllowNull, MaybeNull]
public string Prop => field ??= GetPropValue();
}
Jednym z powodów, dla których unikaliśmy używania atrybutów nullowalności, jest to, że te, które mamy, są naprawdę skupione na opisywaniu wejść i wyjść sygnatur. Są one kłopotliwe do użycia do opisu podatności na wartość null zmiennych długożyjących.
- W praktyce
[field: MaybeNull, AllowNull]
jest wymagana, aby pole zachowywało się "rozsądnie" jako zmienna dopuszczana do wartości null, co daje stan przepływu początkowego o wartościach null i umożliwia zapisanie do niego możliwych wartości null. Wydaje się to uciążliwe, aby wymagać od użytkowników wykonywania czynności w stosunkowo typowych, drobnych scenariuszach lenistwa. - Jeśli będziemy dążyć do tego podejścia, rozważmy dodanie ostrzeżenia w przypadku użycia
[field: AllowNull]
, co sugeruje również dodanieMaybeNull
. Dzieje się tak, ponieważ samo AllowNull nie robi tego, czego użytkownicy potrzebują od zmiennej dopuszczającej wartość null: zakłada, że pole początkowo nie ma wartości null, kiedy jeszcze nie widzieliśmy, żeby coś było w niej zapisywane. - Możemy również rozważyć dostosowanie zachowania
[field: MaybeNull]
w odniesieniu do słowa kluczowegofield
, a nawet ogólnie pól, aby umożliwić zapisanie wartości null w zmiennej, tak jakbyAllowNull
były niejawnie obecne.
Odpowiedzi na pytania dotyczące LDM
Lokalizacje składni słów kluczowych
W metodach dostępu, w których field
i value
mogą wiązać się z syntetyzowanym polem pomocniczym lub niejawnym parametrem ustawiającym, w których lokalizacjach składni identyfikatory powinny być traktowane jako słowa kluczowe?
- zawsze
- tylko wyrażenia podstawowe
- nigdy
Pierwsze dwa przypadki to zmiany powodujące niezgodność.
Jeśli identyfikatory są zawsze uznawane za słowa kluczowe, jest to znacząca zmiana w przypadku następujących, na przykład:
class MyClass
{
private int field;
public int P => this.field; // error: expected identifier
private int value;
public int Q
{
set { this.value = value; } // error: expected identifier
}
}
Jeśli identyfikatory są słowami kluczowymi, gdy są używane jako wyrażenia podstawowe tylko, zmiana powodująca niezgodność jest mniejsza. Najczęstszym błędem może być niekwalifikowane użycie istniejącego elementu o nazwie field
.
class MyClass
{
private int field;
public int P => field; // binds to synthesized backing field rather than 'this.field'
}
Istnieje również przerwa, gdy field
lub value
jest ponownie zadeklarowana w zagnieżdżonej funkcji. Może to być jedyna przerwa dla value
w przypadku wyrażeń podstawowych.
class MyClass
{
private IEnumerable<string> _fields;
public bool HasNotNullField
{
get => _fields.Any(field => field is { }); // 'field' binds to synthesized backing field
}
public IEnumerable<string> Fields
{
get { return _fields; }
set { _fields = value.Where(value => Filter(value)); } // 'value' binds to setter parameter
}
}
Jeśli identyfikatory są nigdy nie uznane za słowa kluczowe, identyfikatory będą wiązać się tylko z syntetyzowanym polem kopii zapasowej lub niejawnym parametrem, gdy identyfikatory nie są powiązane z innymi elementami członkowskimi. W tym przypadku nie ma żadnych zmian powodujących niezgodność.
Odpowiedź
field
jest słowem kluczowym w odpowiednich akcesorach używanych tylko jako wyrażenie podstawowe ; value
nigdy nie jest uważany za słowo kluczowe.
Scenariusze podobne do { set; }
{ set; }
jest obecnie niedozwolona i ma to sens: pole, które tworzy, nigdy nie może być odczytywane. Istnieją teraz nowe sposoby, aby znaleźć się w sytuacji, w której setter wprowadza pole pomocnicze, które nigdy nie jest odczytywane, na przykład rozszerzenie { set; }
do { set => field = value; }
.
Które z tych scenariuszy powinny być dozwolone do skompilowania? Załóżmy, że ostrzeżenie "pole nigdy nie jest odczytywane", będzie miało zastosowanie tak samo jak w przypadku ręcznie zadeklarowanego pola.
-
{ set; }
- Niedozwolone dzisiaj, kontynuuj zakazywanie { set => field = value; }
{ get => unrelated; set => field = value; }
{ get => unrelated; set; }
-
{ set { if (field == value) return; field = value; SendEvent(nameof(Prop), value); } }
-
{ get => unrelated; set { if (field == value) return; field = value; SendEvent(nameof(Prop), value); } }
Odpowiedź
Nie zezwalaj tylko na to, co jest już niedozwolone dzisiaj we właściwościach automatycznych, bez ciała set;
.
field
w akcesorze zdarzeń
Czy field
powinno być słowem kluczowym w akcesorze zdarzenia i czy kompilator powinien wygenerować pole zapasowe?
class MyClass
{
public event EventHandler E
{
add { field += value; }
remove { field -= value; }
}
}
zalecenie: field
nie jest słowa kluczowego w metodzie dostępu do zdarzeń i nie jest generowane żadne pole zapasowe.
Odpowiedź
Podjęte zalecenie.
field
nie jest ani słowem kluczowym w akcesorze zdarzenia, przez co nie jest generowane żadne pole zapasowe.
Nullość field
Czy proponowana nullowalność field
powinna zostać zaakceptowana? Zapoznaj się z sekcją Nullability i otwartym tam pytaniem.
Odpowiedź
Przyjęto wniosek ogólny. Konkretne zachowanie nadal wymaga więcej przeglądu.
field
w inicjatorze właściwości
Czy field
powinno być słowem kluczowym w inicjatorze właściwości i czy powinno powiązać się z polem zapasowym?
class A
{
const int field = -1;
object P1 { get; } = field; // bind to const (ok) or backing field (error)?
}
Czy istnieją przydatne scenariusze odwoływania się do pola tworzenia kopii zapasowej w inicjatorze?
class B
{
object P2 { get; } = (field = 2); // error: initializer cannot reference instance member
static object P3 { get; } = (field = 3); // ok, but useful?
}
W powyższym przykładzie powiązanie z polem pomocniczym powinno spowodować błąd: „Inicjator nie może odwoływać się do pola niestatycznego”.
Odpowiedź
Zwiążemy inicjalizator tak jak w poprzednich wersjach języka C#. Nie umieścimy pola zapasowego w zakresie ani nie będziemy uniemożliwiać odwoływania się do innych członków o nazwie field
.
Interakcja z właściwościami częściowymi
Inicjatory
Kiedy właściwość częściowa używa field
, które części powinny mieć inicjalizator?
partial class C
{
public partial int Prop { get; set; } = 1;
public partial int Prop { get => field; set => field = value; } = 2;
}
- Wydaje się jasne, że powinien wystąpić błąd, gdy obie części mają inicjalizator.
- Możemy myśleć o przypadkach użycia, w których część definicji lub implementacji może chcieć ustawić początkową wartość
field
. - Wydaje się, że jeśli zezwalamy inicjatorowi na część definicji, skutecznie zmusza implementator do używania
field
, aby program był prawidłowy. Czy to dobrze? - Uważamy, że generatory będą powszechnie stosować
field
, kiedy w implementacji potrzebne jest pole zapasowe tego samego typu. Jest to częściowo spowodowane tym, że generatory często chcą umożliwić użytkownikom używanie[field: ...]
atrybutów docelowych w części definicji właściwości. Użycie słowa kluczowegofield
oszczędza implementatorowi generatora kłopotów z "przekazywaniem" takich atrybutów do niektórego wygenerowanego pola oraz zignorowanie ostrzeżeń dotyczących właściwości. Te same generatory mogą również zezwolić użytkownikowi na określenie wartości początkowej pola.
Zalecenie: Zezwól na inicjator w dowolnej części właściwości częściowej, gdy część implementacji używa field
. Zgłoś błąd, jeśli obie części mają inicjalizator.
Odpowiedź
Zalecenie zaakceptowane. Deklarowanie lub implementowanie lokalizacji właściwości może używać inicjatora, ale nie jednocześnie.
Automatyczne akcesory
Zgodnie z pierwotnym projektem częściowa implementacja właściwości musi mieć ciała dla wszystkich akcesoriów. Jednak ostatnie iteracji funkcji słowa kluczowego field
obejmowały pojęcie "automatycznych metod dostępu". Czy częściowe implementacje właściwości powinny być w stanie używać takich akcesorów? Jeśli są one używane wyłącznie, będzie to nie do odróżnienia od deklaracji definiującej.
partial class C
{
public partial int Prop0 { get; set; }
public partial int Prop0 { get => field; set => field = value; } // this is equivalent to the two "semi-auto" forms below.
public partial int Prop1 { get; set; }
public partial int Prop1 { get => field; set; } // is this a valid implementation part?
public partial int Prop2 { get; set; }
public partial int Prop2 { get; set => field = value; } // what about this? will there be disagreement about which is the "best" style?
public partial int Prop3 { get; }
public partial int Prop3 { get => field; } // it will only be valid to use at most 1 auto-accessor, when a second accessor is manually implemented.
Zalecenie: Nie zezwalaj na automatyczne akcesory w implementacjach właściwości częściowych, ponieważ ograniczenia dotyczące tego, kiedy mogą być używane, są bardziej mylące niż korzyści wynikające z zezwolenia na nie.
Odpowiedź
Przynajmniej jeden akcesor musi zostać zaimplementowany ręcznie, ale drugi akcesor można zaimplementować automatycznie.
Pole tylko do odczytu
Kiedy należy uznać syntetyzowane pole zapasowe za tylko do odczytu?
struct S
{
readonly object P0 { get => field; } = ""; // ok
object P1 { get => field ??= ""; } // ok
readonly object P2 { get => field ??= ""; } // error: 'field' is readonly
readonly object P3 { get; set { _ = field; } } // ok
readonly object P4 { get; set { field = value; } } // error: 'field' is readonly
}
Gdy pole kopii zapasowej jest uznawane za tylko do odczytu, pole emitowane do metadanych jest oznaczone initonly
i zgłaszany jest błąd, jeśli field
jest modyfikowany inny niż w inicjatorze lub konstruktorze.
zalecenie: zsyntetyzowane pole kopii zapasowej jest tylko do odczytu, gdy typ zawierający jest struct
, a właściwość lub typ zawierający jest zadeklarowany readonly
.
Odpowiedź
Zalecenie jest akceptowane.
Kontekst tylko do odczytu i set
Czy dostęp set
powinien być dozwolony w kontekście readonly
dla właściwości, która używa field
?
readonly struct S1
{
readonly object _p1;
object P1 { get => _p1; set { } } // ok
object P2 { get; set; } // error: auto-prop in readonly struct must be readonly
object P3 { get => field; set { } } // ok?
}
struct S2
{
readonly object _p1;
readonly object P1 { get => _p1; set { } } // ok
readonly object P2 { get; set; } // error: auto-prop with set marked readonly
readonly object P3 { get => field; set { } } // ok?
}
Odpowiedź
Mogą wystąpić scenariusze, w których wdrażasz akcesor set
w strukturze readonly
i albo przekazujesz ją dalej, albo rzucasz wyjątek. Pozwolimy na to.
kod [Conditional]
Czy pole syntetyzowane powinno być generowane, gdy field
jest używane tylko w pominiętych wywołaniach do metod warunkowych?
Na przykład czy pole kopii zapasowej powinno być generowane dla następujących elementów w kompilacji innej niż DEBUG?
class C
{
object P
{
get
{
Debug.Assert(field is null);
return null;
}
}
}
Dla odniesienia, pola dla głównych parametrów konstruktora są generowane w podobnych przypadkach — zobacz sharplab.io.
Zalecenie: pole pomocnicze jest generowane, gdy field
jest używane tylko w pominiętych wywołaniach metod warunkowych .
Odpowiedź
Conditional
kod może mieć wpływ na kod niewarunkowy, na przykład Debug.Assert
zmianę anulowalności. Byłoby dziwne, gdyby field
nie miał podobnych skutków. Jest również mało prawdopodobne, aby pojawiło się w większości kodu, więc zrobimy rzecz prostą i zaakceptujemy zalecenie.
Właściwości interfejsu i automatyczne akcesory
Czy kombinacja metod dostępu implementowanych ręcznie i automatycznie jest rozpoznana dla właściwości interface
, w której automatycznie zaimplementowana metoda dostępu odnosi się do syntetyzowanego pola zapasowego?
W przypadku właściwości wystąpienia zostanie zgłoszony błąd, że pola wystąpienia nie są obsługiwane.
interface I
{
object P1 { get; set; } // ok: not an implementation
object P2 { get => field; set { field = value; }} // error: instance field
object P3 { get; set { } } // error: instance field
static object P4 { get; set { } } // ok: equivalent to { get => field; set { } }
}
Zalecenie: Automatyczne akcesory są rozpoznawane we właściwościach interface
i odnoszą się do zsyntetyzowanego pola pomocniczego. W przypadku właściwości instancji zgłaszany jest błąd mówiący, że pola instancji nie są obsługiwane.
Odpowiedź
Standaryzacja wokół pola instancji będącego przyczyną błędu jest zgodna z częściowymi właściwościami klas, co nam odpowiada. Zalecenie jest akceptowane.
C# feature specifications