Udostępnij za pośrednictwem


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; i init; 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;lub init;).

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:

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

  2. 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 kluczowego field.

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 pola jest określana jako odporne na wartości null lub nie przez przeprowadzenie specjalnej analizy dopuszczalnej do wartości null jej akcesorium.

  • Dla celów tej analizy zakłada się, że field jest tymczasowo oznaczony jako posiadający adnotację zanulowania, np. string?. Powoduje to, że field może mieć wartość null lub może być z domyślnym stanem początkowym w akcesorze get, 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, NotNulllub DisallowNull, 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 lub T) 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 lub T) 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, seti init 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

  1. akcesor z ciałem zawierającym tylko średnik
  2. użycie kontekstowego słowa kluczowego field w akcesorach lub treści wyrażeniawłaściwości

Gdy 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 średnikami get 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 kluczowego fieldwe 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 za readonly (§15.5.3). Podobnie jak w przypadku pola readonly, 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 w tylko do odczytu polu pomocniczym właściwości.

Właściwość automatyczna nie może mieć tylko jednego setset akcesorium bez get 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 opisanego w sekcji dotyczącym nullowalności, grupa robocza zasugerowała następujące alternatywy do rozważenia przez LDM:

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.

  1. Zmienna field zawsze ma tę samą adnotację dopuszczającą wartość null co właściwość.
  2. Atrybuty zerowalności [field: MaybeNull, AllowNull] itp. mogą służyć do dostosowywania zerowalności pola pomocniczego.
  3. 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.
  4. 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ż dodanie MaybeNull. 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 kluczowego field, a nawet ogólnie pól, aby umożliwić zapisanie wartości null w zmiennej, tak jakby AllowNull 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?

  1. zawsze
  2. tylko wyrażenia podstawowe
  3. 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.

  1. { set; } - Niedozwolone dzisiaj, kontynuuj zakazywanie
  2. { set => field = value; }
  3. { get => unrelated; set => field = value; }
  4. { get => unrelated; set; }
  5. {
        set
        {
            if (field == value) return;
            field = value;
            SendEvent(nameof(Prop), value);
        }
    }
    
  6. {
        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 kluczowego field 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 initonlyi 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.