Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Słowo kluczowe „
Uwaga / Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Kwestia dotycząca mistrza: https://github.com/dotnet/csharplang/issues/8635
Podsumowanie
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ć akcesory bez ciała obok akcesorów z ciałem.
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 od teraz zawsze trzeba utworzyć pole instancji i samodzielnie napisać całą właściwość. 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.
Słownik
Właściwość Automatyczna: 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: Jest to akcesorium, które ma ciało. Implementacja nie jest zapewniana przez kompilator, chociaż pamięć pomocnicza może nadal być dostępna (jak w przykładzie
set => field = value;).Właściwość wspierana polem: jest to właściwość wykorzystująca
fieldsłowo kluczowe w ciele akcesora lub właściwość automatyczna.Pole zapasowe: Jest to zmienna oznaczona przez
fieldsłowo kluczowe 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 ciałach akcesorów właściwości, aby uzyskać dostęp do pola pomocniczego dla deklaracji właściwości (decyzja 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 akcesory 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 przełomowe
Istnienie kontekstowego słowa kluczowego field w ciałach akcesorów właściwości jest potencjalnie wprowadzającą niezgodność zmianą.
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 ciałach akcesorów właściwości mogą zabezpieczyć przed awariami podczas uaktualniania z wersji języka C# wcześniejszych niż 14 poprzez dodanie początkowego @.
Jeśli zmienna o nazwie field jest zadeklarowana w metodzie dostępu właściwości, występuje błąd.
W wersji językowej 14 lub nowszej jest zgłaszane ostrzeżenie, jeśli wyrażenie pierwotnefield odwołuje się do pola pomocniczego, ale we wcześniejszej wersji językowej odwoływałoby się do innego symbolu.
Atrybuty ukierunkowane na pola
Podobnie jak we właściwościach automatycznych, każda właściwość używająca pola zapasowego w jednym ze swoich metod dostępu będzie mogła korzystać z atrybutów skierowanych na pole.
[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 inicjowane bezpośrednio, zamiast wywołania settera (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 (EqualityComparer<T>.Default.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; }
}
Zdefiniowane przypisanie w strukturach
Mimo że nie można odnosić się do nich w konstruktorze, pola zapasowe oznaczone słowem kluczowym field podlegają domyślnej inicjalizacji oraz domyślnemu ostrzeganiu o ich wyłączeniu w tych samych warunkach, co 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. Obecnie brak jest przypadków użycia, dlatego nie jest to czas na umożliwienie zapisywania właściwości zwracających przez odwołanie 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 zagwarantować odpowiedni poziom zabezpieczeń przed wartościami null dla różnych zastosowań funkcji słowa kluczowego
field.Wzorce używające słowa kluczowego
fieldpowinny 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
Zobacz Słownik definicji nowych terminów.
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 wartość null intuicyjnie oznacza, że akcesorium właściwości get zachowuje bezpieczeństwo o wartości null nawet wtedy, gdy pole zawiera default wartość jej typu.
Właściwość oparta na polu jest określana jako odporna na wartości null lub nie, poprzez przeprowadzanie specjalnej analizy związanej z obsługą wartości null jej dostępu. Należy pamiętać, że ta analiza jest wykonywana tylko wtedy, gdy właściwość może być typu odwołania i nie jest adnotacją.
- Wykonywane są dwie oddzielne analizy dopuszczane do wartości null: jedna, w której
fieldadnotacja dopuszczana do wartości null nie jest adnotacją, i jedna, w której adnotacja dopuszczana do wartości null jest adnotacją. Rejestrowana jest diagnostyka dopuszczająca wartość null wynikająca z każdej analizy. - Jeśli w annotowanym przebiegu istnieje diagnostyka dopuszczana do wartości null, która nie była obecna w nienotowanym przekazaniu, właściwość nie jest odporna na wartości null. W przeciwnym razie jest odporny na wartości null.
- Implementacja może być optymalizowana, wykonując najpierw dodawanie adnotacji . Jeśli w ogóle nie zostanie określona diagnostyka, właściwość jest odporna na wartości null, a nieznotowana passa może zostać pominięta.
- 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.
Operatory typu null forgiving (!), dyrektywy, takie jak #nullable disable i #pragma disable warning, i konwencjonalne ustawienia na poziomie projektu, takie jak <NoWarn>, są przestrzegane podczas podejmowania decyzji, czy analiza dopuszczana do wartości null ma diagnostykę.
Narzędzia diagnosticSuppressors są ignorowane podczas podejmowania decyzji, czy analiza dopuszczana do wartości null ma diagnostykę.
Adnotacja dopuszczana do wartości null pola kopii zapasowej jest określana w następujący sposób:
- Jeśli adnotacja dopuszczana do wartości null skojarzonej właściwości jest adnotacja lub nieświadoma, adnotacja dopuszczana do wartości null jest taka sama jak adnotacja dopuszczana do wartości null skojarzonej właściwości.
- Jeśli adnotacja dopuszczana do wartości null skojarzonej właściwości nie jest adnotacją, wówczas:
- Jeśli właściwość jest odporna na wartości null, adnotacja dopuszczana do wartości null jest oznaczona adnotacją.
- Jeśli właściwość nie jest odporna na wartości null, adnotacja dopuszczana do wartości null nie jest adnotacją.
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ąć (nowy język pogrubiony):
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.
Uwaga / Notatka
Kluczową cechą właściwości odpornej na wartości null jest to, że zwraca wartość z "poprawną" wartością null nawet wtedy, gdy wartość pola zapasowego to default. Biorąc pod uwagę to, możemy rozważyć nawet nie śledzenie stanu przepływu dla takich właściwości. Wydaje się to jednak większą zmianą w analizie właściwości dopuszczających wartość null, która nie ma obecnie żadnego scenariusza motywowania zmiany.
Jest to zasadniczo ograniczona analiza międzyprocjowa. Przewidujemy, że w celu przeanalizowania konstruktora konieczne będzie przeprowadzenie analizy powiązania i "odporności null" dla wszystkich potencjalnie odpowiednich metod dostępu w tym samym typie, które używają field słowa kluczowego kontekstowego i nie mają adnotacji 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 odnosić się do akcesora set lub init.
Należy sprawdzić, czy elementy ustawiające właściwości oparte na polach nieodparte na wartości null rzeczywiście inicjują pole zapasowe.
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(); // no warning, 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 inicjator, stan początkowego przepływu jest taki sam jak stan przepływu po wizycie inicjatora.
- 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
Ten formuła jest celowo podobna do zwykłych pól w konstruktorach. Zasadniczo, ponieważ tylko metody dostępu do właściwości mogą rzeczywiście odwoływać się do pola wspierającego, setter jest traktowany jako "mini-konstruktor" dla tego pola.
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 nullowalności.
nameof
W miejscach, w których field jest słowem kluczowym, nie można skompilować nameof(field) (decyzja LDM), przykładowo 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.
Nadpisania
Właściwości nadpisujące mogą korzystać z 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ć możliwe przechwycenie w funkcjach lokalnych i lambdach, a odwołania do field z wewnątrz 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 językiem w wersji 14 lub nowszej field jest uznawane za słowo kluczowe, które jest używane jako wyrażenie podstawowe (decyzja LDM) w następujących lokalizacjach (decyzja LDM):
- W ciałach metod
get,setiinitakcesoró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
§15.7.1Właściwości - Ogólne
Property_initializer można podać tylko dla
automatycznie zaimplementowanej właściwości orazwł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 zaimplementowane właściwości
Automatycznie zaimplementowana właściwość (lub właściwość automatyczna w skrócie) jest właściwością nieabstrakcyjną, nie-extern i nieo wartości referencyjnej z ciałami akcesorów zawierającymi tylko średnik. Właściwości automatyczne muszą mieć akcesor get i mogą opcjonalnie mieć akcesor set. jednej lub obu z:
- akcesor z ciałem składającym się wyłącznie ze średnika
- użycie słowa kluczowego
fieldkontekstowego w akcesorach lubwyrażeniu w treści wł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 średnikamigetsą implementowane do odczytu z, a akcesory wyłącznie ze średnikamisetdo zapisu w polu pomocniczym.
Ukryte pole zapasowe jest niedostępne, można je odczytywać i zapisywać tylko za pośrednictwem automatycznie zaimplementowanych metod dostępu do właściwości, nawet w obrębie typu zawierającego.Pole zapasowe można odwoływać się bezpośrednio przy użyciu słowa kluczowegofieldwe wszystkich metodach dostępu i w treści wyrażenia właściwości. Ponieważ pole jest nienazwane, nie można go użyć w wyrażeniunameof.Jeśli właściwość automatyczna nie ma
akcesora ustawieńtylko akcesor get zakończony średnikiem, pole pomocnicze jest uznawane zareadonly(§15.5.3). Podobnie jak pole, właściwość automatyczna tylko do odczytureadonlymoże być również przypisana w treści konstruktora otaczającej klasy. Takie przypisanie odnosi się bezpośrednio do pola wspomagającegotylko do odczytuwłaściwości.Właściwość automatyczna nie może mieć wyłącznie jednego akcesora w postaci pojedynczego średnika bez drugiego akcesora.
Właściwość automatyczna może opcjonalnie mieć property_initializer, który jest stosowany bezpośrednio do pola pomocniczego 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 do nullowalności
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.
- Brak specjalnego traktowania zmiennej pola podczas analizowania akcesorów 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 nulowalne
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
fieldzawsze 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.
- Settery we właściwościach opartych na polu sprawdzają, czy
fieldjest zainicjowane, podobnie jak konstruktory.
Oznaczałoby to, że "scenariusz trochę leniwy" 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 jakbyAllowNullbyły niejawnie obecne.
Wnioskowanie początkowego stanu przepływu field
Zamiast definiować zachowanie pod względem fieldadnotacji dopuszczanej do wartości null, możemy po prostu zdefiniować początkowy stan field przepływu elementu w metodzie dostępu "get". Odzwierciedlałoby to rzeczywistość, w przypadku, gdy można przypisać do niej fieldmożliwą wartość null. Inne formy analizy udałoby się zidentyfikować wszelkie wynikające z tego problemy z bezpieczeństwem o wartości null.
Jednak stało się to w fazie wdrażania, w której rezygnacja wprowadzania tej zmiany nie była uważana za wartą. Jest to coś, co moglibyśmy dostosować pod maską w przyszłości, z bardzo niewielu użytkowników postrzegających wszelkie przerwy - to alternatywne podejście jest ogólnie bardziej permissive niż ten, który rzeczywiście zaimplementowaliśmy.
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 łamiące kompatybilność.
Jeśli identyfikatory są zawsze uznawane za słowa kluczowe, oznacza to zmianę powodującą niezgodność na przykład w następujących przypadkach:
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 tylko jako wyrażenia podstawowe, zmiana wywołująca niezgodność jest mniej poważna. 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'
}
Występuje również przerwanie, gdy field lub value jest ponownie zadeklarowane w zagnieżdżonej funkcji. Może to być jedyna przerwa dla valuewyraż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 nigdy nie są uznawane za słowa kluczowe, będą się wiązać tylko z syntetyzowanym polem pomocniczym lub niejawnym parametrem, pod warunkiem że nie są powiązane z innymi członkami. W tym przypadku nie ma 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ź
Zabraniaj tylko tego, co jest już niedozwolone dzisiaj we właściwościach automatycznych, czyli 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łowem kluczowym w akcesorze zdarzenia i nie jest generowane żadne pole pomocnicze.
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 oraz zawartym w niej otwartym 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 pomocniczego w zakresie i 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 inicjalizatorowi w części definicji, skutecznie zmusza to implementatora do korzystania z
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 kluczowegofieldoszczę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 inicjalizator na dowolnej części właściwości częściowej, gdy część implementacyjna 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, implementacja częściowa właściwości musi mieć ciało dla wszystkich akcesorów. Jednak ostatnie iteracje funkcji słowa kluczowego field obejmowały pojęcie "automatycznych akcesorów". 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 syntetyzowane pole zapasowe powinno być traktowane jako 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 pomocnicze jest uznawane za tylko do odczytu, pole emitowane do metadanych jest oznaczane initonly, a zgłaszany jest błąd, jeśli field jest modyfikowane poza inicjatorem lub konstruktorem.
Zalecenie: zsyntetyzowane pole zapasowe 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 zsyntetyzowane pole powinno być generowane, gdy field jest używane tylko w pominiętych wywołaniach 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;
}
}
}
W celu odniesienia, pola dla podstawowych parametrów konstruktora zostają generowane w podobnych przypadkach — zobacz sharplab.io.
Zalecenie: pole zapasowe 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 instancji pojawi się błąd informujący, że pola instancji 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