Udostępnij za pośrednictwem


Automatycznie domyślne struktury

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 spotkań projektowych dotyczących 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 .

Problem z liderem: https://github.com/dotnet/csharplang/issues/5737

Streszczenie

Ta funkcja powoduje, że w konstruktorach struktury identyfikujemy pola, które nie zostały jawnie przypisane przez użytkownika przed zwróceniem lub użyciem, i inicjuje się je niejawnie do default zamiast wywoływania błędów określonego przypisania.

Motywacja

Ten wniosek jest przedstawiany jako możliwe złagodzenie problemów z użytecznością znalezionych w dotnet/csharplang#5552 i dotnet/csharplang#5635, oraz odniesienie się do #5563 (wszystkie pola muszą być definitywnie przypisane, ale field nie jest dostępne w konstruktorze).


Od języka C# 1.0 konstruktory struktury muszą zdecydowanie przypisać this tak, jakby był to parametr out.

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

Przedstawia to problemy podczas ręcznego definiowania setterów w półautomatycznych właściwościach, ponieważ kompilator nie może traktować przypisania właściwości jako równoważnej przypisaniu pola pomocniczego.

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

Zakładamy, że wprowadzenie bardziej precyzyjnych ograniczeń dla metod ustawiających, na przykład schematu, w którym setter nie przyjmuje ref this, ale raczej out field jako parametr, może okazać się zbyt niszowe i niewystarczające dla niektórych przypadków użycia.

Jednym z podstawowych napięć, z którymi zmagamy się, jest to, że gdy właściwości struktur mają ręcznie zaimplementowane settery, użytkownicy często muszą wykonywać jakąś formę "powtarzania" – czy to poprzez wielokrotne przypisywanie, czy powtarzanie swojej logiki.

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

Poprzednia dyskusja

Niewielka grupa przejrzała ten problem i rozważyła kilka możliwych rozwiązań:

  1. Wymagaj od użytkowników przypisywania this = default, gdy właściwości częściowo automatyczne mają ręcznie zaimplementowane moduły ustawiania. Zgadzamy się, że jest to niewłaściwe rozwiązanie, ponieważ wydmuchuje wartości ustawione w inicjatorach pól.
  2. Niejawnie inicjuj wszystkie pola wspierające właściwości automatycznych/półautomatycznych.
    • Rozwiązuje to problem z "półautomatycznymi ustawieniami właściwości" i umieszcza jawnie zadeklarowane pola pod innymi regułami: "nie inicjalizuj niejawnie moich pól, ale inicjalizuj niejawnie moje właściwości automatyczne".
  3. Umożliwienie przypisania pola bazowego właściwości częściowo automatycznej oraz wymaganie, aby użytkownicy je przypisali.
    • Może to być kłopotliwe w porównaniu do (2). Właściwość automatyczna ma być "automatyczna", a być może obejmuje "automatyczne" inicjowanie pola. Może to wprowadzać zamieszanie, gdy bazowe pole jest ustawiane za pomocą przypisania do właściwości, a kiedy wywoływany jest setter właściwości.

Otrzymaliśmy również opinii od użytkowników, którzy chcą na przykład uwzględnić kilka inicjatorów pól w strukturach bez konieczności jawnego przypisywania wszystkiego. Możemy rozwiązać ten problem, a także "właściwość częściowo automatyczna z ręcznie zaimplementowanym ustawiaczem" w tym samym czasie.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

Dostosowywanie określonego przypisania

Zamiast wykonywać określoną analizę przypisania, aby generować błędy dla nieprzypisanych pól w this, robimy to, aby określić, które pola muszą być inicjowane niejawnie. Taka inicjalizacja jest wstawiana na początku konstruktora.

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

W przykładach (4) i (5) wynikowy kodgen czasami zawiera "podwójne przypisania" pól. Ogólnie rzecz biorąc, jest to w porządku, ale dla użytkowników, którzy obawiają się takich podwójnych przypisań, możemy wyemitować to, co wcześniej było określane jako diagnostyka błędu przypisania, jako ostrzeżenie wyłączone domyślnie diagnostyka ostrzegawcza.

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

Użytkownicy, którzy ustawiają ważność tej diagnostyki na "błąd", wybiorą zachowanie sprzed C# 11. Tacy użytkownicy są zasadniczo "wykluczeni" z właściwości półautomatycznych z ręcznie zaimplementowanymi ustawieniami.

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

Na pierwszy rzut oka wydaje się być dziurą w tej funkcji, ale to jest rzeczywiście właściwą rzeczą do zrobienia. Włączając diagnostykę, użytkownik informuje nas, że nie chce, aby kompilator domyślnie inicjował pola w konstruktorze. W tym miejscu nie ma możliwości uniknięcia niejawnej inicjalizacji, więc rozwiązaniem jest użycie innego sposobu inicjowania pola niż ręcznie zaimplementowany setter, taki jak ręczne deklarowanie pola i przypisywanie go lub dołączanie inicjatora pól.

Obecnie JIT nie eliminuje zbędnych zapisów przy użyciu odwołań, co oznacza, że te niejawne inicjalizacje mają rzeczywisty koszt. Ale to może być możliwe do naprawienia. https://github.com/dotnet/runtime/issues/13727

Warto zauważyć, że inicjowanie poszczególnych pól zamiast całego wystąpienia jest naprawdę tylko optymalizacją. Kompilator powinien mieć swobodę w implementacji dowolnej heurystyki, pod warunkiem że spełnia niezmiennik, który określa, że pola, które nie są na pewno przypisane w żadnym z punktów zwrotnych lub przed jakimkolwiek dostępem do członka niebędącego polem this, są inicjowane niejawnie.

Jeśli na przykład struktura ma 100 pól, a tylko jedno z nich jest jawnie zainicjowane, bardziej sensowne byłoby wykonanie initobj na całej strukturze, niż niejawne wyemitowanie initobj dla pozostałych 99 pól. Jednak implementacja, która niejawnie emituje initobj dla 99 innych pól, nadal będzie prawidłowa.

Zmiany specyfikacji języka

Dostosowujemy następującą sekcję standardu:

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

Jeśli deklaracja konstruktora nie ma inicjatora konstruktora, zmienna this zachowuje się dokładnie tak samo jak parametr out typu struktury. W szczególności oznacza to, że zmienna musi być jednoznacznie przypisana w każdej ścieżce wykonywania konstruktora wystąpienia.

Dostosujemy ten język do odczytu:

Jeśli deklaracja konstruktora nie ma inicjatora konstruktora, zmienna this zachowuje się podobnie do parametru out typu struktury, z wyjątkiem tego, że nie jest to błąd, gdy określone wymagania przypisania (§9.4.1) nie są spełnione. Zamiast tego wprowadzamy następujące zachowania:

  1. Gdy zmienna this sama nie spełnia wymagań, wszystkie nieprzypisane zmienne wystąpienia w this na wszystkich punktach, gdzie wymagania są naruszane, są automatycznie inicjowane do wartości domyślnej (zgodnie z§9.3) w fazie inicjacji , zanim zostanie uruchomiony jakikolwiek inny kod w konstruktorze.
  2. Jeśli zmienna instancji v w ramach this nie spełnia wymagań lub któraś zmienna instancji na dowolnym poziomie zagnieżdżenia w v również nie spełnia wymagań, to v jest niejawnie inicjowana domyślną wartością w fazie inicjalizacji , zanim zostanie uruchomiony jakikolwiek inny kod w konstruktorze.

Spotkania projektowe

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs