Udostępnij za pośrednictwem


Ulepszenia struktury niskiego poziomu

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ą przechwytywane w odpowiednich spotkania projektowego 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 .

Streszczenie

Ta propozycja jest agregacją kilku różnych propozycji dotyczących ulepszeń wydajności struct: ref pól i możliwości zastąpienia domyślnych wartości okresu trwałości. Celem jest projekt, który uwzględnia różne propozycje tworzenia pojedynczego nadrzędnego zestawu funkcji na potrzeby ulepszeń struct niskiego poziomu.

Uwaga: Poprzednie wersje tej specyfikacji używały terminów "ref-safe-to-escape" i "safe-to-escape", które zostały wprowadzone w specyfikacji funkcji bezpieczeństwa Span. Komisja Standardowa ECMA zmieniła nazwy na "ref-safe-context" i "safe-context", odpowiednio. Wartości bezpiecznego kontekstu zostały uściślone w celu spójnego używania "bloku deklaracji", "elementu funkcji" i "kontekstu wywołującego". Speclety używały różnych sformułowań dla tych terminów, a także używały "bezpiecznego powrotu" jako synonimu dla "caller-context". Ten speclet został zaktualizowany, aby używać terminów w standardzie C# 7.3.

Nie wszystkie funkcje opisane w tym dokumencie zostały zaimplementowane w języku C# 11. C# 11 obejmuje:

  1. ref pola i scoped
  2. [UnscopedRef]

Te funkcje pozostają otwartymi propozycjami przyszłej wersji języka C#:

  1. ref pola do ref struct
  2. Typy z ograniczeniami o zachodzie słońca

Motywacja

Wcześniejsze wersje języka C# dodały do języka szereg funkcji wydajności niskiego poziomu: ref zwraca, ref struct, wskaźniki funkcji itp. ... Dzięki temu deweloperzy platformy .NET mogą pisać wysoce wydajny kod, kontynuując korzystanie z reguł języka C# na potrzeby bezpieczeństwa typów i pamięci. Pozwoliło to również na tworzenie podstawowych typów wydajności w bibliotekach platformy .NET, takich jak Span<T>.

Ponieważ te funkcje zyskały popularność w ekosystemie platformy .NET, deweloperzy, zarówno wewnętrzni, jak i zewnętrzni, dostarczają nam informacje na temat pozostałych kwestii problematycznych w ekosystemie. Miejsca, w których nadal muszą porzucać kod unsafe, aby wykonać pracę, lub wymagać środowiska uruchomieniowego do specjalnych typów przypadków, takich jak Span<T>.

Obecnie Span<T> jest realizowane przy użyciu typu internalByReference<T>, który środowisko uruchomieniowe skutecznie traktuje jako pole ref. Zapewnia to korzyści z ref pól, ale z wadą, że język nie zapewnia weryfikacji bezpieczeństwa, podobnie jak w przypadku innych zastosowań ref. Ponadto tylko dotnet/runtime może używać tego typu, ponieważ jest to internal, więc strony trzecie nie mogą projektować własnych prymitywów na podstawie pól ref. Częścią motywacji do tej pracy jest usunięcie ByReference<T> i użycie odpowiednich pól ref we wszystkich bazach kodu.

Ten wniosek planuje rozwiązać te problemy, opierając się na istniejących funkcjach niskiego poziomu. W szczególności ma na celu:

  • Zezwalaj typom ref struct na deklarowanie pól ref.
  • Zezwól środowisku uruchomieniowemu na pełne zdefiniowanie Span<T> przy użyciu systemu typów języka C# i usunięcie specjalnego typu przypadku, takiego jak ByReference<T>
  • Zezwolić typom struct na zwracanie ref do ich pól.
  • Pozwól środowisku uruchomieniowemu usunąć użycia unsafe spowodowane ograniczeniami domyślnych ustawień okresów istnienia.
  • Zezwalanie na deklarowanie bezpiecznych buforów fixed dla typów zarządzanych i niezarządzanych w struct

Szczegółowy projekt

Reguły bezpieczeństwa ref struct są zdefiniowane w dokumencie bezpieczeństwa zakresu przy użyciu poprzednich terminów. Zasady te zostały włączone do standardu C# 7 w §9.7.2 i §16.4.12. W tym dokumencie opisano wymagane zmiany w tym dokumencie w wyniku tej propozycji. Po zaakceptowaniu jako zatwierdzonej funkcji te zmiany zostaną włączone do tego dokumentu.

Po zakończeniu tego projektu definicja Span<T> będzie następująca:

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    // This constructor does not exist today but will be added as a part 
    // of changing Span<T> to have ref fields. It is a convenient, and
    // safe, way to create a length one span over a stack value that today 
    // requires unsafe code.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

Podaj pola ref i zakres

Język umożliwi deweloperom deklarowanie pól ref wewnątrz ref struct. Może to być przydatne na przykład w przypadku enkapsulacji dużych modyfikowalnych wystąpień struct lub definiowania typów wysokiej wydajności, takich jak Span<T> w bibliotekach obok środowiska uruchomieniowego.

ref struct S 
{
    public ref int Value;
}

Pole ref będzie emitowane do metadanych przy użyciu podpisu ELEMENT_TYPE_BYREF. Nie różni się to od tego, jak emitujemy lokalnych ref lub argument ref. Na przykład ref int _field będą emitowane jako ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. Będzie to wymagało aktualizacji ECMA335, aby zezwolić na ten wpis, ale powinno to być raczej proste.

Deweloperzy mogą nadal inicjować ref struct z użyciem pola ref i wyrażenia default, co spowoduje, że wszystkie zadeklarowane pola ref będą miały wartość null. Każda próba użycia takich pól spowoduje zgłoszenie błędu NullReferenceException.

ref struct S 
{
    public ref int Value;
}

S local = default;
local.Value.ToString(); // throws NullReferenceException

Język C# udaje, że ref nie może stać się null, co jest legalne na poziomie środowiska uruchomieniowego i ma dobrze zdefiniowaną semantykę. Deweloperzy, którzy wprowadzają ref pola do swoich typów, muszą mieć świadomość tej możliwości i należy ich zdecydowanie odradzać przed wyciekaniem tego szczegółu do kodu używającego tych typów. Zamiast tego pola ref powinny być sprawdzane pod kątem niezerowych wartości z użyciem pomocników środowiska uruchomieniowego oraz, z wyrzuceniem błędu, gdy nieprawidłowo używana jest niezainicjowana struct.

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

Pole ref można połączyć z modyfikatorami readonly w następujący sposób:

  • readonly ref: jest to pole, którego nie można ponownie przypisać poza konstruktorem lub metodami init. Może być przypisana wartość również w innych kontekstach.
  • ref readonly: jest to pole, które można ponownie przypisać jako referencję, ale nie można przypisać mu wartości w żadnym momencie. W ten sposób można ponownie przypisać parametr in do pola ref.
  • readonly ref readonly: kombinacja ref readonly i readonly ref.
ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // Okay
        Field1 = array[0];      // Error: can't assign ref readonly value (value is readonly)
        Field2 = ref array[0];  // Error: can't repoint readonly ref
        Field2 = array[0];      // Okay
        Field3 = ref array[0];  // Error: can't repoint readonly ref
        Field3 = array[0];      // Error: can't assign ref readonly value (value is readonly)
    }
}

readonly ref struct będzie wymagać, aby pola ref zostały zadeklarowane readonly ref. Nie ma wymogu, aby zostały zadeklarowane readonly ref readonly. Pozwala to readonly struct mieć mutacje pośrednie za pośrednictwem takiego pola, ale nie różni się to od pola readonly wskazującego typ referencyjny dzisiaj (więcej szczegółów)

readonly ref zostanie emitowany do metadanych przy użyciu flagi initonly, tak samo jak każde inne pole. Pole ref readonly będzie posiadać atrybut System.Runtime.CompilerServices.IsReadOnlyAttribute. readonly ref readonly zostanie wyemitowany z obydwoma elementami.

Ta funkcja wymaga obsługi środowiska uruchomieniowego i zmian specyfikacji ECMA. W związku z tym zostaną one włączone tylko wtedy, gdy odpowiednia flaga funkcji zostanie ustawiona w pliku corelib. Problem śledzący dokładny interfejs API jest śledzony tutaj https://github.com/dotnet/runtime/issues/64165

Zestaw zmian w naszych regułach bezpiecznego kontekstu niezbędnych do zezwolenia na pola ref jest mały i specyficzny. Reguły już uwzględniają pola ref, które istnieją i są wykorzystywane z interfejsów API. Zmiany muszą skupić się tylko na dwóch aspektach: sposobie ich tworzenia i sposobie, w jaki są ponownie przypisywane.

Najpierw należy zaktualizować reguły ustanawiania wartości ref-safe-context dla pól ref w następujący sposób:

Wyrażenie w formularzu ref e.Fref-safe-context w następujący sposób:

  1. Jeśli F jest polem ref, jego ref-safe-context jest bezpieczny kontekste.
  2. W przeciwnym razie jeśli e jest typu referencyjnego, posiada kontekst bezpieczny dla referencji w kontekście wywołującym
  3. W przeciwnym razie kontekst bezpieczeństwa odniesienia jest pobierany z kontekstu bezpieczeństwa odniesienia e.

Nie reprezentuje to jednak zmiany reguły, ponieważ reguły zawsze uwzględniały stan ref w ref struct. Tak właśnie działał stan ref w Span<T> od zawsze, a zasady zużycia prawidłowo to uwzględniają. Ta zmiana ma na celu umożliwienie deweloperom bezpośredniego dostępu do pól ref oraz zapewnienie, że robią to zgodnie z już istniejącymi zasadami stosowanymi do Span<T>.

Oznacza to jednak, że pola ref mogą być zwracane jako ref z ref struct, natomiast normalne pola nie mają takiej możliwości.

ref struct RS
{
    ref int _refField;
    int _field;

    // Okay: this falls into bullet one above. 
    public ref int Prop1 => ref _refField;

    // Error: This is bullet four above and the ref-safe-context of `this`
    // in a `struct` is function-member.
    public ref int Prop2 => ref _field;
}

Może to wydawać się błędem na pierwszy rzut oka, ale jest to celowy punkt projektowania. Ponownie jednak, nie jest to nowa reguła tworzona przez tę propozycję; zamiast tego jest to uznanie istniejących reguł Span<T>, które były przestrzegane dotąd, aby deweloperzy mogli zadeklarować własny stan ref.

Następnie należy dostosować reguły ponownego przypisania ref, aby uwzględnić obecność pól ref. Podstawowym scenariuszem ponownego przypisania ref jest ref struct konstruktory przechowujące parametry ref w polach ref. Obsługa będzie bardziej ogólna, ale jest to podstawowy scenariusz. Aby to wesprzeć, reguły ponownego przypisania ref zostaną dostosowane do uwzględnienia pól ref w następujący sposób:

Reguły ponownego przypisania ref

Lewy operand operatora = ref musi być wyrażeniem, które wiąże się ze zmienną lokalną ref, parametrem ref (innym niż this), parametrem out, lub polem ref.

W przypadku ponownego przypisania ref w formularzu e1 = ref e2 oba następujące elementy muszą mieć wartość true:

  1. e2 musi mieć ref-safe-context co najmniej tak duży jak ref-safe-context z e1
  2. e1 musi mieć taki sam kontekst bezpieczny jak e2Uwaga

Oznacza to, że żądany konstruktor Span<T> działa bez dodatkowej adnotacji:

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    public Span(ref T value)
    {
        // Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The 
        // safe-context of `this` is *return-only* and ref-safe-context of `value` is 
        // *caller-context* hence this is legal.
        _field = ref value;
        _length = 1;
    }
}

Zmiana reguł ponownego przypisania ref oznacza, że parametry ref mogą teraz uciec od metody jako pola ref w wartości ref struct. Zgodnie z opisem w sekcji dotyczącym zagadnień zgodności, może to zmienić zasady dla istniejących interfejsów API, które nigdy nie miały być przeznaczone do tego, aby parametry ref były traktowane jako pole ref. Reguły okresu istnienia parametrów są oparte wyłącznie na ich deklaracji, a nie na ich użyciu. Wszystkie parametry ref i in mają ref-safe-context kontekstu wywołującego, dlatego mogą być teraz zwracane przez ref lub pole ref. Aby obsługiwać interfejsy API z ref parametrami, które mogą być 'escaping' lub 'non-escaping', i tym samym aby przywrócić semantykę miejsca wywołań języka C# 10, język zostanie wprowadzony z ograniczonymi adnotacjami okresu istnienia.

modyfikator scoped

Słowo kluczowe scoped będzie używane do ograniczania okresu istnienia wartości. Można go zastosować do ref lub wartości, która jest ref struct i ma wpływ na ograniczenie ref-safe-context lub bezpiecznego kontekstu okresu istnienia odpowiednio do składowej funkcji. Na przykład:

Parametr lub lokalny ref-safe-context bezpieczny kontekst
Span<int> s element funkcji kontekstu wywołującego
scoped Span<int> s element funkcji element funkcji
ref Span<int> s kontekstu wywołującego kontekstu wywołującego
scoped ref Span<int> s element funkcji kontekstu wywołującego

W tej relacji kontekst bezpieczny wartości nigdy nie może być szerszy niż bezpieczny kontekst.

Dzięki temu interfejsy API w języku C# 11 mogą być oznaczone adnotacjami, tak aby miały te same reguły co C# 10:

Span<int> CreateSpan(scoped ref int parameter)
{
    // Just as with C# 10, the implementation of this method isn't relevant to callers.
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 and legal in C# 11 due to scoped ref
    return CreateSpan(ref parameter);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

Adnotacja scoped oznacza również, że parametr this elementu struct można teraz zdefiniować jako scoped ref T. Wcześniej trzeba było stosować specjalne przypadki w regułach jako parametr ref, który miał różne reguły kontekstu ref niż inne parametry ref (zobacz wszystkie odwołania do dołączania lub wykluczania odbiornika w regułach bezpiecznego kontekstu). Teraz można je wyrazić jako ogólną koncepcję w ramach reguł, które jeszcze bardziej je upraszczają.

Adnotacja scoped można również zastosować do następujących lokalizacji:

  • ustawienia lokalne: ta adnotacja ustawia okres istnienia jako bezpieczny kontekstlub kontekst bezpieczny dla referencji w przypadku lokalnego ref, dla członka funkcji niezależnie od okresu istnienia inicjatora.
Span<int> ScopedLocalExamples()
{
    // Error: `span` has a safe-context of *function-member*. That is true even though the 
    // initializer has a safe-context of *caller-context*. The annotation overrides the 
    // initializer
    scoped Span<int> span = default;
    return span;

    // Okay: the initializer has safe-context of *caller-context* hence so does `span2` 
    // and the return is legal.
    Span<int> span2 = default;
    return span2;

    // The declarations of `span3` and `span4` are functionally identical because the 
    // initializer has a safe-context of *function-member* meaning the `scoped` annotation
    // is effectively implied on `span3`
    Span<int> span3 = stackalloc int[42];
    scoped Span<int> span4 = stackalloc int[42];
}

Inne zastosowania scoped w ustawieniach lokalnych zostały omówione poniżej.

Adnotacja scoped nie może być stosowana do żadnej innej lokalizacji, w tym do zwracania, pól, elementów tablicy itp. Ponadto scoped ma wpływ, gdy jest stosowana do dowolnych ref, in lub out, ale tylko wtedy, gdy jest stosowana do wartości, które są ref struct. Posiadanie deklaracji takich jak scoped int nie ma wpływu, ponieważ nie ref struct jest zawsze bezpieczny do powrotu. Kompilator utworzy diagnostykę dla takich przypadków, aby uniknąć nieporozumień deweloperów.

Zmienianie zachowania parametrów out

Aby dodatkowo ograniczyć wpływ zmiany dotyczącej zgodności związaną z możliwością zwracania parametrów i jako pól , język zmieni domyślną wartość dla parametrów , tak aby była zgodna z funkcją członkowskąw kontekście bezpiecznym dla odnośników . Faktycznie out parametry są niejawnie scoped out na przyszłość. Z punktu widzenia zgodności oznacza to, że nie można ich zwrócić przy użyciu ref.

ref int Sneaky(out int i) 
{
    i = 42;

    // Error: ref-safe-context of out is now function-member
    return ref i;
}

To zwiększy elastyczność interfejsów API, które zwracają ref struct wartości i mają out parametry, ponieważ nie trzeba już uwzględniać parametru przechwytywanego przez referencję. Jest to ważne, ponieważ jest to typowy wzorzec w API stylu czytelnika.

Span<byte> Read(Span<byte> buffer, out int read)
{
    // .. 
}

Span<byte> Use()
{
    var buffer = new byte[256];

    // If we keep current `out` ref-safe-context this is an error. The language must consider
    // the `read` parameter as returnable as a `ref` field
    //
    // If we change `out` ref-safe-context this is legal. The language does not consider the 
    // `read` parameter to be returnable hence this is safe
    int read;
    return Read(buffer, out read);
}

Język nie będzie już traktować argumentów przekazywanych do parametru out jako możliwych do zwrócenia. Traktowanie danych wejściowych dla parametru out jako możliwych do zwrotu było niezwykle mylące dla deweloperów. Zasadniczo odwraca intencję out, zmuszając deweloperów do rozważenia wartości przekazanej przez obiekt wywołujący, która w przeciwnym razie nigdy nie jest używana, z wyjątkiem języków, które nie respektują out. W przyszłości języki obsługujące ref struct muszą upewnić się, że oryginalna wartość przekazana do parametru out nigdy nie jest odczytywana.

Język C# osiąga to za pośrednictwem określonych reguł przypisania. To spełnia zarówno nasze zasady bezpiecznego kontekstu ref, jak i umożliwia działanie istniejącego kodu, który przypisuje wartości parametrów i następnie zwraca out.

Span<int> StrangeButLegal(out Span<int> span)
{
    span = default;
    return span;
}

Te zmiany oznaczają, że argument parametru out nie przyczynia się do wartości bezpiecznego kontekstu ani wartości ref-bezpiecznego kontekstu podczas wywołań metod. Znacznie zmniejsza to całkowity wpływ pól ref, a także upraszcza sposób, w jaki programiści myślą o out. Argument parametru out nie przyczynia się do zwracania, jest to po prostu dane wyjściowe.

Wnioskowanie bezpiecznego kontekstu wyrażeń deklaracji

bezpiecznego kontekstu zmiennej deklaracji z argumentu () lub dekonstrukcji () jest najwęższą następujących elementów:

  • kontekst wywołujący
  • Jeśli zmienna out jest oznaczona scoped, to znajduje się w obrębie deklaracji (tj. element składowy funkcji lub bardziej szczegółowy).
  • jeśli typ zmiennej out jest ref struct, należy wziąć pod uwagę wszystkie argumenty zawierające wywołanie, w tym odbiornik:
    • bezpieczny kontekst dowolnego argumentu, w którym odpowiadający mu parametr nie jest i ma bezpieczny kontekst tylko do powrotu lub szerszy
    • ref-safe-context dowolnego argumentu, w którym odpowiadający mu parametr ma ref-safe-contextzwracanych tylko lub szerszy

Zobacz również Przykłady wnioskowanych bezpiecznego kontekstu wyrażeń deklaracji.

Niejawnie scoped parametrów

ogólnie istnieją dwie lokalizacje ref, które są niejawnie zadeklarowane jako scoped:

  • this w metodzie instancji struct
  • parametry out

Reguły bezpiecznego kontekstu ref zostaną napisane pod względem scoped ref i ref. W celach kontekstu bezpiecznego ref parametr in jest odpowiednikiem ref, a out jest odpowiednikiem scoped ref. Zarówno in, jak i out będą wywoływane tylko wtedy, gdy jest to szczególnie ważne dla semantyki reguły. W przeciwnym razie są one traktowane jako ref i scoped ref odpowiednio.

Podczas omawiania ref-safe-context dla argumentów odpowiadających parametrom in, będą one uogólnione jako argumenty ref w specyfikacji. Jeśli argument to lvalue, wówczas ref-safe-context jest takim lvalue, w przeciwnym razie jest to członek funkcji. Ponownie in będzie wywołane tutaj tylko wtedy, gdy jest to ważne do semantyki bieżącej reguły.

Kontekst bezpieczny tylko dla powrotu

Projekt wymaga również wprowadzenia nowego bezpiecznego kontekstu: tylko do odczytu. Jest to podobne do kontekstu wywołującego w tym, że można go zwrócić, ale może tylko być zwracane za pomocą instrukcji .

Szczegóły tylko do zwracania polegają na tym, że jest to kontekst większy niż funkcja członkowska , ale mniejszy niż kontekst wywołującego . Wyrażenie podane w instrukcji return musi być co najmniej zwracane tylko. W związku z tym większość istniejących reguł wypada. Na przykład przypisanie parametru z wyrażenia z bezpiecznym kontekstem zwracanych zakończy się niepowodzeniem, ponieważ jest mniejszy niż kontekstu bezpiecznego kontekstu wywołującego. Potrzeba tego nowego kontekstu ucieczki zostanie omówiona poniżej.

Istnieją trzy lokalizacje, które domyślnie tylko do zwrócenia:

  • Parametr ref lub in będzie miał tylko do zwrotu. Jest to wykonywane częściowo dla ref struct, aby zapobiec głupim cyklicznym problemom z przypisywaniem. Odbywa się to jednak równomiernie, aby uprościć model, a także zminimalizować zmiany zgodności.
  • Parametr out dla ref struct będzie miał bezpieczny kontekst przy zwracaniu tylko. To pozwala na to, aby zarówno return, jak i out były równie wyraziste. Nie ma tu głupiego problemu z cyklicznym przypisaniem, ponieważ out jest niejawnie scoped, więc kontekst bezpieczny dla referencji jest nadal mniejszy niż bezpieczny kontekst .
  • Parametr this konstruktora struct będzie miał bezpieczny konteksttylko do zwracania. Wynika to z faktu, że jest modelowane jako parametry out.

Każde wyrażenie lub instrukcja, która jawnie zwraca wartość z metody lub wyrażenia lambda, musi mieć safe-context, a jeżeli ma to zastosowanie ref-safe-context, co najmniej tylko do zwracania. Obejmuje to return instrukcje, członków zdefiniowanych za pomocą wyrażeń oraz wyrażenia lambda.

Podobnie każde przypisanie do out musi mieć bezpieczny kontekst co najmniej tylko do zwracania. Nie jest to jednak szczególny przypadek, wynika to tylko z istniejących reguł przypisania.

Uwaga: wyrażenie, którego typ nie jest typem ref struct, zawsze ma bezpieczny kontekstkontekstu wywołującego.

Reguły wywołania metody

Reguły bezpiecznego kontekstu ref dla wywołania metody zostaną zaktualizowane na kilka sposobów. Pierwszy z nich polega na rozpoznaniu wpływu, jaki scoped ma na argumenty. Dla danego argumentu expr, który jest przekazywany do parametru p:

  1. Jeśli p jest scoped ref, expr nie przyczynia się do kontekstu bezpiecznego dla ref podczas rozważania argumentów.
  2. Jeśli p jest scoped, expr nie wnosi do bezpiecznego kontekstu przy rozważaniu argumentów.
  3. Jeśli p jest out, expr nie przyczynia się do bezpiecznego kontekstu ani bezpiecznego kontekstuwięcej szczegółów

Język "nie przyczynia się" oznacza, że argumenty nie są po prostu brane pod uwagę podczas obliczania ref-safe-context lub wartości bezpiecznego kontekstu zwracanej odpowiednio wartości metody. Wynika to z faktu, że wartości nie mogą przyczynić się do tego okresu istnienia, ponieważ adnotacja scoped uniemożliwia jej działanie.

Reguły wywołania metody można teraz uprościć. Odbiornik nie musi już być specjalny, w przypadku struct jest to teraz po prostu scoped ref T. Reguły wartości muszą ulec zmianie, aby uwzględnić zwracane wartości pola ref.

Wartość wynikająca z wywołania metody e1.M(e2, ...), gdzie M() nie zwraca struktury ref-to-ref, ma bezpieczny kontekst pobrany z najwęższego z następujących elementów:

  1. kontekst wywołujący
  2. Gdy wartość zwracana to ref struct, do bezpiecznego kontekstu przyczyniają się wszystkie wyrażenia argumentów.
  3. Kiedy zwrot jest ref struct, kontekst bezpieczny dla ref tworzony przez wszystkie argumenty ref

Jeśli M() zwraca strukturę ref-to-ref-struct, bezpieczny kontekst jest taki sam jak bezpieczny kontekst wszystkich argumentów, które są ref-to-ref-struct. Jest to błąd, jeśli istnieje wiele argumentów z różnymi bezpiecznymi kontekstami, ponieważ argumenty metody muszą być zgodne z.

Reguły wywoływania ref można uprościć do następujących czynności:

Wartość wynikająca z wywołania metody ref e1.M(e2, ...), gdzie M() nie zwraca struktury odwołującej-się-do-struktury, jest w kontekście bezpiecznym dla referencji najwęższym z następujących kontekstów:

  1. kontekst wywołujący
  2. bezpieczny kontekst dostarczony przez wszystkie wyrażenia argumentów
  3. ref-safe-context wprowadziły wszystkie argumenty

Jeśli M() zwraca ref-to-ref-struct, ref-safe-context jest najwęższym ref-safe-context definiowanym przez wszystkie argumenty, które są ref-to-ref-struct.

Ta reguła pozwala teraz zdefiniować dwa warianty żądanych metod:

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *function-member* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> CreateAndCapture(ref int value)
{
    // Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *caller-context* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
    // Okay: the safe-context of `span` is *caller-context* hence this is legal.
    return span;

    // Okay: the local `refLocal` has a ref-safe-context of *function-member* and a 
    // safe-context of *caller-context*. In the call below it is passed to a 
    // parameter that is `scoped ref` which means it does not contribute 
    // ref-safe-context. It only contributes its safe-context hence the returned
    // rvalue ends up as safe-context of *caller-context*
    Span<int> local = default;
    ref Span<int> refLocal = ref local;
    return ComplexScopedRefExample(ref refLocal);

    // Error: similar analysis as above but the safe-context of `stackLocal` is 
    // *function-member* hence this is illegal
    Span<int> stackLocal = stackalloc int[42];
    return ComplexScopedRefExample(ref stackLocal);
}

Reguły inicjatorów obiektów

bezpieczny kontekst wyrażenia inicjatora obiektów jest najbardziej ograniczony.

  1. bezpieczny kontekst wywołania konstruktora.
  2. bezpieczny kontekst i kontekst bezpieczny ref dla argumentów do indeksatorów inicjujących członków, które mogą wyciekać do odbiornika.
  3. bezpieczny kontekst RHS przypisań w inicjatorach składowych do nieczytanych zestawów lub ref-safe-context w przypadku przypisania odwołania.

Innym sposobem modelowania tego jest myślenie o każdym argumencie inicjalizatora składowego, który można przypisać do odbiornika, jako o argumencie konstruktora. Jest to spowodowane tym, że inicjator składowy jest efektywnie wywołaniem konstruktora.

Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
    Field = stackSpan;
}

// Can be modeled as 
var x = new S(ref heapSpan, stackSpan);

Modelowanie jest ważne, ponieważ pokazuje, że nasze MAMM muszą być uwzględniane w szczególny sposób dla inicjatorów składowych. Należy wziąć pod uwagę, że ten konkretny przypadek powinien być nielegalny, ponieważ umożliwia przypisanie wartości z węższym bezpiecznym kontekstem do szerszego.

Argumenty metody muszą być zgodne

Obecność pól ref oznacza, że reguły dotyczące argumentów metody muszą zostać zaktualizowane, ponieważ parametr ref może być teraz przechowywany jako pole w argumencie ref struct metody. Wcześniej reguła musiała rozważyć tylko inne ref struct przechowywane jako pole. Wpływ tego omówiono w kwestie zgodności. Nowa reguła to ...

W przypadku dowolnego wywołania metody e.M(a1, a2, ... aN)

  1. Oblicz najwęższy bezpieczny kontekst z:
    • kontekstu wywołującego
    • bezpieczny kontekst wszystkich argumentów
    • ref-safe-context wszystkich argumentów ref, których odpowiednie parametry mają ref-safe-contextkontekstu wywołującego
  2. Wszystkie argumenty typu ref o charakterystyce ref struct muszą być możliwe do przypisania wartości z tego bezpiecznego kontekstu. Jest to przypadek, w którym ref nie uogólniać, aby uwzględnić in i out

W przypadku dowolnego wywołania metody e.M(a1, a2, ... aN)

  1. Oblicz najwęższy bezpieczny kontekst z:
    • kontekstu wywołującego
    • bezpieczny kontekst wszystkich argumentów
    • Kontekst ref-safe-context wszystkich argumentów ref, dla których odpowiadające im parametry nie są scoped
  2. Wszystkie argumenty typu out o charakterystyce ref struct muszą być możliwe do przypisania wartości z tego bezpiecznego kontekstu.

Obecność scoped pozwala deweloperom zmniejszyć tarcie tworzone przez tę regułę, oznaczając parametry, które nie są zwracane, jako scoped. Spowoduje to usunięcie ich parametrów z (1) w obu powyższych przypadkach i zapewni większą elastyczność dzwoniących.

Wpływ tej zmiany omówiono bardziej szczegółowo poniżej. Ogólnie rzecz biorąc, pozwoli to deweloperom na bardziej elastyczne tworzenie miejsc wywołań poprzez adnotację wartości przypominających odwołania, które nie uciekają, przy użyciu scoped.

Wariancja zakresu parametrów

Modyfikator scoped i atrybut [UnscopedRef] (zobacz poniżej) również ma wpływ na nasze zastępowanie obiektów, implementację interfejsu i reguły konwersji delegate. Sygnatura zastąpienia, implementacji interfejsu lub konwersji delegate może:

  • Dodawanie scoped do parametru ref lub in
  • Dodawanie scoped do parametru ref struct
  • Usuwanie [UnscopedRef] z parametru out
  • Usuń [UnscopedRef] z parametru ref typu ref struct

Wszelkie inne różnice w odniesieniu do scoped lub [UnscopedRef] są uznawane za niezgodność.

Kompilator zgłosi diagnostykę niebezpiecznych niezgodności w zakresie między przesłonięciami, implementacjami interfejsu i konwersjami delegowanymi w następujących przypadkach:

  • Metoda ma parametr ref lub out typu ref struct z niezgodnością przy dodawaniu [UnscopedRef] (nie usuwając scoped). (W tym przypadku możliwe jest bezsensowne cykliczne przypisanie, dlatego nie są potrzebne żadne inne parametry).
  • Albo oba te elementy są prawdziwe:
    • Metoda zwraca ref struct lub zwraca ref lub ref readonlyalbo metoda ma parametr ref lub out typu ref struct.
    • Metoda ma co najmniej jeden dodatkowy parametr ref, inlub out albo parametr typu ref struct.

Diagnostyka nie jest zgłaszana w innych przypadkach, ponieważ:

  • Metody z takimi podpisami nie mogą przechwytywać przekazanych odwołań, więc niezgodności związane z zakresem nie są niebezpieczne.
  • Obejmują one bardzo typowe i proste scenariusze (np. zwykłe, stare parametry out używane w sygnaturach metod TryParse). Raportowanie rozbieżności w zakresie tylko dlatego, że są one używane w wersji 11 języka (i w związku z tym parametr out ma inny zakres) byłoby mylące.

Diagnostyka jest zgłaszana jako błąd , jeśli niezgodne podpisy używają reguł bezpiecznego kontekstu ref w języku C#11. W przeciwnym razie diagnostyka jest ostrzeżeniem .

Ostrzeżenie dotyczące niezgodności zakresowej może być zgłaszane w module skompilowanym przy użyciu reguł kontekstu bezpiecznego dla ref w języku C#7.2, gdzie scoped jest niedostępne. W niektórych przypadkach może być konieczne pominięcie ostrzeżenia, jeśli nie można zmodyfikować innego niezgodnego podpisu.

Modyfikator scoped i atrybut [UnscopedRef] mają również następujący wpływ na podpisy metody:

  • Modyfikator scoped i atrybut [UnscopedRef] nie mają wpływu na ukrywanie
  • Przeciążenia nie mogą się różnić tylko w przypadku scoped lub [UnscopedRef]

Sekcja dotycząca pola ref i scoped jest długa, dlatego warto zakończyć ją krótkim podsumowaniem proponowanych zmian powodujących niezgodność.

  • Wartość, która ma ref-safe-context w kontekście wywołującego , może być zwrócona przez pole ref lub ref.
  • Parametr out będzie miał bezpieczny kontekstskładowych funkcji .

Szczegółowe uwagi:

  • Pole ref można zadeklarować tylko wewnątrz ref struct
  • Pole ref nie można zadeklarować jako static, volatile lub const
  • Pole ref nie może mieć typu, który jest ref struct
  • Proces generowania zestawu odniesienia musi zachować obecność pola ref wewnątrz ref struct
  • readonly ref struct musi zadeklarować swoje pola ref jako readonly ref
  • W przypadku wartości by-ref modyfikator scoped musi pojawić się przed in, outlub ref
  • Dokument dotyczący zasad bezpieczeństwa dla zakresu zostanie zaktualizowany zgodnie z opisem w tym dokumencie
  • Nowe reguły bezpiecznego kontekstu "ref" będą obowiązywać, gdy
    • Biblioteka podstawowa zawiera flagę funkcji wskazującą obsługę pól ref
    • Wartość langversion wynosi 11 lub więcej

Składnia

13.6.2 Deklaracje zmiennych lokalnych: dodano 'scoped'?.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 Instrukcja for: dodano pośrednio 'scoped'? z local_variable_declaration.

13.9.5 Instrukcja foreach: dodano 'scoped'?.

foreach_statement
    : 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
      embedded_statement
    ;

12.6.2 Listy argumentów: dodano 'scoped'? dla deklaracji zmiennej out.

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' ('scoped'? local_variable_type)? identifier
    ;

12.7 wyrażenia dekonstrukcji:

[TBD]

15.6.2 Parametry metody: dodano 'scoped'? do parameter_modifier.

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

parameter_modifier
    | 'this' 'scoped'? parameter_mode_modifier?
    | 'scoped' parameter_mode_modifier?
    | parameter_mode_modifier
    ;

parameter_mode_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

20.2 Deklaracje delegatów: dodano 'scoped'?pośrednio z fixed_parameter.

12.19 Anonimowe wyrażenia funkcji: dodano 'scoped'?.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

Typy z ograniczeniami o zachodzie słońca

Kompilator ma koncepcję zestawu "typów ograniczonych", który jest w dużej mierze nieudokumentowany. Te typy miały specjalny status, ponieważ w języku C# 1.0 nie było ogólnego sposobu, aby wyrazić ich zachowanie. W szczególności fakt, że typy mogą zawierać odwołania do stosu wywołań. Zamiast tego kompilator miał specjalną wiedzę na ich temat i ograniczył ich użycie do sposobów, które zawsze byłyby bezpieczne: niedozwolone instrukcje zwrotu, nie mogą być używane jako elementy tablic, nie mogą być używane w kontekstach ogólnych itp.

Gdy pola ref są dostępne i rozszerzone do obsługi ref struct te typy można poprawnie zdefiniować w języku C#, używając kombinacji pól ref struct i ref. W związku z tym gdy kompilator wykryje, że środowisko uruchomieniowe obsługuje ref pola, nie będzie już mieć pojęcia typów ograniczonych. Zamiast tego użyje typów zdefiniowanych w kodzie.

Nasze reguły bezpiecznego kontekstu ref zostaną zaktualizowane w następujący sposób, aby to wspierać:

  • __makeref będzie traktowana jako metoda z sygnaturą static TypedReference __makeref<T>(ref T value)
  • __refvalue będzie traktowana jako metoda z podpisem static ref T __refvalue<T>(TypedReference tr). Wyrażenie __refvalue(tr, int) będzie efektywnie używać drugiego argumentu jako parametru typu.
  • jako parametr będzie miał kontekstu bezpiecznego i bezpiecznego kontekstu składowych funkcji.
  • jako wyrażenie będzie miało bezpiecznego kontekstu i bezpiecznego kontekstu składowych funkcji.

Zgodne środowiska uruchomieniowe zapewnią, że TypedReference, RuntimeArgumentHandle i ArgIterator są zdefiniowane jako ref struct. Dalsze TypedReference należy wyświetlić jako pole ref do ref struct dla dowolnego możliwego typu (może przechowywać dowolną wartość). W połączeniu z powyższymi zasadami zapewnia, że odwołania do stosu nie wydostaną się poza ich czas życia.

Uwaga: ściśle mówiąc, są to szczegóły implementacji kompilatora, a nie część języka. Jednak zważywszy na relacje z polami ref, uwzględnia się go w propozycji języka dla uproszczenia.

Podaj niezakresowy

Jednym z najbardziej godnych uwagi punktów tarcia jest niezdolność do zwracania pól przez ref w przypadku elementów członkowskich struct. Oznacza to, że deweloperzy nie mogą tworzyć metod/właściwości zwracających wartość ref i muszą sięgnąć po bezpośrednie uwidacznianie pól. To zmniejsza użyteczność zwrotów ref w struct, gdzie są one często najbardziej pożądane.

struct S
{
    int _field;

    // Error: this, and hence _field, can't return by ref
    public ref int Prop => ref _field;
}

Uzasadnienie dla tego domyślnego jest uzasadnione, ale z założenia nie ma nic złego w przypadku struct ucieczki this przez odwołanie, jest to po prostu ustawienie domyślne wybrane przez reguły bezpiecznego kontekstu ref.

Aby rozwiązać ten problem, język programowania zapewni przeciwieństwo adnotacji okresu istnienia scoped poprzez wsparcie dla UnscopedRefAttribute. Można to zastosować do dowolnego i zmieni bezpieczny kontekst jako jeden poziom szerszy niż domyślny. Na przykład:

UnscopedRef zastosowane do Oryginalny kontekst ref-safe-context Nowe ref-safe-context
członek instancji składowa funkcji wyłącznie do zwrotu
parametr in / ref wyłącznie do zwrotu kontekst wywołujący
parametr out składowa funkcji wyłącznie do zwrotu

Zastosowanie [UnscopedRef] do metody instancji struct ma wpływ na modyfikację niejawnego parametru this. Oznacza to, że this działa jako nieoznaczony element ref tego samego rodzaju.

struct S
{
    int field; 

    // Error: `field` has the ref-safe-context of `this` which is *function-member* because 
    // it is a `scoped ref`
    ref int Prop1 => ref field;

    // Okay: `field` has the ref-safe-context of `this` which is *caller-context* because 
    // it is a `ref`
    [UnscopedRef] ref int Prop1 => ref field;
}

Adnotację można również umieścić na parametrach out, aby przywrócić je do zachowania języka C# 10.

ref int SneakyOut([UnscopedRef] out int i)
{
    i = 42;
    return ref i;
}

Dla reguł bezpiecznego kontekstu ref, taki [UnscopedRef] out jest traktowany po prostu jako ref. Podobnie jak in jest uznawany za ref w celach życiowych.

Adnotacja [UnscopedRef] będzie niedozwolona dla członków init i konstruktorów wewnątrz struct. Członkowie ci są już wyjątkowi w odniesieniu do semantyki ref, ponieważ postrzegają członków readonly jako zmiennych. Oznacza to, że przekazanie ref tym członkom wydaje się prostym ref, a nie ref readonly. Jest to dozwolone w granicach konstruktorów i init. Zezwolenie [UnscopedRef] pozwoliłoby takie ref na niepoprawne opuszczenie poza konstruktor i zezwolić na mutację po tym, jak semantyka readonly miała miejsce.

Typ atrybutu będzie miał następującą definicję:

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(
        AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class UnscopedRefAttribute : Attribute
    {
    }
}

Szczegółowe uwagi:

  • Metoda wystąpienia lub właściwość z adnotacjami z [UnscopedRef] ma ref-safe-contextthis ustawioną na kontekstu wywołującego.
  • Członek z adnotacją [UnscopedRef] nie może zaimplementować interfejsu.
  • Jest to błąd podczas używania [UnscopedRef]
    • Członek, który nie jest zadeklarowany w struct
    • Członek static, członek init lub konstruktor w struct
    • Parametr oznaczony scoped
    • Parametr przekazany przez wartość
    • Parametr przekazywany jako referencja, który nie jest niejawnie zadeklarowany

ScopedRefAttribute

Adnotacje scoped zostaną zapisywane w metadanych za pośrednictwem atrybutu typu System.Runtime.CompilerServices.ScopedRefAttribute. Atrybut będzie zgodny z nazwą kwalifikowaną przestrzeni nazw, więc definicja nie musi być wyświetlana w żadnym konkretnym zestawie.

Typ ScopedRefAttribute jest przeznaczony tylko dla kompilatora — nie jest dozwolony w źródle. Deklaracja typu jest syntetyzowana przez kompilator, jeśli nie została jeszcze uwzględniona w kompilacji.

Typ będzie miał następującą definicję:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class ScopedRefAttribute : Attribute
    {
    }
}

Kompilator emituje ten atrybut na parametrze przy użyciu składni scoped. Będzie to emitowane tylko wtedy, gdy składnia powoduje, że wartość różni się od stanu domyślnego. Na przykład scoped out nie spowoduje emitowania atrybutu.

RefSafetyRulesAttribute

Istnieje kilka różnic w reguły bezpiecznego kontekstu ref między C#7.2 i C#11. Każda z tych różnic może spowodować istotne zmiany podczas ponownej kompilacji przy użyciu C#11 względem odwołań skompilowanych przy użyciu C#10 lub wcześniejszej wersji.

  1. Niezakresowe parametry ref/in/out mogą wyjść poza wywołanie metody jako pole ref obiektu ref struct w języku C#11, co nie jest możliwe w C#7.2.
  2. parametry out są niejawnie ograniczone w języku C#11 i niezakresowe w języku C#7.2
  3. ref / in parametry do typów ref struct posiadają domyślne zakresy w C#11, a są bez zakresu w C#7.2

Aby zmniejszyć prawdopodobieństwo zmiany powodującej niezgodność podczas ponownego kompilowania przy użyciu języka C#11, zaktualizujemy kompilator języka C#11, aby użyć reguł bezpiecznego kontekstu ref wywołania metody, które zgodne z regułami używanymi do analizowania deklaracji metody. Zasadniczo podczas analizowania wywołania metody skompilowanej przy użyciu starszego kompilatora kompilator języka C#11 będzie używać reguł bezpiecznego kontekstu C#7.2.

Aby to włączyć, kompilator wyemituje nowy atrybut [module: RefSafetyRules(11)], gdy moduł zostanie skompilowany z użyciem -langversion:11 lub nowszej wersji albo skompilowany z corlibem zawierającym flagę funkcji dla pól ref.

Argument atrybutu wskazuje wersję języka ref safe context reguł używanych podczas kompilowania modułu. Wersja jest obecnie stała w 11 niezależnie od rzeczywistej wersji językowej przekazanej do kompilatora.

Oczekuje się, że przyszłe wersje kompilatora uaktualnią zasady bezpiecznego kontekstu ref i wygenerują atrybuty z różnymi wersjami.

Jeśli kompilator ładuje moduł zawierający [module: RefSafetyRules(version)]z version innym niż 11, kompilator zgłosi ostrzeżenie dla nierozpoznanej wersji, jeśli istnieją wywołania metod zadeklarowanych w tym module.

Gdy kompilator języka C#11 analizuje wywołanie metody:

  • Jeśli moduł zawierający deklarację metody zawiera [module: RefSafetyRules(version)], niezależnie od version, wywołanie metody jest analizowane przy użyciu reguł języka C#11.
  • Jeśli moduł zawierający deklarację metody pochodzi ze źródła i skompilowany przy użyciu -langversion:11 lub z corlibem zawierającym flagę funkcji dla pól ref, wywołanie metody jest analizowane przy użyciu reguł języka C#11.
  • Jeśli moduł zawierający deklarację metody odwołuje się do System.Runtime { ver: 7.0 }, wywołanie metody jest analizowane przy użyciu reguł języka C#11. Ta reguła jest tymczasowym ograniczeniem ryzyka dla modułów skompilowanych we wcześniejszych wersjach zapoznawczych języka C#11 / .NET 7 i zostanie usunięta później.
  • W przeciwnym razie wywołanie metody jest analizowane przy użyciu reguł języka C#7.2.

Kompilator pre-C#11 zignoruje wszystkie RefSafetyRulesAttribute i przeanalizuje wywołania metod tylko przy użyciu reguł języka C#7.2.

RefSafetyRulesAttribute będzie zgodna z nazwą kwalifikowaną przestrzeni nazw, więc definicja nie musi być wyświetlana w żadnym konkretnym zestawie.

Typ RefSafetyRulesAttribute jest przeznaczony tylko dla kompilatora — nie jest dozwolony w źródle. Deklaracja typu jest syntetyzowana przez kompilator, jeśli nie została jeszcze uwzględniona w kompilacji.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
    internal sealed class RefSafetyRulesAttribute : Attribute
    {
        public RefSafetyRulesAttribute(int version) { Version = version; }
        public readonly int Version;
    }
}

Bufory o stałym rozmiarze zaprojektowane z myślą o bezpieczeństwie

Bezpieczne bufory o stałym rozmiarze nie zostały dostarczone w C# 11. Tę funkcję można zaimplementować w przyszłej wersji języka C#.

Język złagodzi ograniczenia dotyczące tablic o stałym rozmiarze, tak aby można je było zadeklarować w bezpiecznym kodzie, a typ elementu może być zarządzany lub niezarządzany. Spowoduje to, że następujące typy staną się legalne:

internal struct CharBuffer
{
    internal char Data[128];
}

Deklaracje te, podobnie jak ich odpowiedniki unsafe, definiują sekwencję elementów N w zawierającym typie. Dostęp do tych członków można uzyskać za pomocą indeksatora, a także można je przekonwertować na wystąpienia Span<T> i ReadOnlySpan<T>.

Podczas indeksowania w buforze fixed typu T należy wziąć pod uwagę stan readonly kontenera. Jeśli kontener jest readonly, indeksator zwraca ref readonly T w przeciwnym razie zwraca ref T.

Uzyskiwanie dostępu do buforu fixed bez indeksatora nie ma typu naturalnego, jest jednak możliwe do konwersji na typy Span<T>. W przypadku, gdy kontener jest readonly, bufor jest niejawnie konwertowany na ReadOnlySpan<T>, w przeciwnym razie może być niejawnie konwertowany na Span<T> lub ReadOnlySpan<T> (konwersja Span<T> jest uważana za lepszą).

Wynikowa instancja Span<T> będzie miała długość równą rozmiarowi zadeklarowanemu na buforze fixed. bezpieczny kontekst zwróconej wartości będzie równy bezpiecznemu kontekstowi kontenera, tak jak gdyby dostęp do danych źródłowych był uzyskiwany jako pole.

Dla każdej deklaracji fixed w typie z elementem T, język wygeneruje odpowiadającą metodę indeksatora tylko get, którego typ zwracany jest ref T. Indeksator zostanie oznaczony adnotacją z atrybutem [UnscopedRef], ponieważ implementacja będzie zwracać pola typu deklaratywnego. Dostępność członka będzie zgodna z dostępnością w polu fixed.

Na przykład podpis indeksatora dla CharBuffer.Data będzie następujący:

[UnscopedRef] internal ref char DataIndexer(int index) => ...;

Jeśli podany indeks znajduje się poza zadeklarowaną granicą tablicy fixed, zostanie zgłoszony IndexOutOfRangeException. W przypadku podania stałej wartości zostanie zastąpiona bezpośrednim odwołaniem do odpowiedniego elementu. Jeśli stała nie znajduje się poza zadeklarowaną granicą, w tym przypadku wystąpi błąd czasu kompilacji.

Dla każdego buforu fixed zostanie również wygenerowany nazwany akcesor, który umożliwia wykonywanie operacji get i set przez wartość. Oznacza to, że fixed będą bardziej przypominać istniejącą semantykę tablic dzięki użyciu akcesora ref oraz operacjom przeprowadzanym przez wartość get i set. Oznacza to, że kompilatory będą miały taką samą elastyczność podczas emitowania kodu korzystającego z buforów fixed, jak podczas korzystania z tablic. To powinno ułatwić emitowanie operacji, takich jak await na fixed bufory.

Ma to również dodatkową korzyść, co sprawi, że bufory fixed będą łatwiejsze do wykorzystania w innych językach. Nazwane indeksatory to funkcja, która istniała od wersji 1.0 platformy .NET. Nawet języki, które nie mogą bezpośrednio emitować nazwanego indeksatora, mogą z nich ogólnie korzystać (C# jest dobrym przykładem tego).

Magazyn zapasowy buforu zostanie wygenerowany przy użyciu atrybutu [InlineArray]. Jest to mechanizm omówiony w problem 12320, który pozwala w szczególności na efektywne deklarowanie sekwencji pól tego samego typu. Ten konkretny problem jest nadal przedmiotem aktywnej dyskusji i oczekuje się, że implementacja tej funkcji będzie zależeć od wyników tej dyskusji.

Inicjatory z wartościami ref w wyrażeniach new i with

W sekcji 12.8.17.3 inicjatory obiektów, aktualizujemy gramatykę do:

initializer_value
    : 'ref' expression // added
    | expression
    | object_or_collection_initializer
    ;

W sekcji dla wyrażenia withzaktualizujemy gramatykę na:

member_initializer
    : identifier '=' 'ref' expression // added
    | identifier '=' expression
    ;

Lewy operand przypisania musi być wyrażeniem, które wiąże się z polem ref.
Prawy operand musi być wyrażeniem, które zwraca wartość lvalue określającą wartość tego samego typu co lewy operand.

Dodajemy podobną regułę do ref lokalne przypisanie:
Jeśli lewy operand jest zapisywalnym ref (tj. wyznacza coś innego niż pole ref readonly), prawy operand musi być zapisywalną wartością lvalue.

Reguły ucieczki dla wywołań konstruktora pozostają:

Wyrażenie new, które wywołuje konstruktor, przestrzega tych samych reguł co wywołanie metody, które zwraca konstruowany typ.

Mianowicie reguły wywołania metody zaktualizowane powyżej:

Wartość rvalue wynikająca z wywołania metody e1.M(e2, ...) ma bezpieczny kontekst z najmniejszego z następujących kontekstów:

  1. kontekst wywołujący
  2. bezpieczny kontekst dostarczony przez wszystkie wyrażenia argumentów
  3. Gdy zwrot jest ref struct, to kontekst ref-safe jest wynikiem wkładu wszystkich argumentów ref.

W przypadku wyrażenia new z inicjatorami, wyrażenia inicjalizujące są liczone jako argumenty (przyczyniają się do swojego bezpiecznego kontekstu) i wyrażenia inicjalizujące ref są liczone jako argumenty ref (przyczyniają się one do swojego ref-safe-context), rekursywnie.

Zmiany w niebezpiecznym kontekście

Typy wskaźników (sekcja23.3) zostały rozszerzone, aby umożliwiać wykorzystywanie typów zarządzanych jako typów odniesienia. Takie typy wskaźników są zapisywane jako typ zarządzany, po którym następuje token *. Generują ostrzeżenie.

Operator address-of ( sekcja23.6.5) jest zrelaksowany do akceptowania zmiennej o typie zarządzanym jako operand.

Instrukcja fixed (sekcja23.7) została złagodzona, aby akceptować fixed_pointer_initializer, który jest adresem zmiennej typu zarządzanego T lub wyrażeniem typu array_type z elementami typu zarządzanego T.

Inicjalizator alokacji stosu (sekcja12.8.22) jest podobnie złagodzony.

Zagadnienia dotyczące

Inne elementy stosu rozwojowego powinny być brane pod uwagę podczas oceniania tej funkcji.

Zagadnienia dotyczące zgodności

Wyzwaniem w tym wniosku jest wpływ zgodności tego projektu na nasze istniejące obejmują zasady bezpieczeństwa, lub §9.7.2. Podczas gdy te reguły w pełni wspierają koncepcję ref struct mającego pola ref, nie pozwalają one na to, aby interfejsy API, z wyjątkiem stackalloc, przechwytywały stan ref odnoszący się do stosu. Reguły kontekstu bezpiecznego ref mają twarde założenielub §16.4.12.8, że konstruktor formularza Span(ref T value) nie istnieje. Oznacza to, że reguły bezpieczeństwa nie uwzględniają parametru ref, który może uciec jako pole ref, dlatego umożliwia kod podobny do poniższego.

Span<int> CreateSpanOfInt()
{
    // This is legal according to the 7.2 span rules because they do not account
    // for a constructor in the form Span(ref T value) existing. 
    int local = 42;
    return new Span<int>(ref local);
}

W rzeczywistości istnieją trzy sposoby, aby parametr ref uciekł z wywołania metody:

  1. Według wartości zwracanej
  2. Do ref powrócić
  3. Na podstawie pola ref w ref struct, które jest zwracane lub przekazywane jako parametr ref / out

Istniejące reguły uwzględniają tylko (1) i (2). Nie uwzględniają one (3), dlatego luki, takie jak zwracanie zmiennych lokalnych jako pola ref, nie są brane pod uwagę. Ten projekt musi zmienić zasady, aby uwzględnić (3). Będzie to miało niewielki wpływ na zgodność z istniejącymi interfejsami API. W szczególności będzie to miało wpływ na interfejsy API, które mają następujące właściwości.

  • Posiadanie ref struct w podpisie
    • Gdzie ref struct jest typem zwracanym, ref lub out parametru
    • Z wyłączeniem odbiornika, ma dodatkowy parametr in lub ref.

W języku C# 10 osoby wywołujące takie interfejsy API nigdy nie musiały brać pod uwagę, że wejście do stanu ref w interfejsie API mogło zostać przechwycone jako pole ref. To pozwala na istnienie kilku wzorców, które są bezpieczne w C# 10, ale staną się niebezpieczne w C# 11 ze względu na możliwość ucieczki stanu ref jako pole ref. Na przykład:

Span<int> CreateSpan(ref int parameter)
{
    // The implementation of this method is irrelevant when considering the lifetime of the 
    // returned Span<T>. The ref safe context rules only look at the method signature, not the 
    // implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
    // to escape by ref in this method
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 but would be illegal with ref fields
    return CreateSpan(ref parameter);

    // Legal in C# 10 but would be illegal with ref fields
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 but would be illegal with ref fields
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

Oczekuje się, że wpływ tej przerwy zgodności będzie bardzo mały. Kształt interfejsu API, na który ma wpływ, nie ma sensu w przypadku braku pól ref, dlatego jest mało prawdopodobne, aby klienci utworzyli wiele z nich. Eksperymenty uruchamiające narzędzia do odnajdywania tej struktury API w istniejących repozytoriach potwierdzają tę tezę. Jedynym repozytorium z jakąkolwiek znaczącą liczbą tego kształtu jest dotnet/runtime, a to dlatego, że to repozytorium może tworzyć pola ref za pośrednictwem typu wbudowanego ByReference<T>.

Mimo to projekt musi uwzględniać istnienie takich interfejsów API, ponieważ wyrażają one prawidłowy wzorzec, po prostu nieczęsto spotykany. Dlatego projekt musi dostarczyć deweloperom narzędzi do przywrócenia istniejących reguł cyklu życia przy aktualizacji do C# 10. W szczególności musi zawierać mechanizmy, które umożliwiają deweloperom dodawanie adnotacji do parametrów ref, ponieważ nie mogą uciec przez pole ref lub ref. Dzięki niemu klienci mogą definiować interfejsy API w języku C# 11, które mają te same reguły miejsca wywołania w języku C# 10.

Zestawy odwołań

Zestaw referencyjny do kompilacji z użyciem funkcji opisanych w tej propozycji musi utrzymywać elementy, które zawierają informacje o bezpiecznym kontekście "ref". Oznacza to, że wszystkie atrybuty adnotacji dotyczących okresu istnienia muszą być zachowane w ich pierwotnej pozycji. Każda próba zastąpienia lub pominięcia ich może prowadzić do nieprawidłowych zestawów odwołań.

Reprezentacja pól ref jest bardziej zniuansowana. Najlepiej, aby pole ref było wyświetlane w zestawie odniesienia, tak jak w przypadku każdego innego pola. Jednak pole ref reprezentuje zmianę formatu metadanych i może to powodować problemy z łańcuchami narzędzi, które nie są aktualizowane, aby zrozumieć tę zmianę metadanych. Konkretny przykład to C++/CLI, który prawdopodobnie zgłosi błąd, jeśli przetwarza pole ref. W związku z tym jest to korzystne, jeśli ref pola można pominąć z zestawów odwołań w naszych podstawowych bibliotekach.

Samo pole ref nie ma wpływu na reguły bezpiecznego kontekstu ref. Jako konkretny przykład należy wziąć pod uwagę, że przerzucanie istniejącej definicji Span<T> do używania pola ref nie ma wpływu na zużycie. W związku z tym ref można bezpiecznie pominąć. Jednak pole ref ma inne skutki dla zużycia, które muszą zostać zachowane.

  • ref struct, który ma pole ref, nigdy nie jest uznawany za unmanaged
  • Typ pola ref ma wpływ na nieskończone ogólne reguły rozszerzania. W związku z tym, jeśli typ pola ref zawiera parametr typu, który musi zostać zachowany

Biorąc pod uwagę te reguły, jest to prawidłowa transformacja zestawu referencyjnego dla ref struct:

// Impl assembly 
ref struct S<T>
{
    ref T _field;
}

// Ref assembly 
ref struct S<T>
{
    object _o; // force managed 
    T _f; // maintain generic expansion protections
}

Adnotacje

Okresy istnienia są najbardziej naturalnie wyrażane przy użyciu typów. Bezpieczeństwo okresów istnienia danego programu jest zapewnione, gdy sprawdzanie zgodności typów okresów istnienia jest zakończone powodzeniem. Chociaż składnia języka C# niejawnie dodaje okresy istnienia do wartości, istnieje podstawowy system typów, który opisuje tutaj podstawowe reguły. Często łatwiej jest omówić implikację zmian w projekcie pod względem tych zasad, więc są one zawarte tutaj w celu dyskusji.

Należy pamiętać, że nie jest to 100% kompletna dokumentacja. Dokumentowanie każdego pojedynczego zachowania nie jest tutaj celem. Zamiast tego ma to na celu ustalenie ogólnego zrozumienia i wspólnego czasownika, za pomocą którego można omówić model i potencjalne zmiany.

Zazwyczaj nie trzeba bezpośrednio mówić o typach okresu istnienia. Wyjątki to miejsca, gdzie okresy istnienia mogą się różnić w zależności od konkretnych miejsc "instancji". Jest to rodzaj polimorfizmu i nazywamy te różne okresy istnienia "ogólne okresy istnienia", reprezentowane jako parametry ogólne. Język C# nie udostępnia składni dla ogólnych parametrów dotyczących czasu życia, dlatego definiujemy niejawną translację z języka C# na rozszerzony język niższego poziomu, który zawiera jawne parametry ogólne.

W poniższych przykładach użyto nazwanych cykli życia. Składnia $a odnosi się do okresu życia o nazwie a. Jest to okres istnienia, który sam nie ma znaczenia, ale może mieć relację z innymi okresami istnienia za pośrednictwem składni where $a : $b. Określa to, że $a jest konwertowany na $b. Może być pomocne myślenie o tym jako o określeniu, że $a jest okresem istnienia co najmniej tak długim, jak $b.

Istnieje kilka wstępnie zdefiniowanych okresów istnienia dla wygody i zwięzłości poniżej:

  • $heap: to jest okres trwania każdej wartości znajdującej się w stercie. Jest ona dostępna we wszystkich kontekstach i sygnaturach metod.
  • $local: jest to okres istnienia dowolnej wartości, która istnieje na stosie metody. Jest to w rzeczywistości symbol zastępczy nazwy dla członka funkcji. Jest ona niejawnie zdefiniowana w metodach i może być wyświetlana w podpisach metod z wyjątkiem dowolnej pozycji wyjściowej.
  • $ro: symbol zastępczy nazwy dla zwraca tylko
  • $cm: identyfikator zastępczy dla kontekstu wywołującego

Istnieje kilka wstępnie zdefiniowanych relacji między okresami istnienia:

  • where $heap : $a dla wszystkich okresów żywotności $a
  • where $cm : $ro
  • where $x : $local dla wszystkich wstępnie zdefiniowanych okresów istnienia. Okresy istnienia zdefiniowane przez użytkownika nie mają związku z lokalnymi, chyba że zostaną one jawnie określone.

Zmienne okresu istnienia zdefiniowane dla typów mogą być niezmienne lub kowariantne. Są one wyrażane przy użyciu tej samej składni co parametry ogólne:

// $this is covariant
// $a is invariant
ref struct S<out $this, $a> 

Parametr okresu istnienia $this definicji typów jest nie wstępnie zdefiniowany, ale ma kilka skojarzonych z nim reguł:

  • Musi to być pierwszy parametr okresu istnienia.
  • Musi być kowariantny: out $this.
  • Okres istnienia pól ref musi być konwertowany na $this
  • Okres istnienia $this wszystkich pól innych niż ref musi być $heap lub $this.

Czas życia odwołania jest wyrażany przez podanie argumentu czasu życia do odwołania. Na przykład ref, które odnosi się do sterty, jest przedstawiane jako ref<$heap>.

Podczas definiowania konstruktora w modelu nazwa new będzie używana dla metody . Konieczne jest posiadanie listy parametrów dla zwróconej wartości, a także argumentów konstruktora. Jest to konieczne, aby wyrazić relację między danymi wejściowymi konstruktora a skonstruowaną wartością. Zamiast Span<$a><$ro> model będzie używać Span<$a> new<$ro>. Typ this w konstruktorze, łącznie z okresami istnienia, będzie stanowił zdefiniowaną wartość zwracaną.

Podstawowe zasady okresu trwałości są definiowane jako:

  • Wszystkie okresy istnienia są wyrażane składniowo jako argumenty ogólne, pochodzące przed argumentami typu. Dotyczy to wstępnie zdefiniowanych okresów istnienia z wyjątkiem $heap i $local.
  • Wszystkie typy T, które nie są ref struct, niejawnie mają okres istnienia T<$heap>. Jest to niejawne, nie ma potrzeby pisania int<$heap> w każdym przykładzie.
  • W przypadku pola ref zdefiniowanego jako ref<$l0> T<$l1, $l2, ... $ln>:
    • Wszystkie okresy istnienia $l1 do $ln należy pozostawić niezmiennymi.
    • Okres istnienia $l0 musi być konwertowany na $this
  • Dla ref zdefiniowanego jako ref<$a> T<$b, ...>, $b musi być konwertowane na $a
  • ref zmiennej ma okres istnienia zdefiniowany przez:
    • W przypadku ref lokalnego, parametru, pola lub zwrotu typu ref<$a> T czas życia to $a
    • $heap dla wszystkich typów odwołań i typów pól referencyjnych
    • $local dla wszystkiego innego
  • Przypisanie lub zwrot jest legalne, gdy konwersja typu bazowego jest legalna
  • Czas życia wyrażeń można uczynić bardziej przejrzystym, stosując adnotacje rzutowania.
    • (T<$a> expr) okres istnienia wartości jest jawnie $a dla T<...>
    • ref<$a> (T<$b>)expr okres istnienia wartości wynosi $b dla T<...>, a okres istnienia referencji wynosi $a.

W przypadku reguł okresu istnienia ref jest traktowana jako część typu wyrażenia na potrzeby konwersji. Jest on logicznie reprezentowany przez konwersję ref<$a> T<...> na ref<$a, T<...>>, gdzie $a jest kowariantna i T jest niezmienna.

Następnie zdefiniujmy reguły, które umożliwiają mapowanie składni języka C# na bazowy model.

W celu zachowania zwięzłości, typ, który nie ma jawnych parametrów życia, jest traktowany tak, jakby out $this jest zdefiniowany i zastosowany do wszystkich pól typu. Typ z polem ref musi definiować jawne parametry okresu istnienia.

Te zasady istnieją, aby wspierać nasze istniejące założenie niezmienności, że T można przypisać do scoped T dla wszystkich typów. Odwzorowuje się to w ten sposób, że T<$a, ...> można przypisać do T<$local, ...> dla wszystkich okresów życia, które można przekonwertować na $local. Ponadto obsługuje to inne elementy, takie jak możliwość przypisania Span<T> z sterta do tych na stosie. Wyklucza to typy, w których pola mają różne okresy istnienia wartości innych niż ref, ale jest to rzeczywistość języka C#. Zmiana wymagałaby znacznej zmiany reguł języka C#, które musiałyby zostać opracowane.

Typ this dla typu S<out $this, ...> wewnątrz metody wystąpienia jest niejawnie zdefiniowany jako następujący:

  • W przypadku metody wystąpienia normalnego: ref<$local> S<$cm, ...>
  • Na przykład metoda oznaczona [UnscopedRef]: ref<$ro> S<$cm, ...>

Brak jawnego parametru this wymusza w tym miejscu niejawne reguły. W przypadku złożonych przykładów i dyskusji rozważ zastosowanie metody static i uczynienie z this jawnego parametru.

ref struct S<out $this>
{
    // Implicit this can make discussion confusing 
    void M<$ro, $cm>(ref<$ro> S<$cm> s) {  }

    // Rewrite as explicit this to simplify discussion
    static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}

Składnia metody języka C# odpowiada modelowi w następujący sposób:

  • parametry ref mają ref okres istnienia $ro
  • parametry typu ref struct mają taki czas trwania $cm
  • Zwroty referencyjne mają okres życia $ro
  • zwracane wartości typu ref struct mają okres istnienia wartości $ro
  • scoped w parametrze lub ref zmienia czas życia referencji na $local

Przyjrzyjmy się prostemu przykładzie, który demonstruje model.

ref int M1(ref int i) => ...

// Maps to the following. 

ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
    // okay: has ref lifetime $ro which is equal to $ro
    return ref i;

    // okay: has ref lifetime $heap which convertible $ro
    int[] array = new int[42];
    return ref array[0];

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Teraz przyjrzyjmy się temu samemu przykładowi przy użyciu ref struct:

ref struct S
{
    ref int Field;

    S(ref int f)
    {
        Field = ref f;
    }
}

S M2(ref int i, S span1, scoped S span2) => ...

// Maps to 

ref struct S<out $this>
{
    // Implicitly 
    ref<$this> int Field;

    S<$ro> new<$ro>(ref<$ro> int f)
    {
        Field = ref f;
    }
}

S<$ro> M2<$ro>(
    ref<$ro> int i,
    S<$ro> span1)
    S<$local> span2)
{
    // okay: types match exactly
    return span1;

    // error: has lifetime $local which has no conversion to $ro
    return span2;

    // okay: type S<$heap> has a conversion to S<$ro> because $heap has a
    // conversion to $ro and the first lifetime parameter of S<> is covariant
    return default(S<$heap>)

    // okay: the ref lifetime of ref $i is $ro so this is just an 
    // identity conversion
    S<$ro> local = new S<$ro>(ref $i);
    return local;

    int[] array = new int[42];
    // okay: S<$heap> is convertible to S<$ro>
    return new S<$heap>(ref<$heap> array[0]);

    // okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These 
    // are convertible.
    return new S<$ro>(ref<$heap> array[0]);

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Następnie zobaczmy, jak pomaga to w przypadku cyklicznego problemu z przypisywaniem własnym:

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        s.refField = ref s.field;
    }
}

// Maps to 

ref struct S<out $this>
{
    int field;
    ref<$this> int refField;

    static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
    {
        // error: the types work out here to ref<$cm> int = ref<$ro> int and that is 
        // illegal as $ro has no conversion to $cm (the relationship is the other direction)
        s.refField = ref<$ro> s.field;
    }
}

Następnie zobaczmy, jak to pomaga w przypadku problemu z głupim przechwytywaniem parametrów:

ref struct S
{
    ref int refField;

    void Use(ref int parameter)
    {
        // error: this needs to be an error else every call to this.Use(ref local) would fail 
        // because compiler would assume the `ref` was captured by ref.
        this.refField = ref parameter;
    }
}

// Maps to 

ref struct S<out $this>
{
    ref<$this> int refField;
    
    // Using static form of this method signature so the type of this is explicit. 
    static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
    {
        // error: the types here are:
        //  - refField is ref<$cm> int
        //  - ref parameter is ref<$ro> int
        // That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and 
        // hence this reassignment is illegal
        @this.refField = ref<$ro> parameter;
    }
}

Otwarte problemy

Zmień projekt, aby uniknąć przerw zgodności

Ten projekt proponuje kilka naruszeń zgodności z istniejącymi regułami ref-safe-context. Mimo że uważa się, że zmiany mają minimalny wpływ, znaczną uwagę poświęcono projektowi, który nie wprowadzał żadnych zmian powodujących niezgodności.

Projekt zachowujący kompatybilność był jednak znacznie bardziej złożony niż ten. Aby zachować pola ref zgodności, muszą mieć różne okresy istnienia, aby umożliwić zwracanie ich przez ref i zwracanie ich przez pole ref. Zasadniczo wymaga to dostarczenia ref-field-safe-context śledzenia wszystkich parametrów w metodzie. Należy to obliczyć dla wszystkich wyrażeń i śledzić we wszystkich wartościach praktycznie wszędzie tam, gdzie obecnie śledzony jest kontekst ref-safe-context.

Ponadto ta wartość ma relacje z ref-safe-context. Na przykład nie jest sensyczne, aby wartość mogła zostać zwrócona jako pole ref, ale nie bezpośrednio jako ref. Wynika to z faktu, że pola ref mogą być już trywialnie zwracane przez ref (stanref w ref struct może być zwracany przez ref nawet wtedy, gdy zawierająca wartość nie może). W związku z tym przepisy wymagają dalszego stałego dostosowania, aby zapewnić, że te wartości są rozsądne w odniesieniu do siebie nawzajem.

Oznacza to również, że język musi mieć składnię do reprezentowania parametrów ref, które mogą być zwracane na trzy różne sposoby: przez pole ref, przez ref i przez wartość. Wartość domyślna jest zwracana przez ref. W przyszłości jednak bardziej naturalnego powrotu, szczególnie gdy ref struct są zaangażowane, oczekuje się przez pole ref lub ref. Oznacza to, że nowe interfejsy API wymagają domyślnie dodatkowej adnotacji składniowej. Jest to niepożądane.

Te zmiany kompatybilności jednak będą miały wpływ na metody, które mają następujące właściwości:

  • Miej Span<T> albo ref struct
    • Gdzie ref struct jest typem zwracanym, ref lub out parametru
    • Ma dodatkowy parametr in lub ref (z wyłączeniem odbiornika)

Aby zrozumieć wpływ, warto podzielić interfejsy API na kategorie:

  1. Zależy Ci, aby użytkownicy uwzględniali ref jako pole ref. Typowy przykład to konstruktory Span(ref T value)
  2. Nie należy, aby konsumenci traktowali ref jako pole ref. Są one jednak podzielone na dwie kategorie
    1. Niebezpieczne interfejsy API. Są to interfejsy API wewnątrz typów Unsafe i MemoryMarshal, z których MemoryMarshal.CreateSpan jest najbardziej wyróżniający się. Te interfejsy API przechwytują ref niebezpiecznie, ale są one również znane jako niebezpieczne interfejsy API.
    2. Bezpieczne interfejsy API. Są to interfejsy API, które przyjmują parametry ref dla wydajności, ale w rzeczywistości nie są one nigdzie przechwytywane. Przykłady są małe, ale jeden z nich to AsnDecoder.ReadEnumeratedBytes

Ta zmiana przynosi przede wszystkim korzyści (1) powyżej. Oczekuje się, że stanowią one większość interfejsów API, które przyjmują ref i zwracają ref struct w przyszłości. Zmiany negatywnie wpływają na (2.1) i (2.2), ponieważ zmieniają się reguły okresu istnienia, co zakłóca istniejącą semantykę wywołań.

Interfejsy API w kategorii (2.1) są jednak w dużej mierze tworzone przez firmę Microsoft lub przez tych deweloperów, którzy odnoszą największe korzyści z pól ref (takich jak np. Tanner). Należy założyć, że ta klasa deweloperów byłaby skłonna do przyjęcia podatku kompatybilności przy uaktualnieniu do języka C# 11 w formie kilku adnotacji, aby zachować istniejącą semantykę, jeśli w zamian dostarczono by pola ref.

API w kategorii (2.2) są największym problemem. Nie wiadomo, ile takich interfejsów API istnieje i nie jest jasne, czy byłyby one częstsze lub rzadziej występowały w kodzie strony trzeciej. Oczekuje się, że ich liczba będzie bardzo mała, szczególnie jeśli weźmiemy pod uwagę przerwę zgodności przy out. Jak dotąd, wyszukiwania ujawniły bardzo małą liczbę tych obiektów na powierzchni public. Jest to trudny wzorzec do wyszukania, ponieważ wymaga analizy semantycznej. Przed wprowadzeniem tej zmiany potrzebne byłoby podejście oparte na narzędziach, aby zweryfikować założenia dotyczące wpływu na niewielką liczbę znanych przypadków.

W obu przypadkach wspomnianych w kategorii (2) poprawka jest prosta. Parametry ref, które nie chcą być uznawane za przechwytywalne, muszą dodać scoped do ref. W wersji (2.1) prawdopodobnie wymusi to również na deweloperze użycie Unsafe lub MemoryMarshal, ale jest to oczekiwane w przypadku niebezpiecznych interfejsów API stylizacji.

W idealnym przypadku język mógłby zmniejszyć wpływ cichych zmian powodujących niekompatybilność, wydając ostrzeżenie, gdy interfejs API cicho przechodzi w kłopotliwe zachowanie. Byłaby to metoda, która przyjmuje ref, zwraca ref struct, ale nie przechwytuje ref w ref struct. Kompilator może wydać ostrzeżenie w tym przypadku, informując programistów, że ref powinno być oznaczone jako scoped ref.

Decyzja Ten projekt można zrealizować, ale wynikowa funkcja jest bardziej skomplikowana w użyciu, co doprowadziło do decyzji o złamaniu zgodności.

Decision Kompilator wyświetli ostrzeżenie, gdy metoda spełnia kryteria, ale nie przechwytuje parametru ref jako pola ref. Powinno to odpowiednio ostrzegać klientów podczas uaktualnienia o potencjalnych problemach, które mogą się pojawić.

Słowa kluczowe a atrybuty

Ten projekt wymaga użycia atrybutów do dodawania adnotacji do nowych zasad dotyczących okresu życia. Można to również zrobić tak samo łatwo z kontekstowymi słowami kluczowymi. Na przykład [DoesNotEscape] można przyporządkować do scoped. Jednak słowa kluczowe, nawet te kontekstowe, zazwyczaj muszą spełniać bardzo wysoki próg, by zostać uwzględnionymi. Zajmują ważne miejsce w języku i stanowią bardziej znaczące jego części. Ta funkcja, choć cenna, będzie obsługiwać mniejszość deweloperów języka C#.

Na pierwszy rzut oka, to wydaje się sugerować niewykorzystywanie słów kluczowych, ale istnieją dwie ważne kwestie do rozważenia:

  1. Adnotacje będą wpływać na semantykę programu. C# jest niechętny do wprowadzania atrybutów, które wpływają na semantykę programu, i nie jest jasne, czy ta funkcja jest powodem, dla którego język powinien podjąć taki krok.
  2. Deweloperzy, którzy najprawdopodobniej będą używać tej funkcji, silnie pokrywają się z grupą deweloperów korzystających ze wskaźników funkcji. Ta funkcja, choć używana również przez mniejszość deweloperów, uzasadniała nową składnię i ta decyzja jest nadal postrzegana jako rozsądna.

Razem oznacza to, że należy wziąć pod uwagę składnię.

Przybliżony szkic składni to:

  • [RefDoesNotEscape] mapuje na scoped ref
  • [DoesNotEscape] mapuje na scoped
  • [RefDoesEscape] mapuje na unscoped

decyzja użyj składni dla scoped i scoped ref; użyj atrybutu dla unscoped.

Zezwalaj na lokalne bufor stałe

Ten projekt umożliwia bezpieczne bufory fixed, które mogą obsługiwać dowolny typ. Jedno z możliwych rozszerzeń pozwala na deklarowanie takich buforów fixed jako zmiennych lokalnych. Umożliwiłoby to zastąpienie wielu istniejących operacji stackalloc buforem fixed. Rozszerzyłoby to również zestaw scenariuszy, w których moglibyśmy mieć alokacje w stylu stosu, ponieważ stackalloc jest ograniczony do niezarządzanych typów elementarnych, podczas gdy bufory fixed nie są.

class FixedBufferLocals
{
    void Example()
    {
        Span<int> span = stackalloc int[42];
        int buffer[42];
    }
}

To się trzyma razem, ale wymaga od nas nieznacznego rozszerzenia składni dla zmiennych lokalnych. Niejasne, czy jest to lub nie jest warte dodatkowej złożoności. Możliwe, że moglibyśmy teraz zdecydować się na 'nie' i powrócić do tego później, jeśli zostanie wykazana wystarczająca potrzeba.

Przykład tego, gdzie byłoby korzystne: https://github.com/dotnet/runtime/pull/34149

decyzja wstrzymać się z tym na razie

Czy użyć modreqs, czy nie?

Należy podjąć decyzję, jeśli metody oznaczone nowymi atrybutami okresu istnienia powinny lub nie powinny być tłumaczone na modreq w emitie. Byłoby skuteczne mapowanie 1:1 między adnotacjami a modreq, gdyby to podejście zostało zastosowane.

Uzasadnieniem dodawania modreq jest to, że atrybuty zmieniają semantykę reguł bezpiecznego kontekstu ref. Tylko języki, które rozumieją te semantyki, powinny wywoływać te metody. Ponadto w przypadku scenariuszy OHI, czas życia staje się kontraktem, który muszą implementować wszystkie metody pochodne. Gdy adnotacje istnieją bez modreq, może to prowadzić do sytuacji, w których ładowane są virtual łańcuchy metod z kolidującymi adnotacjami dotyczącymi okresu istnienia (może się to zdarzyć, jeśli tylko jedna część łańcucha virtual jest skompilowana, a druga nie).

Początkowa praca w kontekście bezpiecznym dla odwołań nie korzystała z modreq, ale zamiast tego opierała się na językach i ramach systemowych, aby zrozumieć. Jednocześnie wszystkie elementy, które przyczyniają się do reguł bezpiecznego kontekstu ref, są silną częścią podpisu metody: ref, in, ref structitp. W związku z tym każda zmiana istniejących reguł metody już powoduje zmianę binarną podpisu. Aby nowe adnotacje okresu istnienia miały taki sam wpływ, potrzebne będzie ich wymuszanie za pomocą modreq.

Problemem jest to, czy to jest przesadne. Negatywne działanie polega na tym, że uczynienie podpisów bardziej elastycznymi, na przykład przez dodanie [DoesNotEscape] do parametru, spowoduje zmianę zgodności binarnej. Ten kompromis oznacza, że struktury, takie jak BCL, prawdopodobnie nie będą w stanie w przyszłości złagodzić takich podpisów. Można to częściowo złagodzić, stosując podejście, które język przyjmuje względem parametrów in, i stosując modreq tylko w pozycjach wirtualnych.

Decyzja Nie używaj modreq w metadanych. Różnica między out a ref nie polega na modreq, lecz na tym, że teraz mają różne wartości kontekstu ref. Nie ma żadnych rzeczywistych korzyści tylko do połowy wymuszania zasad z modreq tutaj.

Zezwalaj na wielowymiarowe stałe bufory

Czy projekt buforów fixed powinien zostać rozszerzony w celu uwzględnienia wielowymiarowych tablic stylizacji? Zasadniczo zezwalanie na deklaracje podobne do następujących:

struct Dimensions
{
    int array[42, 13];
}

decyzja Nie zezwalaj teraz

Naruszenie zakresu

Repozytorium środowiska uruchomieniowego zawiera kilka wewnętrznych API, które przechwytują parametry ref jako pola ref. Są one niebezpieczne, ponieważ okres istnienia wynikowej wartości nie jest śledzony. Na przykład konstruktor Span<T>(ref T value, int length).

Większość tych interfejsów API prawdopodobnie zdecyduje się na odpowiednie śledzenie okresu istnienia zwracanych wartości, co zostanie osiągnięte po prostu przez aktualizację do C# 11. Niektórzy jednak będą chcieli zachować swoją obecną semantykę niesledzenia wartości zwracanej, ponieważ ich intencja polega na byciu niebezpiecznymi. Najbardziej istotne przykłady to MemoryMarshal.CreateSpan i MemoryMarshal.CreateReadOnlySpan. Zostanie to osiągnięte przez oznaczenie parametrów jako scoped.

Oznacza to, że środowisko uruchomieniowe wymaga ustalonego wzorca niebezpiecznego usuwania scoped z parametru:

  1. Unsafe.AsRef<T>(in T value) może rozszerzyć swój istniejący cel, zmieniając się na scoped in T value. Pozwoliłoby to zarówno usunąć in, jak i scoped z parametrów. Następnie staje się uniwersalną metodą usuwania zabezpieczeń odwołań.
  2. Wprowadzenie nowej metody, której całym celem jest usunięcie scoped: ref T Unsafe.AsUnscoped<T>(scoped in T value). Spowoduje to usunięcie in również dlatego, że jeśli nie, osoby wywołujące nadal potrzebują kombinacji wywołań metod w celu "usunięcia bezpieczeństwa ref", w którym to momencie istniejące rozwiązanie jest prawdopodobnie wystarczające.

Czy domyślnie pozbawić zakreślenia?

Projekt ma tylko dwie lokalizacje, które domyślnie mają wartość scoped.

  • this jest scoped ref
  • out jest scoped ref

Decyzja dotycząca out ma na celu znaczne zmniejszenie obciążenia kompatybilności dla pól ref, a jednocześnie jest bardziej naturalnym domyślnym wyborem. Dzięki temu deweloperzy mogą traktować out jako dane przepływające tylko na zewnątrz, podczas gdy w przypadku ref, reguły muszą uwzględniać przepływ danych w obu kierunkach. Prowadzi to do poważnych nieporozumień deweloperów.

Decyzja w sprawie this jest niepożądana, ponieważ oznacza, że struct nie może zwrócić pola za pomocą ref. Jest to ważny scenariusz dla deweloperów skoncentrowanych na wysokiej wydajności, a atrybut [UnscopedRef] został dodany specjalnie dla tego jednego scenariusza.

Słowa kluczowe mają wysokie wymagania, a dodanie ich w jednym scenariuszu budzi wątpliwości. W związku z tym rozważano, czy w ogóle można uniknąć tego słowa kluczowego, tworząc this po prostu ref domyślnie, a nie scoped ref. Wszyscy członkowie, którzy potrzebują, aby this było scoped ref, mogą to zrobić, oznaczając metodę scoped (jako metodę można oznaczyć readonly w celu stworzenia readonly ref dzisiaj).

Na normalnym struct jest to głównie pozytywna zmiana, ponieważ wprowadza problemy z zgodnością tylko wtedy, gdy członek ma powrót ref. Istnieje bardzo kilka z tych metod i narzędzie może je wykryć i przekonwertować na scoped członków szybko.

W ref struct ta zmiana wprowadza znacznie większe problemy z zgodnością. Rozważ następujące kwestie:

ref struct Sneaky
{
    int Field;
    ref int RefField;

    public void SelfAssign()
    {
        // This pattern of ref reassign to fields on this inside instance methods would now
        // completely legal.
        RefField = ref Field;
    }

    static Sneaky UseExample()
    {
        Sneaky local = default;

        // Error: this is illegal, and must be illegal, by our existing rules as the 
        // ref-safe-context of local is now an input into method arguments must match. 
        local.SelfAssign();

        // This would be dangerous as local now has a dangerous `ref` but the above 
        // prevents us from getting here.
        return local;
    }
}

Zasadniczo oznaczałoby to, że wszystkie wywołania metod instancji w lokalne zmienneref struct modyfikowalne byłyby nielegalne, chyba że lokalne zostałyby dodatkowo oznaczone jako scoped. Reguły muszą uwzględnić sytuację, w której pola zostały przypisane ponownie do innych pól w this. readonly ref struct nie ma tego problemu, ponieważ readonly charakter zapobiega ponownemu przypisaniu ref. Nadal byłaby to znacząca zmiana powodująca niekompatybilność wsteczną, ponieważ wpłynęłaby na praktycznie wszystkie istniejące modyfikowalne ref struct.

Jednak readonly ref struct nadal jest problematyczny, gdy rozszerzamy się na posiadanie pól od ref do ref struct. Rozwiązanie umożliwia rozwiązanie tego samego podstawowego problemu poprzez przeniesienie przechwytywania do wartości pola ref.

readonly ref struct ReadOnlySneaky
{
    readonly int Field;
    readonly ref ReadOnlySpan<int> Span;

    public void SelfAssign()
    {
        // Instance method captures a ref to itself
        Span = new ReadOnlySpan<int>(ref Field, 1);
    }
}

Zastanawiano się nad pomysłem, aby this miało różne wartości domyślne w zależności od typu struct lub członka. Na przykład:

  • this jako ref: struct, readonly ref struct lub readonly member
  • this jako scoped ref: ref struct lub readonly ref struct z polem ref do ref struct

Minimalizuje to przerwy w kompatybilności i maksymalizuje elastyczność, ale kosztem komplikowania sytuacji dla klientów. Nie rozwiązuje to również w pełni problemu, ponieważ przyszłe funkcje, takie jak bezpieczne fixed, wymagają, aby modyfikowalne ref struct miały ref zwracane dla pól, które nie działają tylko w ramach tego projektu, ponieważ należą do kategorii scoped ref.

decyzja zachować this jako scoped ref. Oznacza to, że powyższe podstępne przykłady generują błędy kompilatora.

ref pola do ref struktura

Ta funkcja otwiera nowy zestaw reguł bezpiecznego kontekstu odniesienia, ponieważ umożliwia polu ref odwołanie się do ref struct. Ten ogólny charakter ByReference<T> oznaczał, że do tej pory środowisko uruchomieniowe nie mogło mieć takiej konstrukcji. W rezultacie wszystkie nasze zasady są napisane zgodnie z założeniem, że nie jest to możliwe. Funkcja pola ref w znacznej mierze nie chodzi o tworzenie nowych reguł, ale o kodyfikowanie istniejących reguł w naszym systemie. Zezwolenie na ref pól do ref struct wymaga od nas ujednolicania nowych reguł, ponieważ należy wziąć pod uwagę kilka nowych scenariuszy.

Po pierwsze, readonly ref jest teraz w stanie przechowywać stan ref. Na przykład:

readonly ref struct Container
{
    readonly ref Span<int> Span;

    void Store(Span<int> span)
    {
        Span = span;
    }
}

Oznacza to, że podczas analizowania argumentów metody pod kątem zgodności z regułami, należy rozważyć, że readonly ref T jest potencjalnym wynikiem metody, gdy T potencjalnie ma pole ref prowadzące do ref struct.

Drugim problemem jest język musi rozważyć nowy typ bezpiecznego kontekstu: ref-field-safe-context. Wszystkie ref struct, które przechodnio zawierają pole ref, mają inny zakres wyjścia reprezentujący wartości w polach ref. W przypadku wielu pól ref można je zbiorczo śledzić jako pojedynczą wartość. Wartość domyślna dla parametrów to kontekst wywołujący.

ref struct Nested
{
    ref Span<int> Span;
}

Span<int> M(ref Nested nested) => nested.Span;

Ta wartość nie jest powiązana z bezpiecznym kontekstem kontenera; czyli, gdy kontekst kontenera staje się mniejszy, nie ma to wpływu na ref-field-safe-context wartości pól ref. Ponadto kontekst bezpiecznego pola odniesienia nigdy nie może być mniejszy niż kontekst bezpieczny kontenera.

ref struct Nested
{
    ref Span<int> Span;
}

void M(ref Nested nested)
{
    scoped ref Nested refLocal = ref nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is illegal
    refLocal.Span = stackalloc int[42];

    scoped Nested valLocal = nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is still illegal
    valLocal.Span = stackalloc int[42];
}

Ten ref-field-safe-context zasadniczo zawsze istniał. Do tej pory pola ref mogły wskazywać tylko na normalne struct, dlatego zostały trywialnie zredukowane do kontekstu wywołującego . Aby obsługiwać pola ref i ref struct, należy zaktualizować istniejące reguły, aby uwzględniały ten nowy ref-safe-context.

Po trzecie, należy zaktualizować reguły ponownego przypisania ref, aby upewnić się, że nie naruszamy kontekstu ref-field-context dla wartości . Zasadniczo dla , gdzie typ jest , kontekstu bezpiecznego pola ref musi być równy.

Te problemy są łatwo rozwiązywalne. Zespół kompilatora naszkicował kilka wersji tych reguł i w dużej mierze wynikają one z naszej istniejącej analizy. Problem polega na tym, że dla takich reguł nie ma używanego kodu, który pomaga udowodnić poprawność i użyteczność. To sprawia, że jesteśmy bardzo niezdecydowani, aby dodać wsparcie z obawy, że wybierzemy nieprawidłowe wartości domyślne i wpędzimy środowisko uruchomieniowe w trudności z użytecznością, gdy zacznie z tego korzystać. Ten problem jest szczególnie silny, ponieważ .NET 8 prawdopodobnie kieruje nas w tym kierunku z allow T: ref struct i Span<Span<T>>. Reguły byłyby lepiej napisane, gdyby zostały stworzone w połączeniu z kodem konsumpcji.

decyzja Opóźnienie zezwalające ref pole na ref struct do platformy .NET 8, w których mamy scenariusze, które pomogą w kierowania regułami dotyczącymi tych scenariuszy. Nie zostało to zaimplementowane na platformie .NET 9

Co wprowadzi C# 11.0?

Funkcje opisane w tym dokumencie nie muszą być implementowane w jednym etapie. Zamiast tego można je zaimplementować etapami w ramach kilku wydań językowych w następujących kategoriach:

  1. ref pola i scoped
  2. [UnscopedRef]
  3. ref pola do ref struct
  4. Typy z ograniczeniami o zachodzie słońca
  5. bufory o stałym rozmiarze

To, co jest implementowane w danym wydaniu, jest jedynie ćwiczeniem określania zakresu.

decyzje dotyczące tylko (1) i (2) składają się na C# 11.0. Pozostałe będą brane pod uwagę w przyszłych wersjach języka C#.

Przyszłe zagadnienia

Zaawansowane adnotacje dotyczące okresu życia

Adnotacje dotyczące cyklu życia w tej propozycji są ograniczone w tym sensie, że umożliwiają deweloperom zmianę domyślnego zachowania dotyczącego ucieczki znaków wartości. Zapewnia to zaawansowaną elastyczność naszego modelu, ale nie zmienia radykalnie zestawu relacji, które można wyrazić. W rdzeniu model języka C# jest nadal skutecznie binarny: czy można zwrócić wartość, czy nie?

Pozwala to zrozumieć ograniczone relacje okresu istnienia. Na przykład wartość, której nie można zwrócić z metody, ma mniejszy okres istnienia niż wartość, którą można zwrócić z metody. Nie ma jednak możliwości opisania relacji okresu istnienia między wartościami, które mogą być zwracane z metody. W szczególności nie ma sposobu, aby stwierdzić, że jedna wartość ma dłuższy okres istnienia niż druga, gdy ustalono, że obie mogą być zwrócone z metody. Następnym krokiem w naszej ewolucji życia byłoby umożliwienie opisania takich relacji.

Inne metody, takie jak Rust, umożliwiają wyrażenia tego typu relacji, a tym samym mogą implementować bardziej złożone operacje scoped stylu. Nasz język może podobnie przynieść korzyści, jeśli ta funkcja została uwzględniona. W tej chwili nie ma presji, aby to zrobić, ale jeśli pojawi się w przyszłości, nasz model scoped można rozszerzyć, aby uwzględnić to w dość prosty sposób.

Każdemu scoped można przypisać nazwany czas życia, dodając argument typu ogólnego do składni. Na przykład scoped<'a> to wartość, która ma okres istnienia 'a. Ograniczenia, takie jak where, mogą być następnie używane do opisywania relacji między tymi okresami istnienia.

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
  where 'b >= 'a
{
    s.Span = span;
}

Ta metoda definiuje dwa okresy istnienia 'a i 'b i ich relację, w szczególności, że 'b jest większa niż 'a. Dzięki temu miejsce wywołania może mieć bardziej szczegółowe zasady dotyczące bezpiecznego przekazywania wartości do metod w porównaniu ze zgrubnymi zasadami obecnie.

Problemy

Wszystkie następujące problemy są związane z tą propozycją:

Propozycje

Następujące propozycje są związane z tą propozycją:

Istniejące przykłady

Utf8JsonReader

Ten konkretny fragment kodu wymaga użycia niebezpiecznego kodu, ponieważ ujawnia problemy związane z przekazywaniem Span<T>, które mogą być przypisane do metody instancyjnej na ref struct. Mimo że ten parametr nie jest przechwytywany, język musi zakładać, że jest to i dlatego niepotrzebnie powoduje tarcia tutaj.

Utf8JsonWriter

Ten fragment kodu chce mutować parametr przez ucieczkę elementów danych. Dane mogą być efektywnie przydzielane na stosie. Mimo że parametr nie zostanie usunięty, kompilator przypisuje mu bezpieczny kontekst poza otaczającej metody, ponieważ jest to parametr. Oznacza to, że aby można było używać alokacji stosu, implementacja musi używać unsafe w celu przypisania z powrotem do parametru po ucieczce danych.

Zabawne próbki

ReadOnlySpan<T>

public readonly ref struct ReadOnlySpan<T>
{
    readonly ref readonly T _value;
    readonly int _length;

    public ReadOnlySpan(in T value)
    {
        _value = ref value;
        _length = 1;
    }
}

Lista oszczędna

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public FrugalList(){}

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

Przykłady i notatki

Poniżej przedstawiono zestaw przykładów pokazujących, jak i dlaczego reguły działają tak, jak działają. Dołączono kilka przykładów pokazujących niebezpieczne zachowania i sposób, w jaki reguły uniemożliwiają im działanie. Ważne jest, aby pamiętać o nich podczas wprowadzania zmian w propozycji.

Ponowne przypisywanie referencji i miejsca wywołań

Demonstrujemy, jak ponowne przypisanie i wywołanie metody współpracują ze sobą.

ref struct RS
{
    ref int _refField;

    public ref int Prop => ref _refField;

    public RS(int[] array)
    {
        _refField = ref array[0];
    }

    public RS(ref int i)
    {
        _refField = ref i;
    }

    public RS CreateRS() => ...;

    public ref int M1(RS rs)
    {
        // The call site arguments for Prop contribute here:
        //   - `rs` contributes no ref-safe-context as the corresponding parameter, 
        //      which is `this`, is `scoped ref`
        //   - `rs` contribute safe-context of *caller-context*
        // 
        // This is an lvalue invocation and the arguments contribute only safe-context 
        // values of *caller-context*. That means `local1` has ref-safe-context of 
        // *caller-context*
        ref int local1 = ref rs.Prop;

        // Okay: this is legal because `local` has ref-safe-context of *caller-context*
        return ref local1;

        // The arguments contribute here:
        //   - `this` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `this` contributes safe-context of *caller-context*
        //
        // This is an rvalue invocation and following those rules the safe-context of 
        // `local2` will be *caller-context*
        RS local2 = CreateRS();

        // Okay: this follows the same analysis as `ref rs.Prop` above
        return ref local2.Prop;

        // The arguments contribute here:
        //   - `local3` contributes ref-safe-context of *function-member*
        //   - `local3` contributes safe-context of *caller-context*
        // 
        // This is an rvalue invocation which returns a `ref struct` and following those 
        // rules the safe-context of `local4` will be *function-member*
        int local3 = 42;
        var local4 = new RS(ref local3);

        // Error: 
        // The arguments contribute here:
        //   - `local4` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `local4` contributes safe-context of *function-member*
        // 
        // This is an lvalue invocation and following those rules the ref-safe-context 
        // of the return is *function-member*
        return ref local4.Prop;
    }
}

Ponowne przypisywanie ref i niebezpieczne ucieczki

Powód następującej linii w regułach ponownego przypisania odniesienia może nie być oczywisty na pierwszy rzut oka:

e1 musi mieć taki sam bezpieczny kontekst jak e2

Dzieje się tak, ponieważ okres istnienia wartości wskazywanych przez lokalizacje ref jest niezmienny. Pośrednictwo uniemożliwia nam zezwolenie na jakąkolwiek wariancję w tym miejscu, nawet w przypadku węższych czasów życia. Jeśli zawężenie jest dozwolone, zostanie otwarty następujący niebezpieczny kod:

void Example(ref Span<int> p)
{
    Span<int> local = stackalloc int[42];
    ref Span<int> refLocal = ref local;

    // Error:
    // The safe-context of refLocal is narrower than p. For a non-ref reassignment 
    // this would be allowed as its safe to assign wider lifetimes to narrower ones.
    // In the case of ref reassignment though this rule prevents it as the 
    // safe-context values are different.
    refLocal = ref p;

    // If it were allowed this would be legal as the safe-context of refLocal
    // is *caller-context* and that is satisfied by stackalloc. At the same time
    // it would be assigning through p and escaping the stackalloc to the calling
    // method
    // 
    // This is equivalent of saying p = stackalloc int[13]!!! 
    refLocal = stackalloc int[13];
}

Dla przejścia z ref do nie ref struct ta reguła jest trywialnie spełniona, ponieważ wszystkie wartości mają ten sam bezpieczny kontekst. Ta reguła naprawdę wchodzi w grę tylko wtedy, gdy wartość jest ref struct.

To zachowanie ref będzie również ważne w przyszłości, w której pozwolimy, aby pola ref mogły ref struct.

lokalne o określonym zakresie

Użycie scoped w zmiennych lokalnych będzie szczególnie przydatne w przypadku wzorców kodu, które warunkowo przypisują wartości z różnymi bezpiecznymi kontekstami do zmiennych lokalnych. Oznacza to, że kod nie musi już polegać na trikach inicjalizacyjnych, takich jak = stackalloc byte[0] do zdefiniowania lokalnego bezpiecznego kontekstu, lecz teraz można po prostu użyć scoped.

// Old way 
// Span<byte> span = stackalloc byte[0];
// New way 
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
    span = stackalloc byte[len];
}
else
{
    span = new byte[len];
}

Ten wzorzec występuje często w kodzie niskiego poziomu. Gdy zaangażowany ref struct jest Span<T> można użyć powyższej sztuczki. Nie ma jednak zastosowania do innych typów ref struct i może spowodować, że kod niskopoziomowy będzie musiał uciec się do unsafe, aby obejść problem z określeniem czasu życia.

wartości parametrów o określonym zakresie

Jednym ze źródeł powtarzających się tarć w kodzie niskiego poziomu jest to, że domyślne traktowanie parametrów jest zbyt liberalne. Są one w bezpiecznym kontekście wobec kontekstu wywołującego. Jest to rozsądna wartość domyślna, ponieważ jest zgodna ze schematami kodowania platformy .NET jako całości. W kodzie niskiego poziomu istnieje większe użycie ref struct i to ustawienie domyślne może powodować tarcie z innymi częściami reguł bezpiecznego kontekstu ref.

Główny punkt tarcia występuje dlatego, że argumenty metody muszą być zgodne z regułą. Ta reguła najczęściej wchodzi w grę z metodami wystąpień w ref struct, gdzie co najmniej jeden parametr jest również ref struct. Jest to typowy wzorzec w kodzie niskiego poziomu, w którym typy ref struct często wykorzystują parametry Span<T> w swoich metodach. Na przykład wystąpi w każdym stylu pisania ref struct, który używa Span<T> do przekazywania buforów.

Ta reguła istnieje, aby zapobiec scenariuszom, takim jak następujące:

ref struct RS
{
    Span<int> _field;
    void Set(Span<int> p)
    {
        _field = p;
    }

    static void DangerousCode(ref RS p)
    {
        Span<int> span = stackalloc int[] { 42 };

        // Error: if allowed this would let the method return a reference to 
        // the stack
        p.Set(span);
    }
}

Zasadniczo ta reguła istnieje, ponieważ język musi zakładać, że wszystkie dane wejściowe do metody są traktowane jako uciekające w maksymalnym dozwolonym bezpiecznym kontekście . W przypadku parametrów ref lub out, w tym odbiorników, dane wejściowe mogą uciekać jako pola tych wartości ref (jak to się dzieje w RS.Set powyżej).

W praktyce istnieje wiele takich metod, które przekazują ref struct jako parametry, które nigdy nie zamierzają przechwytywać ich w danych wyjściowych. Jest to tylko wartość używana w bieżącej metodzie. Na przykład:

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Error: The safe-context of `span` is function-member 
        // while `reader` is outside function-member hence this fails
        // by the above rule.
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

Aby wykorzystać ten kod niskiego poziomu, będzie się uciekać do sztuczek unsafe, aby okłamać kompilator w kwestii cyklu życia ich ref struct. Znacznie zmniejsza to wartość ref struct, ponieważ mają one być środkiem, aby uniknąć unsafe podczas pisania kodu o wysokiej wydajności.

W tym przypadku scoped jest skutecznym narzędziem do zarządzania parametrami ref struct, ponieważ eliminuje je z rozważania jako zwracane przez metodę. Zgodnie z zaktualizowaną regułą zgodności, argumenty metody muszą być zgodne z zasadą. Parametr ref struct, który jest używany, ale nigdy nie zwracany, może być oznaczony jako scoped, aby zwiększyć elastyczność miejsc wywołań.

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(scoped ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Okay: the compiler never considers `span` as capturable here hence it doesn't
        // contribute to the method arguments must match rule
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

Zapobieganie trudnemu przypisaniu ref z mutacji readonly

Gdy ref zostanie przeniesiony do pola readonly w konstruktorze lub init składowym, typ jest ref, a nie ref readonly. Jest to długotrwałe zachowanie, które umożliwia kod podobny do następującego:

struct S
{
    readonly int i; 

    public S(string s)
    {
        M(ref i);
    }

    static void M(ref int i) { }
}

To jednak stanowi potencjalny problem, jeśli taki ref można było przechowywać w polu ref w tym samym typie. Pozwoliłoby to na bezpośrednią mutację readonly struct z członka instancji:

readonly ref struct S
{ 
    readonly int i; 
    readonly ref int r; 
    public S()
    {
        i = 0;
        // Error: `i` has a narrower scope than `r`
        r = ref i;
    }

    public void Oops()
    {
        r++;
    }
}

Wniosek uniemożliwia to jednak, ponieważ narusza zasady bezpiecznego kontekstu ref. Rozważ następujące kwestie:

  • kontekstu bezpiecznego this to element funkcji, a w bezpiecznym kontekście jest kontekst wywołujący. Są one standardem dla this w struct elementu członkowskiego.
  • ref-safe-contexti jest funkcji-członkowskiej. Wynika to z reguł dotyczących czasów życia pól . W szczególności reguła 4.

W tym momencie linia r = ref i jest niezgodna z regułami przypisania referencji .

Zasady te nie miały na celu zapobiegania temu zachowaniu, ale robią to jako efekt uboczny. Należy pamiętać o każdej przyszłej aktualizacji reguły, aby ocenić wpływ na takie scenariusze.

Głupie przypisanie cykliczne

Jednym aspektem, z jakim zmagał się ten projekt, jest to, jak swobodnie można zwrócić ref z metody. Zezwalanie na zwracanie wszystkich ref równie swobodnie jak normalnych wartości jest tym, czego programiści intuicyjnie oczekują. Jednak pozwala to na patologiczne scenariusze, które kompilator musi wziąć pod uwagę podczas obliczania bezpieczeństwa ref. Rozważ następujące kwestie:

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        // Error: s.field can only escape the current method through a return statement
        s.refField = ref s.field;
    }
}

Nie jest to wzorzec kodu, którego oczekujemy od deweloperów. Jednak gdy ref można zwrócić z tym samym okresem istnienia co wartość, jest to zgodne z zasadami. Kompilator musi wziąć pod uwagę wszystkie możliwe przypadki podczas oceniania wywołania metody, co prowadzi do tego, że takie interfejsy API stają się praktycznie nieużywalne.

void M(ref S s)
{
    ...
}

void Usage()
{
    // safe-context to caller-context
    S local = default; 

    // Error: compiler is forced to assume the worst and concludes a self assignment
    // is possible here and must issue an error.
    M(ref local);
}

Aby te interfejsy API były użyteczne, kompilator zapewnia, że czas życia ref dla parametru ref jest krótszy niż czas życia jakichkolwiek odwołań w powiązanej wartości parametru. Jest to uzasadnienie kontekstu bezpiecznego do ref struct być out tylko do zwrotu, a kontekstu wywołującego. Uniemożliwia to przypisanie cykliczne z powodu różnicy w czasach życia.

Należy pamiętać, że [UnscopedRef]promuje poprzezref-safe-context dowolne wartości ref do ref struct do kontekstu wywołującego , co umożliwia cykliczne przypisywanie i wymusza rozpowszechnione użycie [UnscopedRef] w górę łańcucha wywołań:

S F()
{
    S local = new();
    // Error: self assignment possible inside `S.M`.
    S.M(ref local);
    return local;
}

ref struct S
{
    int field;
    ref int refField;

    public static void M([UnscopedRef] ref S s)
    {
        // Allowed: s has both safe-context and ref-safe-context of caller-context
        s.refField = ref s.field;
    }
}

Podobnie [UnscopedRef] out zezwala na przypisanie cykliczne, ponieważ parametr ma zarówno bezpiecznego kontekstu, jak i ref-safe-context tylko zwracanych.

Promowanie [UnscopedRef] ref do kontekstu wywołującego jest przydatne, gdy typ nie jest ref struct (należy pamiętać, że chcemy zachować reguły proste, aby nie rozróżniały odwołań do ref a nie ref struktury):

int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2

static S F([UnscopedRef] ref int x)
{
    S local = new();
    local.M(ref x);
    return local;
}

ref struct S
{
    public ref int RefField;

    public void M([UnscopedRef] ref int data)
    {
        RefField = ref data;
    }
}

Jeśli chodzi o zaawansowane adnotacje, projekt [UnscopedRef] tworzy następujące elementy:

ref struct S { }

// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)

// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
  where 'b >= 'a

readonly nie można stosować w pełni na polach ref

Rozważmy poniższy przykładowy kod:

ref struct S
{
    ref int Field;

    readonly void Method()
    {
        // Legal or illegal?
        Field = 42;
    }
}

Podczas projektowania reguł dla pól ref w wystąpieniach readonly w próżni, reguły mogą być zaprojektowane w taki sposób, że powyższe jest legalne lub nielegalne. Zasadniczo readonly może być głęboko za pośrednictwem pola ref lub może mieć zastosowanie tylko do ref. Zastosowanie tylko do ref uniemożliwia ponowne przypisanie ref, ale umożliwia normalne przypisanie, które zmienia wartość, do której odwołuje się odwołanie.

Ten projekt nie istnieje jednak w próżni, projektuje zasady dla typów, które mają już skuteczne pola ref. Najbardziej widoczne z nich, Span<T>, ma już silną zależność od tego, że readonly nie jest tutaj zbyt głęboko. Jego podstawowym scenariuszem jest możliwość przypisania do pola ref za pośrednictwem wystąpienia readonly.

readonly ref struct SpanOfOne
{
    readonly ref int Field;

    public ref int this[int index]
    {
        get
        {
            if (index != 1)
                throw new Exception();
            return ref Field;
        }
    }
}

Oznacza to, że musimy wybrać płytkią interpretację readonly.

Konstruktory modelowania

Jednym z subtelnych pytań projektowych jest: W jaki sposób ciała konstruktorów są modelowane pod kątem bezpieczeństwa referencji? Zasadniczo jak jest analizowany następujący konstruktor?

ref struct S
{
    ref int field;

    public S(ref int f)
    {
        field = ref f;
    }
}

Istnieją mniej więcej dwa podejścia:

  1. Model jako metoda static, w której this jest lokalny, w kontekście którego jego bezpieczny kontekst jest kontekstem wywołującym
  2. Model jako metoda static, w której this jest parametrem out.

Ponadto konstruktor musi spełniać następujące niezmienne elementy:

  1. Upewnij się, że parametry ref można przechwycić jako pola ref.
  2. Upewnij się, że ref do pól this nie można uciec za pomocą parametrów ref. To naruszałoby trudne przypisania ref.

Intencją jest wybranie formularza, który spełnia nasze niezmienności bez wprowadzenia żadnych specjalnych reguł dla konstruktorów. Biorąc pod uwagę, że najlepszy model dla konstruktorów traktuje this jako parametr out. zwracać tylko charakter out pozwala nam spełnić wszystkie zmienne powyżej bez specjalnej wielkości liter:

public static void ctor(out S @this, ref int f)
{
    // The ref-safe-context of `ref f` is *return-only* which is also the 
    // safe-context of `this.field` hence this assignment is allowed
    @this.field = ref f;
}

Argumenty metody muszą być zgodne

Reguła zgodności argumentów metody jest typowym źródłem nieporozumień dla deweloperów. Jest to reguła, która ma wiele specjalnych przypadków, które trudno zrozumieć, chyba że znasz rozumowanie reguły. Aby lepiej zrozumieć przyczyny reguły, uprościmy kontekst ref-bezpieczny i bezpieczny kontekst do po prostu kontekstu.

Metody mogą stosunkowo dowolnie zwracać stan przekazany do nich jako parametr. Zasadniczo każdy osiągalny stan, który jest niezakresowy, może zostać zwrócony (w tym zwracany przez ref). Można to zwrócić bezpośrednio za pomocą instrukcji return lub pośrednio, przypisując je do wartości ref.

Zwroty bezpośrednie nie stanowią wielu problemów z bezpieczeństwem ref. Kompilator po prostu musi przyjrzeć się wszystkim danym wejściowym możliwym do zwrócenia do metody, a następnie skutecznie ogranicza zwracaną wartość do minimalnego kontekstu danych wejściowych. Ta wartość zwracana następnie przechodzi przez normalne przetwarzanie.

Zwroty pośrednie stanowią istotny problem, ponieważ wszystkie ref są zarówno danymi wejściowymi, jak i wyjściowymi metody. Te dane wyjściowe mają już znany kontekst . Kompilator nie może wywnioskować nowych wniosków, musi je wziąć pod uwagę na obecnym poziomie. Oznacza to, że kompilator musi przyjrzeć się każdemu pojedynczemu ref, który można przypisać w wywoływanej metodzie, ocenić jego kontekst , a następnie zweryfikować, czy żadne zwracalne dane wejściowe do metody nie mają mniejszego kontekstu niż ref. Jeśli taki przypadek istnieje, wywołanie metody musi być niedozwolone, ponieważ może naruszać bezpieczeństwo ref.

Argumenty metody muszą być zgodne; jest to proces, dzięki któremu kompilator sprawdza tę kontrolę bezpieczeństwa.

Innym sposobem oceny tego, co jest często łatwiejsze do rozważenia przez deweloperów, jest wykonanie następującego ćwiczenia:

  1. Przyjrzyj się definicji metody, zidentyfikuj wszystkie miejsca, gdzie można pośrednio zwrócić stan: a. Modyfikowalne parametry ref wskazujące na ref struct b. Modyfikowalne parametry ref z przypisywalnymi do ref polami ref. Parametry przypisywalne ref lub pola ref wskazujące na ref struct (rozważ rekursję)
  2. Przyjrzyj się lokacji wywołania. Zidentyfikuj konteksty, które są zgodne z lokalizacjami określonymi powyżej b. Zidentyfikuj konteksty wszystkich danych wejściowych metody, które są zwracane (nie są zgodne z parametrami scoped)

Jeśli jakakolwiek wartość w 2.b jest mniejsza niż 2.a, wywołanie metody musi być niedozwolone. Przyjrzyjmy się kilku przykładom, aby zilustrować reguły:

ref struct R { }

class Program
{
    static void F0(ref R a, scoped ref R b) => throw null;

    static void F1(ref R x, scoped R y)
    {
        F0(ref x, ref y);
    }
}

Patrząc na wywołanie do F0, przejdźmy przez punkty (1) i (2). Parametry z możliwością zwrotu pośredniego są a i b, ponieważ oba te parametry można przypisać bezpośrednio. Argumenty, które są zgodne z tymi parametrami:

  • a, który mapuje na x, posiadający kontekst z kontekstu wywołującego
  • b mapowania na y z kontekstem składowych funkcji

Zestaw zwracanych danych wejściowych do metody to

  • x z zakresu ucieczki kontekstu wywołującego
  • ref x z zakresu ucieczki kontekstu wywołującego
  • y z zakresu ucieczki składowej funkcji

Wartość ref y nie jest zwracana, ponieważ jest przyporządkowana do scoped ref, dlatego nie jest traktowana jako dane wejściowe. Jednak biorąc pod uwagę, że istnieje co najmniej jedno dane wejściowe z mniejszym zakresu ucieczki ( argumenty) niż jeden z danych wyjściowych ( argumentx), wywołanie metody jest niedozwolone.

Inna odmiana jest następująca:

ref struct R { }

class Program
{
    static void F0(ref R a, ref int b) => throw null;

    static void F1(ref R x)
    {
        int y = 42;
        F0(ref x, ref y);
    }
}

Parametry z możliwością zwrotu pośredniego to ponownie a i b, ponieważ oba można przypisać bezpośrednio. Ale b można wykluczyć, ponieważ nie wskazuje ref struct dlatego nie może być używany do przechowywania stanu ref. W ten sposób mamy:

  • a, który mapuje na x, posiadający kontekst z kontekstu wywołującego

Zestaw zwracanych danych wejściowych do metody to:

  • x z kontekstemkontekstu wywołującego
  • ref x z kontekstemkontekstu wywołującego
  • ref y z kontekstem elementu funkcji

Biorąc pod uwagę, że istnieje co najmniej jedno dane wejściowe z mniejszym zakresu ucieczki ( argumentref y) niż jeden z danych wyjściowych ( argumentx), wywołanie metody jest niedozwolone.

Logika ta ma obejmować zasadę, według której argumenty metody muszą być zgodne z regułą. To idzie dalej, ponieważ traktuje scoped jako sposób na usunięcie danych wejściowych z rozważań, a readonly jako sposób na usunięcie ref jako wyjścia (nie można przypisać do readonly ref, więc nie może być źródłem wyjścia). Te specjalne przypadki powodują dodanie złożoności do reguł, ale robi się to z korzyścią dla dewelopera. Kompilator dąży do usunięcia wszystkich danych wejściowych i wyjściowych, o których wie, że nie przyczynią się do wyniku, aby zapewnić deweloperom maksymalną elastyczność podczas wywoływania członka. Podobnie jak rozwiązanie przeciążenia, warto zwiększyć złożoność naszych reguł, tworząc większą elastyczność dla konsumentów.

Przykłady wnioskowanych bezpiecznego kontekstu wyrażeń deklaracji

Związane z Określanie kontekstu bezpiecznego wyrażeń deklaracji.

ref struct RS
{
    public RS(ref int x) { } // assumed to be able to capture 'x'

    static void M0(RS input, out RS output) => output = input;

    static void M1()
    {
        var i = 0;
        var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M3(RS rs1)
    {
        M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
    }
}

Należy pamiętać, że kontekst lokalny, który wynika z modyfikatora scoped, jest najwęższy, który może być używany dla zmiennej — każdy węższy oznacza, że wyrażenie odnosi się do zmiennych, które są deklarowane tylko w węższym kontekście niż wyrażenie.