Sdílet prostřednictvím


Vylepšení datových struktur nízké úrovně

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách z příslušné schůzky návrhu jazyka (LDM) .

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Shrnutí

Tento návrh je agregace několika návrhů pro zlepšení výkonu struct: ref polí a možnost přepsat výchozí nastavení životnosti. Cílem je návrh, který bere v úvahu různé návrhy k vytvoření jedné nadlimitní sady funkcí pro vylepšení nízké úrovně struct.

Poznámka: Předchozí verze této specifikace používaly termíny „ref-safe-to-escape“ a „safe-to-escape“, které byly zavedeny ve specifikaci funkce Span safety. Standardní výbor ECMA změnil názvy na "ref-safe-context" a "safe-context". Hodnoty bezpečného kontextu byly upřesněny tak, aby konzistentně používaly "declaration-block", "function-member" a "caller-context". Speclety používaly pro tyto termíny různé formulace a také používaly "bezpečný návrat" jako synonymum pro "kontext volajícího". Tato specifikace byla aktualizována tak, aby používala termíny ve standardu C# 7.3.

Ne všechny funkce uvedené v tomto dokumentu byly implementovány v jazyce C# 11. C# 11 zahrnuje:

  1. ref polí a scoped
  2. [UnscopedRef]

Tyto funkce zůstávají otevřené návrhy pro budoucí verzi jazyka C#:

  1. ref polí pro ref struct
  2. Typy s omezenými západy slunce

Motivace

Dřívější verze jazyka C# přidaly do jazyka řadu funkcí nízké úrovně výkonu: ref vrací, ref struct, ukazatele funkcí atd. ... Tito vývojáři .NET umožnili psát vysoce výkonný kód a současně nadále využívat pravidla jazyka C# pro zabezpečení typů a paměti. Umožnil také vytváření základních typů výkonu v knihovnách .NET, jako je Span<T>.

Vzhledem k tomu, že tyto funkce získaly oblibu mezi vývojáři v ekosystému .NET, jak interními, tak externími, poskytovali nám informace o zbývajících třecích bodech v ekosystému. Místa, kde stále potřebují vypustit kód unsafe, aby se jejich práce dokončila, nebo vyžadují modul runtime pro speciální typy případů, jako je Span<T>.

Dnes Span<T> se dosahuje pomocí typu internalByReference<T>, se kterým modul runtime efektivně pracuje jako s polem ref. To poskytuje výhodu ref polí, ale nevýhodou je, že jazyk neposkytuje pro toto žádné bezpečnostní ověření, zatímco pro jiné využití refano. Tento typ lze dále použít pouze dotnet/runtime, protože je internal, takže třetí strany nemohou navrhnout své vlastní primitivy na základě ref polí. Součástí motivace k této práci je odebrat ByReference<T> a použít správná pole ref ve všech základech kódu.

Tento návrh má v úmyslu řešit tyto problémy prostřednictvím našich stávajících základních funkcí. Konkrétně má za cíl:

  • Povolit ref struct typům deklarovat pole ref.
  • Povolit modulu runtime plně definovat Span<T> pomocí systému typů C# a odebrat speciální typ případu, jako je ByReference<T>
  • Povolte struct typům vrátit ref na svá pole.
  • Povolit modulu runtime odebrat unsafe použití způsobenými omezeními výchozích hodnot životnosti
  • Povolit deklaraci bezpečných vyrovnávacích pamětí fixed pro spravované a nespravované typy v struct

Podrobný návrh

Pravidla pro bezpečnost ref struct jsou definována v bezpečnostním dokumentu rozsahu s použitím předchozích termínů. Tato pravidla byla začleněna do normy C# 7 v §9.7.2 a §16.4.12. Tento dokument popisuje požadované změny tohoto dokumentu v důsledku tohoto návrhu. Po přijetí jako schválené funkce se tyto změny začlení do tohoto dokumentu.

Po dokončení tohoto návrhu bude definice Span<T> následující:

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;
    }
}

Zadejte pole ref a vymezený obor

Jazyk umožní vývojářům deklarovat pole ref uvnitř ref struct. To může být užitečné například při zapouzdření velkých proměnlivých struct instancí nebo definování vysoce výkonných typů, jako je Span<T> v knihovnách kromě modulu runtime.

ref struct S 
{
    public ref int Value;
}

Pole ref se vygeneruje do metadat pomocí podpisu ELEMENT_TYPE_BYREF. To se nijak neliší od způsobu generování ref místních hodnot nebo argumentů ref. Například ref int _field bude emitováno jako ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. To bude vyžadovat, abychom aktualizovali ECMA335, aby tuto položku povolili, ale mělo by to být poměrně jednoduché.

Vývojáři mohou pokračovat v inicializaci ref struct s polem ref pomocí výrazu default v takovém případě budou mít všechna deklarovaná pole ref hodnotu null. Při každém pokusu o použití těchto polí dojde k vyvolání NullReferenceException.

ref struct S 
{
    public ref int Value;
}

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

Zatímco jazyk C# předstíří, že ref nemůže být null to je legální na úrovni modulu runtime a má dobře definovanou sémantiku. Vývojáři, kteří do svých typů zavádějí pole ref, si musí být vědomi této možnosti a měli by být důrazně zabránit úniku těchto podrobností do využívání kódu. Místo toho by ref pole měla být ověřena jako nenulová pomocí pomocných rutin modulu runtime a vyvolání v případě nesprávného použití neinicializovaného 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 lze kombinovat s modifikátory readonly následujícími způsoby:

  • readonly ref: toto je pole, které nelze znovu přiřadit mimo konstruktor nebo init metody. Hodnota může být přiřazena, i když mimo tyto kontexty.
  • ref readonly: jedná se o pole, které může být znovu přiřazeno, ale nemůže být přiřazena hodnota v žádném okamžiku. Tímto způsobem může být parametr in znovu přiřazen k poli ref.
  • readonly ref readonly: kombinace ref readonly a 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 bude vyžadovat, aby byla ref pole deklarována jako readonly ref. Není nutné, aby byly deklarovány readonly ref readonly. To umožňuje, aby readonly struct měly nepřímé mutace prostřednictvím takového pole, ale to se nijak neliší od pole readonly, které dnes odkazuje na referenční typ (více podrobností)

Do metadat se vygeneruje readonly ref pomocí příznaku initonly, který odpovídá jakémukoli jinému poli. Pole ref readonly bude přiřazeno s System.Runtime.CompilerServices.IsReadOnlyAttribute. readonly ref readonly se vygeneruje s oběma položkami.

Tato funkce vyžaduje podporu modulu runtime a změny specifikace ECMA. Tyto funkce budou povoleny pouze v případě, že je v Corelibu nastaven odpovídající příznak dané funkce. Tento problém, který sleduje přesné rozhraní API, je sledován zde https://github.com/dotnet/runtime/issues/64165.

Sada změn pravidel bezpečného kontextu nezbytná pro povolení ref polí je malá a cílová. Pravidla již zahrnují ref pole, která existují a využívají se přes rozhraní API. Změny se musí zaměřit jen na dva aspekty: jak vznikají a jak jsou znovu přiřazovány.

Nejprve je potřeba aktualizovat pravidla pro kontextových polích následujícím způsobem:

Následujícím způsobem: výraz v podobě ref e.Fref-safe-context.

  1. Pokud je F polem ref, pak jeho ref-safe-context je bezpečný kontexte.
  2. Jinak pokud je e typu odkazu, má ref-bezpečný-kontext z kontextu volajícího
  3. Jinak se jeho ref-safe-context bere z ref-safe-context u e.

To nepředstavuje změnu pravidla, i když pravidla vždy zohlednila, že ref stav existuje uvnitř ref struct. Takto ve skutečnosti vždy fungoval stav ref v Span<T> a pravidla spotřeby to správně zohledňují. Tato změna pouze umožňuje vývojářům mít přímý přístup k polím ref a zajistit, že dodržují stávající pravidla implicitně platící pro Span<T>.

To znamená, že pole ref se dají vrátit jako ref z ref struct, ale normální pole nemohou.

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;
}

Může to na první pohled vypadat jako chyba, ale jedná se o záměrný návrhový bod. Ačkoli se tímto návrhem nevytváří nové pravidlo, uznávají stávající pravidla Span<T>, aby vývojáři mohli deklarovat svůj vlastní stav ref.

Dále je potřeba upravit pravidla pro znovupřiřazení odkazů s ohledem na přítomnost polí ref. Primárním scénářem pro opětovné přiřazení ref je uložení parametrů ref struct do polí ref pomocí ref konstruktorů. Podpora bude obecnější, ale toto je základní scénář. Aby to bylo podpořeno, budou pravidla pro přerozdělení referencí upravena tak, aby zohledňovala pole ref, následovně:

Pravidla opětovného přiřazení odkazu

Levý operand operátoru = ref musí být výraz, který je vázán na místní proměnnou ref, parametr ref (jiný než this), výstupní parametr, nebo ref pole.

Pro opětovné přiřazení odkazu ve formuláři e1 = ref e2 musí být splněny obě následující podmínky:

  1. e2 musí mít ref-safe-context alespoň tak velký jako ref-safe-context e1
  2. e1 musí mít stejný bezpečný kontext jako e2Poznámka

To znamená, že požadovaný konstruktor Span<T> funguje bez další poznámky:

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;
    }
}

Změna pravidel opětovného přiřazení odkazu znamená, že parametry ref teď můžou utéct z metody jako pole ref v hodnotě ref struct. Jak je popsáno v části , aspekty kompatibility mohou změnit pravidla pro existující rozhraní API, které nikdy nezamýšlely, aby parametry ref unikly jako pole ref. Pravidla životnosti parametrů jsou založena výhradně na jejich deklaraci, nikoli na jejich použití. Všechny parametry a mají kontextu kontextu kontextu volajícího ref , a proto je teď možné vrátit nebo polem . Aby bylo možné podporovat rozhraní API s ref parametry, které mohou být escapující nebo neeskapující, a tím obnovit sémantiku míst volání v C# 10, bude jazyk zavádět anotace s omezenou životností.

modifikátor scoped

Klíčové slovo scoped se použije k omezení životnosti hodnoty. Lze jej použít na ref nebo hodnotu, která je ref struct a má vliv na omezení životnosti kontextu ref-safe-context nebo safe-context na člena funkce . Například:

Parametr nebo lokální ref-safe-context bezpečný kontext
Span<int> s člen funkce kontextu volajícího
scoped Span<int> s člen funkce člen funkce
ref Span<int> s kontextu volajícího kontextu volajícího
scoped ref Span<int> s člen funkce kontextu volajícího

V této relaci ref-safe-kontext hodnoty nemůže být nikdy širší než bezpečného kontextu.

To umožňuje, aby rozhraní API v jazyce C# 11 byla opatřena poznámkami tak, aby měla stejná pravidla jako 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]);
}

Anotace scoped také znamená, že parametr this u struct lze nyní definovat jako scoped ref T. Dříve musela být v pravidlech aplikována speciální úprava pro parametr ref, který měl odlišná pravidla ref-safe-context než ostatní parametry ref (viz všechny odkazy na zahrnutí nebo vyloučení příjemce v pravidlech bezpečného kontextu). Nyní se dá vyjádřit jako obecný koncept v rámci pravidel, která je dále zjednodušuje.

Poznámka scoped může být také použita na následující místa:

  • locals: Tato poznámka nastaví životnost bezpečného kontextunebo ref-safe-context v případě ref místního člena funkce bez ohledu na dobu životnosti inicializátoru.
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];
}

Další použití pro scoped v místních prostředích jsou popsány níže.

scoped poznámku nelze použít na žádné jiné místo, včetně návratových hodnot, polí, prvků pole atd. Kromě toho má scoped vliv při použití na jakýkoli ref, in nebo out, ale pouze tehdy, pokud je použit na hodnoty, které jsou ref struct. Deklarace jako scoped int nemají žádný vliv, protože návrat ne-ref struct je vždy bezpečný. Kompilátor vytvoří diagnostiku pro takové případy, aby se zabránilo nejasnostem vývojářů.

Změna chování parametrů out

Chcete-li dále omezit dopad změny kompatibility vracející parametry ref a in jako pole ref, jazyk změní výchozí hodnotu ref-safe-context pro parametry out na funkčně-členskou hodnotu. Parametry out se implicitně scoped out do budoucna. Jakmile se podíváme z pohledu kompatibility, to znamená, že je nelze vrátit pomocí ref:

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

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

Tím se zvýší flexibilita rozhraní API, která vracejí ref struct hodnoty a mají parametry out, protože už nemusí brát v úvahu, že se parametr zachytává odkazem. To je důležité, protože se jedná o běžný vzor v rozhraních API pro styl čtečky:

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);
}

Jazyk už nebude považovat argumenty předané parametru out za vrátitelné. Zpracování vstupu na parametr out jako návratové bylo pro vývojáře velmi matoucí. V podstatě odvrací záměr out tím, že vývojářům vynutí vzít v úvahu hodnotu předanou volajícím, která se nikdy nepoužívá s výjimkou jazyků, které nerespektují out. V budoucnu musí jazyky, které podporují ref struct zajistit, aby původní hodnota předaná parametru out nebyla nikdy přečtená.

Jazyk C# toho dosahuje prostřednictvím určitých pravidel přiřazení. Tím se nejen dosáhne dodržování našich pravidel pro bezpečný kontext referencí, ale také se umožní stávajícímu kódu přiřadit a následně vrátit hodnoty parametrů out.

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

Tyto změny společně znamenají, že argument parametru out nepřispívá k hodnotám kontextově bezpečným nebo kontextově bezpečným pro reference při vyvolání metody. To výrazně snižuje celkový dopad na kompatibilitu polí ref a také zjednodušuje, jak vývojáři uvažují o out. Argument out parametru nepřispívá k návratové hodnotě, jedná se o pouhý výstup.

Odvození bezpečného kontextu deklarativních výrazů

bezpečné kontextové proměnné deklarace z argumentu () nebo dekonstrukce () je nejužší z následujících:

  • kontext volajícího
  • Pokud je proměnná označena scoped, pak je deklarativního bloku (tj. jako člen funkce nebo něco specifičtějšího).
  • pokud je typ proměnné ref struct, zvažte všechny argumenty obsahující vyvolání, včetně příjemce:
    • bezpečného kontextu libovolného argumentu, kde odpovídající parametr není out a má bezpečný kontextnávratových nebo širších
    • ref-safe-context jakéhokoli argumentu, kde má odpovídající parametr ref-safe-contextreturn-only nebo širší

Viz také Příklady odvozených bezpečného kontextu výrazů deklarací.

Implicitně scoped parametry

Celkově existují dvě ref místa, která jsou implicitně určena jako scoped:

  • U metody instance this na struct
  • parametry out

Pravidla bezpečného kontextu ref budou vyjádřena prostřednictvím scoped ref a ref. Pro účely bezpečného kontextu ref je parametr in ekvivalentní ref a out je ekvivalentní scoped ref. in i out budou výslovně zmíněna pouze v případech, kdy je to důležité pro sémantiku pravidla. Jinak se považují pouze za ref a scoped ref.

Při diskusi o ref-safe-kontextu argumentů, které odpovídají parametrům in, budou ve specifikaci generalizovány jako ref argumenty. Pokud je argument lvalue, pak ref-safe-kontext odpovídá lvalue, jinak je to člen funkce. Znovu in bude uvedena pouze tehdy, když je důležité pro význam aktuálního pravidla.

Bezpečný kontext pouze pro vrácení

Návrh také vyžaduje, aby bylo zavedeno nové bezpečné prostředí: pouze pro návrat. To se podobá kontextu volajícího v tom, že se dá vrátit, ale ji lze pouze vrátit prostřednictvím příkazu return.

Podrobnosti pro pouze pro návrat je, že je to kontext, který je větší než funkční člen , ale menší než kontext volajícího . Výraz zadaný pro příkaz return musí být alespoň návratový. Jako taková většina stávajících pravidel padá. Například přiřazení k parametru z výrazu s bezpečným kontextem návratového selže, protože je menší než bezpečné kontextovékontextu volajícího . O potřebě tohoto nového únikového kontextu se bude diskutovat níže.

Jsou tři umístění, která jsou ve výchozím nastavení určena pouze pro vracení :

  • Parametr nebo bude mít návratovéhos bezpečným kontextem. To se provádí částečně pro ref struct, aby se zabránilo hloupým cyklickým problémům s přiřazením. Provádí se to jednotně, aby se zjednodušil model i minimalizovaly změny kompatibility.
  • Parametr out pro ref struct bude mít bezpečné kontextovénávratu . To umožňuje, aby návrat a out byly stejně výrazné. To nemá problém s hloupým cyklickým přiřazením, protože out je implicitně scoped, takže kontext bezpečného odkazu je stále menší než bezpečný kontext.
  • Parametr konstruktoru bude mít návratovéhobezpečného kontextu . Dochází k tomu kvůli modelování jako parametrů out.

Jakýkoli výraz nebo příkaz, který explicitně vrací hodnotu z metody nebo lambda, musí mít bezpečný kontexta pokud je k dispozici, alespoň ref-bezpečný kontext, pouze pro návrat. To zahrnuje return příkazy, výrazy bodované členy a výrazy lambda.

Stejně tak každé přiřazení k out musí mít bezpečný kontext alespoň pouze pro návrat. Nejedná se ale o zvláštní případ, který následuje pouze z existujících pravidel přiřazení.

Poznámka: Výraz, jehož typ není typem ref struct, má vždy bezpečný kontext volajícího pro kontext .

Pravidla pro vyvolání metody

Pravidla kontextu bezpečného odkazu pro vyvolání metody se aktualizují několika způsoby. Prvním je rozpoznání dopadu, který scoped má na argumenty. Pro daný argument expr, který se předá parametru p:

  1. Pokud je pscoped ref, expr nepřispívá kontextu ref-safe-context při zvažování argumentů.
  2. Pokud je pscoped, expr nepřispívá bezpečnému kontextu při zvažování argumentů.
  3. Pokud je pout, expr nepřispívá k referenčnímu bezpečnému kontextu nebo bezpečnému kontextuvíce podrobností

Jazyk "nepřispívá" znamená, že argumenty se prostě neberou v úvahu při výpočtu hodnoty vrácení metody, konkrétně ref-bezpečného kontextu, nebo bezpečného kontextu. Je to proto, že hodnoty nemohou přispět k této životnosti, protože poznámka scoped tomu brání.

Pravidla volání metody se teď dají zjednodušit. Příjemce už nemusí být považován za zvláštní případ, v případě struct je teď jednoduše scoped ref T. Pravidla hodnot se musí změnit, aby zohlednila návraty pole ref:

Hodnota vyplývající z vyvolání metody e1.M(e2, ...), kde M() nevrací strukturu ref-to-ref, má bezpečný kontext převzatý z nejužší z následujících možností:

  1. Kontext volajícího
  2. Pokud je návratová hodnota ref struct a bezpečný kontext, na který přispěly všechny výrazy argumentů.
  3. Pokud je návrat , pak ref-safe-context přispěný všemi argumenty .

Pokud M() vrátí ref-to-ref-strukturu, bezpečný kontext je stejný jako bezpečný kontext všech argumentů, které jsou ref-to-ref-struktura. Jedná se o chybu, pokud existuje více argumentů s různým bezpečným kontextem , protože argumenty metody musí být stejné jako.

Pravidla volání ref se dají zjednodušit na:

Hodnota vyplývající z vyvolání metody ref e1.M(e2, ...), kde M() nevrací strukturu ref-to-ref,, je ref-safe-context nejužší z následujících kontextů:

  1. Kontext volajícího
  2. Bezpečný kontext, k němuž přispěly všechny výrazy argumentů.
  3. ref-safe-context byla přispívána všemi argumenty ref

Pokud M() vrátí ref-to-ref-struct, ref-safe-context je nejužší ref-safe-context přispívající všemi argumenty, které jsou ref-to-ref-struct.

Toto pravidlo teď umožňuje definovat dvě varianty požadovaných 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);
}

Pravidla pro inicializátory objektů

bezpečný kontext výrazu inicializátoru objektu je nejužší z:

  1. bezpečný kontext volání konstruktoru.
  2. bezpečného kontextu a argumentů indexerům inicializátoru členů, které mohou utéct do příjemce.
  3. bezpečné kontextové RHS přiřazení v inicializátorech členů na nečtené settery nebo ref-safe-context v případě přiřazení odkazu.

Dalším způsobem modelování je považovat jakýkoli argument pro inicializaci člena, který lze přiřadit příjemci, za argument konstruktoru. Důvodem je, že inicializátor členu je efektivní volání konstruktoru.

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);

Toto modelování je důležité, protože ukazuje, že naše MAMM musí zvlášť zohlednit inicializátory členů. Vezměte v úvahu, že tento konkrétní případ musí být neplatný, protože umožňuje, aby byla hodnota s užším bezpečným kontextem přiřazena k vyšší.

Argumenty metody se musí shodovat.

Přítomnost polí ref znamená, že je nutné aktualizovat pravidla týkající se argumentů metody, protože parametr ref nyní může být uložen jako pole v argumentu metody ref struct. Dříve pravidlo muselo zvážit pouze další ref struct, které se ukládá jako pole. Tento dopad je řešen v ohledech na kompatibilitu. Nové pravidlo je ...

Pro jakoukoli metodu vyvolání e.M(a1, a2, ... aN)

  1. Výpočet nejužšího bezpečného kontextu z:
    • kontextu volajícího
    • bezpečný kontext všech argumentů
    • kontextu ref-safe-context všech argumentů ref, jejichž odpovídající parametry mají kontextu kontextu volajícího
  2. Všechny argumenty ref typů ref struct musí být přiřaditelné pomocí hodnoty s tím bezpečným kontextem. Toto je případ, kdy ref nemůže zobecnit zahrnutí in a out.

Pro jakoukoli metodu vyvolání e.M(a1, a2, ... aN)

  1. Výpočet nejužšího bezpečného kontextu z:
    • kontextu volajícího
    • bezpečný kontext všech argumentů
    • Kontext ref-safe všech argumentů ref, jejichž odpovídající parametry nejsou scoped
  2. Všechny argumenty out typů ref struct musí být přiřaditelné pomocí hodnoty s tím bezpečným kontextem.

Přítomnost scoped umožňuje vývojářům snížit tření tohoto pravidla tím, že označí parametry, které nejsou vráceny jako scoped. Tím se odstraní argumenty z položky (1) v obou výše uvedených případech a volajícím zajistí větší flexibilitu.

Dopad této změny je podrobněji popsán níže. Celkově to umožní vývojářům učinit volací místa flexibilnější tím, že budou označovat neunikající hodnoty podobné referencím pomocí scoped.

Rozptyl rozsahu parametrů

Modifikátor scoped a atribut [UnscopedRef] (viz níže) mají vliv také na přepisování našich objektů, implementaci rozhraní a pravidla pro převod delegate. Podpis pro přepsání, implementaci rozhraní nebo konverzi delegate může:

  • Přidání scoped do parametru ref nebo in
  • Přidání scoped do parametru ref struct
  • Odstraňte [UnscopedRef] z parametru out
  • Odebrání [UnscopedRef] z parametru ref typu ref struct

Jakýkoli jiný rozdíl s ohledem na scoped nebo [UnscopedRef] se považuje za neshodu.

Kompilátor oznámí diagnostiku nebezpečných neshod s rozsahem v případě přepsání, implementací rozhraní a delegování převodů v následujících případech:

  • Metoda vrátí ref struct, ref nebo ref readonly, nebo má parametr ref či out typu ref struct.
  • Metoda má alespoň jeden další ref, innebo out parametr nebo parametr typu ref struct.

Výše uvedená pravidla ignorují parametry this, protože ref struct metody instance nelze použít pro přepsání, implementace rozhraní nebo delegování převodů.

Diagnostika je hlášena jako chyba, pokud oba neodpovídající podpisy používají pravidla bezpečného kontextu ref C#11; jinak je diagnostika varování.

Upozornění na neshodu s vymezeným oborem může být hlášeno v modulu kompilovaném pomocí pravidel bezpečného kontextu C#7.2, kde scoped není k dispozici. V některých takových případech může být nutné potlačit upozornění, pokud se jiný neshodovaný podpis nedá změnit.

Modifikátor scoped a atribut [UnscopedRef] mají také následující účinky na podpisy metody:

  • Modifikátor scoped a atribut [UnscopedRef] nemají vliv na skrytí
  • Přetížení se nemohou lišit pouze u scoped nebo [UnscopedRef]

Část o poli ref a scoped je dlouhá, takže ji chci uzavřít stručným souhrnem navrhovaných nekompatibilních změn:

  • Hodnota, která má kontextu ref-safe-context na kontext volajícího, je možné vrátit nebo pole.
  • Parametr out by měl bezpečný kontext člena funkce .

Podrobné poznámky:

  • Pole ref lze deklarovat pouze uvnitř ref struct
  • Pole ref nelze deklarovat static, volatile ani const
  • Pole ref nemůže mít typ, který je ref struct
  • Proces generování referenčního sestavení musí zachovat přítomnost pole ref uvnitř ref struct
  • readonly ref struct musí deklarovat ref pole jako readonly ref
  • U referenčních hodnot musí být modifikátor scoped uveden před in, outnebo ref
  • Dokument bezpečnostních pravidel bude aktualizován, jak je uvedeno v tomto dokumentu.
  • Nová pravidla bezpečného kontextu 'ref' budou platit, pokud
    • Základní knihovna obsahuje příznak funkce označující podporu polí ref.
    • Hodnota langversion je 11 nebo vyšší.

Syntax

13.6.2 Deklarace místních proměnných: přidáno 'scoped'?.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 Prohlášení for: přidáno 'scoped'?nepřímo z local_variable_declaration.

13.9.5 Příkaz foreach: přidán 'scoped'?.

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

12.6.2 Seznamy argumentů: přidáno 'scoped'? pro deklaraci proměnné out.

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

12.7 výrazy dekonstrukce:

[TBD]

Parametry metody 15.6.2: přidán '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 Deklarace delegátů: přidány 'scoped'?nepřímo z fixed_parameter.

12.19 Anonymní funkční výrazy: přidáno 'scoped'?.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

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

Typy s omezenými západy slunce

Kompilátor má koncept sady "omezených typů", která je z velké části nezdokumentována. Těmto typům byl udělen zvláštní stav, protože v jazyce C# 1.0 neexistuje způsob vyjádření jejich chování pro obecné účely. Zejména skutečnost, že typy mohou obsahovat odkazy na zásobník spouštění. Místo toho měl kompilátor speciální znalosti o nich a omezil jejich použití na způsoby, které by vždy byly bezpečné: nepovolené návraty, nelze použít jako prvky pole, nelze použít v obecných typech atd . .

Jakmile jsou pole ref dostupná a rozšířená tak, aby podporovala ref struct tyto typy lze v jazyce C# správně definovat pomocí kombinace ref struct a ref polí. Proto když kompilátor zjistí, že modul runtime podporuje ref polí, už nebude mít představu o omezených typech. Místo toho použije typy, které jsou definovány v kódu.

Abychom toto podpořili, naše pravidla bezpečného kontextu odkazů budou aktualizována následujícím způsobem:

  • __makeref se bude považovat za metodu s podpisem static TypedReference __makeref<T>(ref T value)
  • __refvalue bude považován za metodu se signaturou static ref T __refvalue<T>(TypedReference tr). Výraz __refvalue(tr, int) efektivně použije druhý argument jako parametr typu.
  • jako parametr bude mít ref-safe-context a bezpečný kontext členů funkce .
  • __arglist(...) jako výraz bude mít referenčně bezpečný kontext a bezpečný kontext člena funkce .

Odpovídající moduly runtime zajistí, aby TypedReference, RuntimeArgumentHandle a ArgIterator byly definovány jako ref struct. Další TypedReference se musí zobrazit jako pole ref do ref struct pro libovolný možný typ (může ukládat libovolnou hodnotu). To v kombinaci s výše uvedenými pravidly zajistí, že odkazy na zásobník neuniknou mimo dobu své platnosti.

Poznámka: Přísně řečeno je to podrobnosti implementace kompilátoru vs. část jazyka. Nicméně vzhledem k vztahu s poli ref je to zahrnuto do návrhu jazyků pro zjednodušení.

Poskytněte bez rozsahu

Jedním z nejvýraznějších třecí bodů je nemožnost vrátit pole ref v případě členů struct. To znamená, že vývojáři nemohou vytvářet ref metody vracející hodnoty nebo atributy a vlastnosti a musí se uchylovat k přímému odhalení polí. To snižuje užitečnost vratek ref v struct, kde je často nejžádanější.

struct S
{
    int _field;

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

odůvodnění pro tuto výchozí hodnotu je rozumné, ale s struct útěkem this pomocí reference není nic inherentně špatného; je to jednoduše výchozí hodnota zvolená pravidly bezpečného kontextu ref.

K vyřešení tohoto problému jazyk poskytne opak poznámky o životnosti scoped podporou UnscopedRefAttribute. To se dá použít u libovolného ref a změní ref-safe-context na jednu úroveň širší než výchozí. Například:

Neomezený odkaz použít na Původní bezpečný kontext ref Nový kontext ref-safe-context
člen instance funkce-člen pouze pro návrat
parametr in / ref pouze pro návrat kontext volajícího
parametr out funkce-člen pouze pro návrat

Při použití [UnscopedRef] na metodu instance struct má vliv na úpravu implicitního this parametru. To znamená, že this působí jako ref stejného typu bez označení.

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;
}

Poznámku lze také umístit na parametry out a obnovit je do chování jazyka C# 10.

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

Pro účely pravidel bezpečného kontextu ref se taková [UnscopedRef] out považuje za ref. Podobá se tomu, jak se in považuje za ref pro účely hodnocení životnosti.

[UnscopedRef] poznámka bude zakázána pro init členy a konstruktory uvnitř struct. Tito členové jsou již speciální s ohledem na ref sémantiku, protože členy readonly považují za proměnlivé. To znamená, že se ref těmto členům zobrazí jako jednoduchý refnikoli ref readonly. To je povoleno v mezích konstruktorů a init. Povolení [UnscopedRef] by takové ref umožnilo nesprávně utéct mimo konstruktor a povolit mutaci po provedení readonly sémantiky.

Typ atributu bude mít následující definici:

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

Podrobné poznámky:

  • Metoda instance nebo vlastnost opatřená poznámkami kontextu ref-safe-context nastaven na kontext volajícího.
  • Člen anotovaný [UnscopedRef] nemůže implementovat rozhraní.
  • Je chyba používat [UnscopedRef] na
    • Člen, který není deklarován v struct
    • Člen static nebo init, nebo konstruktor v struct
    • Parametr označený scoped
    • Parametr předaný hodnotou
    • Parametr předaný odkazem, který není implicitně vymezen

ScopedRefAttribute

Poznámky scoped budou zahrnuty do metadat prostřednictvím atributu typu System.Runtime.CompilerServices.ScopedRefAttribute. Atribut se bude shodovat s názvem kvalifikovaným názvovým prostorem, takže definice nemusí být uvedena v žádném konkrétním sestavení.

Typ ScopedRefAttribute je určen pouze pro použití kompilátoru – ve zdroji není povolený. Deklarace typu je syntetizována kompilátorem, pokud ještě není součástí kompilace.

Typ bude mít následující definici:

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

Kompilátor vygeneruje tento atribut u parametru se syntaxí scoped. Tato funkce se vygeneruje pouze v případě, že syntaxe způsobí, že se hodnota liší od výchozího stavu. Například scoped out způsobí, že se nevygeneruje žádný atribut.

RefSafetyRulesAttribute

Existuje několik rozdílů v bezpečném kontextu pravidla mezi C#7.2 a C#11. Některé z těchto rozdílů můžou vést k zásadním změnám při rekompilování pomocí C#11 proti odkazům zkompilovanými v jazyce C#10 nebo starším.

  1. neskopované parametry ref/in/out mohou uniknout z volání metody jako položka ref objektu ref struct v C#11, ne v C#7.2.
  2. out parametry mají implicitně vymezenou platnost v jazyce C#11 a v jazyce C#7.2 jsou bez vymezené platnosti.
  3. ref / in parametry pro typy ref struct jsou implicitně vymezené v jazyce C#11 a neomezené v jazyce C#7.2.

Abychom snížili pravděpodobnost zásadních změn při rekompilování pomocí C#11, aktualizujeme kompilátor C#11 tak, aby používal pravidla ref bezpečného kontextu pro vyvolání metody, která odpovídají pravidlům použitým k analýze deklarace metody. V podstatě, když se analyzuje volání metody zkompilované pomocí staršího kompilátoru, kompilátor C#11 použije pravidla ref bezpečného kontextu jazyka C#7.2.

Aby to bylo možné, kompilátor vygeneruje nový atribut [module: RefSafetyRules(11)], když je modul zkompilován s -langversion:11 nebo vyšší nebo zkompilován pomocí corlibu obsahujícího příznak funkce pro pole ref.

Argument atributu označuje jazyková verze ref bezpečný kontext pravidla použitá při kompilaci modulu. Verze je aktuálně opravena v 11 bez ohledu na skutečnou jazykovou verzi předanou kompilátoru.

Očekávání spočívá v tom, že budoucí verze kompilátoru aktualizují pravidla kontextu bezpečného odkazu a vygenerují atributy s odlišnými verzemi.

Pokud kompilátor načte modul, který obsahuje [module: RefSafetyRules(version)]s jinou version než 11, kompilátor oznámí upozornění pro nerozpoznanou verzi, pokud existují nějaká volání metod deklarovaných v daném modulu.

Když kompilátor C#11 analyzuje volání metody:

  • Pokud modul obsahující deklaraci metody obsahuje [module: RefSafetyRules(version)], bez ohledu na version, volání metody se analyzuje pomocí pravidel C#11.
  • Pokud je modul obsahující deklaraci metody ze zdroje a zkompilován pomocí -langversion:11 nebo pomocí corlibu obsahujícího příznak funkce pro pole ref, volání metody se analyzuje pomocí pravidel C#11.
  • Pokud modul obsahující deklaraci metody odkazuje System.Runtime { ver: 7.0 }, volání metody se analyzuje pomocí pravidel C#11. Toto pravidlo je dočasné zmírnění rizik pro moduly zkompilované se staršími verzemi Preview C#11 / .NET 7 a později se odeberou.
  • V opačném případě se volání metody analyzuje pomocí pravidel C#7.2.

Kompilátor pre-C#11 bude ignorovat všechny RefSafetyRulesAttribute a analyzovat volání metod pouze pomocí pravidel C#7.2.

RefSafetyRulesAttribute se bude shodovat s názvem kvalifikovaným oborem názvů, takže se definice nemusí zobrazovat v žádném konkrétním sestavení.

Typ RefSafetyRulesAttribute je určen pouze pro použití kompilátoru – ve zdroji není povolený. Deklarace typu je syntetizována kompilátorem, pokud ještě není součástí kompilace.

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;
    }
}

Bezpečné vyrovnávací paměti pevné velikosti

Bezpečné vyrovnávací paměti s pevnou velikostí nebyly zahrnuty v jazyce C# 11. Tuto funkci je možné implementovat v budoucí verzi jazyka C#.

Jazyk uvolní omezení polí s pevnou velikostí tak, aby je bylo možné deklarovat v bezpečném kódu a typ prvku lze spravovat nebo nespravovat. Tímto se typy, jako jsou následující, stanou legálními:

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

Tyto deklarace, podobně jako jejich ekvivalenty unsafe, definují posloupnost N prvků v obsahujícím typu. K těmto členům je možné přistupovat pomocí indexeru a lze je také převést na Span<T> a ReadOnlySpan<T> instance.

Při indexování do vyrovnávací paměti fixed typu T je potřeba vzít v úvahu readonly stav kontejneru. Pokud je kontejner readonly, vrátí indexer ref readonly T jinak vrátí ref T.

Přístup k vyrovnávací paměti fixed bez indexeru nemá žádný přirozený typ, avšak je možné ji převést na typy Span<T>. V případě, že je kontejner readonly, je možné vyrovnávací paměť implicitně převést na ReadOnlySpan<T>, jinak se může implicitně převést na Span<T> nebo ReadOnlySpan<T> (přičemž převod na Span<T> se považuje za lepší).

Výsledná Span<T> instance bude mít délku rovnu velikosti deklarované ve vyrovnávací paměti fixed. bezpečný kontext vrácené hodnoty bude roven bezpečnému kontextu kontejneru, stejně jako kdyby byla podpůrná data přístupná jako pole.

Pro každou deklaraci fixed v rámci typu, kde je typ prvku T, jazyk vygeneruje odpovídající metodu indexeru pouze get, jejíž návratový typ je ref T. Indexer bude označen atributem [UnscopedRef], protože implementace bude vracet pole deklarujícího typu. Přístupnost člena bude odpovídat přístupnosti v poli fixed.

Například podpis indexeru pro CharBuffer.Data bude následující:

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

Pokud je zadaný index mimo hranice deklarované pro pole fixed, bude vyvolána výjimka IndexOutOfRangeException. V případě, že je zadaná konstantní hodnota, bude nahrazena přímým odkazem na příslušný prvek. Pokud není konstanta mimo deklarované hranice, v takovém případě by došlo k chybě času kompilace.

Pro každou vyrovnávací paměť fixed se vygeneruje také pojmenovaný přístupový prvek, který poskytuje operace get a set podle hodnoty. To znamená, že fixed vyrovnávací paměti budou blíže připomínat stávající sémantiku pole tím, že mají ref příslušenství, stejně jako vedlejší get a set operace. To znamená, že kompilátory budou mít stejnou flexibilitu při generování kódu, který využívá vyrovnávací paměti fixed, jako když využívají pole. To by mělo usnadnit generování operací, jako je await, nad bufferem fixed.

To má také přidanou výhodu, že fixed vyrovnávací paměti se dají snáze používat v jiných jazycích. Pojmenované indexery jsou funkce, která existuje od verze 1.0 rozhraní .NET. Dokonce i jazyky, které nemohou přímo emitovat pojmenovaný indexer, je obecně spotřebovávají (C# je ve skutečnosti dobrým příkladem).

Záložní úložiště pro vyrovnávací paměť se vygeneruje pomocí atributu [InlineArray]. Toto je mechanismus diskutovaný v problému 12320, který konkrétně umožňuje efektivně deklarovat posloupnost polí stejného typu. Tento konkrétní problém je stále předmětem aktivní diskuze a očekává se, že implementace této funkce bude následovat podle jejího závěru.

Inicializátory s hodnotami ref ve výrazech new a with

V části 12.8.17.3 Inicializátory objektů, aktualizujeme gramatiku na:

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

V části pro výraz withaktualizujeme gramatiku na:

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

Levý operand přiřazení musí být výraz, který je svázán s polem typu ref.
Pravý operand musí být výraz, který vrací lvalue označující hodnotu stejného typu jako levý operand.

Přidáme podobné pravidlo pro přiřazení ref místní.
Pokud je levý operand odkazem umožňujícím zápis (tj. označuje cokoli jiného než pole ref readonly), musí být pravý operand zapisovatelným lvalue.

Úniková pravidla pro vyvolání konstruktoru zůstanou:

Výraz new, který vyvolá konstruktor, dodržuje stejná pravidla jako volání metody, která je považována za vracející vytvářený typ.

Konkrétně pravidla vyvolání metody aktualizována výše:

Hodnota rvalue vyplývající z vyvolání metody e1.M(e2, ...)bezpečný kontext z nejmenších následujících kontextů:

  1. Kontext volajícího
  2. Bezpečný kontext, k němuž přispěly všechny výrazy argumentů.
  3. Když je návrat ref struct pak ref-safe-kontext přispěl všemi argumenty ref

U výrazu new s inicializátory se výrazy inicializátoru počítají jako argumenty (přispívají svým bezpečným kontextem) a výrazy inicializátoru ref se počítají jako argumenty ref (přispívají svým ref-bezpečným kontextem), rekurzivně.

Změny v nebezpečném kontextu

Typy ukazatelů (oddíl 23.3) jsou rozšířeny tak, aby umožňovaly spravované typy jako odkazující typ. Tyto typy ukazatelů se zapisují jako spravovaný typ následovaný tokenem *. Vygenerují upozornění.

Operátor adresy (oddílu 23.6.5) je uvolněný pro přijetí proměnné se spravovaným typem jako operandem.

Příkaz fixed (oddíl 23.7) je uvolněný, aby přijímal fixed_pointer_initializer, který je adresou proměnné spravovaného typu T nebo výrazem array_type s prvky spravovaného typu T.

Inicializátor přidělování zásobníku (oddíl 12.8.22) je obdobně zjednodušený.

Úvahy

Při vyhodnocování této funkce by měly být zvažovány i další části vývojového zásobníku.

Úvahy o kompatibilitě

Výzvou v tomto návrhu je dopad na kompatibilitu, který tento návrh má na stávající pravidla bezpečnosti, nebo na stávající §9.7.2. Ačkoli tato pravidla plně podporují koncept ref struct s ref poli, nedovolují API, s výjimkou stackalloc, zachytit ref stav, který odkazuje na zásobník. Pravidla bezpečného kontextu ref mají pevný předpokladnebo §16.4.12.8, že konstruktor formuláře Span(ref T value) neexistuje. To znamená, že bezpečnostní pravidla nepočítá, že parametr ref může utéct jako pole ref, a proto umožňuje kód podobný následujícímu.

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);
}

V podstatě existují tři způsoby, jak může ref parametr uniknout během vyvolání metody:

  1. Podle návratu hodnoty
  2. Podle ref return
  3. Pole ref ve ref struct, které je vráceno nebo předáno jako parametr ref / out

Stávající pravidla pouze představují (1) a (2). Nezohlední (3), a proto nejsou zohledněny mezery, jako je vrácení místních proměnných v polích ref. Tento návrh musí změnit pravidla, aby zohlednil (3). To bude mít malý dopad na kompatibilitu stávajících rozhraní API. Konkrétně to bude mít vliv na rozhraní API, která mají následující vlastnosti.

  • Mít něco jako ref struct v podpisu
    • Kde je ref struct návratový typ, ref nebo parametr out
    • Má další parametr in nebo ref s výjimkou příjemce.

V C# 10 volající takových API nikdy nemuseli zvažovat, že vstup ref do rozhraní API mohl být zachycen jako pole ref. To umožnilo existenci několika vzorců v jazyce C# 10, které budou v jazyce C# 11 nebezpečné, kvůli možnosti, že stav ref může uniknout jako pole ref. Například:

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]);
}

Očekává se, že dopad tohoto přerušení kompatibility bude velmi malý. Ovlivněný tvar rozhraní API nemá smysl v nepřítomnosti ref polí, proto je nepravděpodobné, že zákazníci vytvořili mnoho z těchto polí. Probíhají experimenty s nástroji ke zjištění této struktury API nad stávajícími úložišti, které tuto domněnku podporují. Jediným úložištěm s významnými počty tohoto obrazce je dotnet/runtime a je to proto, že toto úložiště může vytvářet ref pole prostřednictvím intrinsického typu ByReference<T>.

Návrh i tak musí zohledňovat existenci takových rozhraní API, protože vyjadřují platný vzor, avšak ne běžný. Návrh proto musí vývojářům poskytnout nástroje k obnovení stávajících pravidel životnosti při upgradu na C# 10. Konkrétně musí poskytovat mechanismy, které vývojářům umožňují anotovat parametry ref tak, aby nemohly uniknout prostřednictvím pole ref nebo ref. To zákazníkům umožňuje definovat rozhraní API v jazyce C# 11, která mají stejná pravidla volání C# 10.

Referenční sestavení

Referenční sestavení pro kompilaci pomocí funkcí popsaných v tomto návrhu musí udržovat prvky, které poskytují informace o bezpečném kontextu ref. To znamená, že všechny atributy anotace životnosti musí být zachovány v původní pozici. Jakýkoli pokus o nahrazení nebo vynechání může vést k neplatným referenčním sestavám.

Reprezentace ref polí je subtilnější. V ideálním případě by se pole ref zobrazovalo v referenčním sestavení stejně jako jakékoli jiné pole. Pole ref ale představuje změnu formátu metadat a může způsobovat problémy s řetězy nástrojů, které se neaktualizují, aby porozuměly této změně metadat. Konkrétním příkladem je C++/CLI, které pravděpodobně vyvolá chybu, pokud zpracovává pole ref. Proto je výhodné, pokud ref pole mohou být vynechána z referenčních sestavení v základních knihovnách.

Pole ref samo o sobě nemá žádný vliv na pravidla bezpečného kontextu ref. Jako konkrétní příklad zvažte, že překlopení existující definice Span<T> na použití pole ref nemá žádný vliv na spotřebu. Proto je možné ref samotné bezpečně vynechat. Pole ref ale má další dopady na spotřebu, které je potřeba zachovat:

  • ref struct, která má pole ref, se nikdy nepovažuje za unmanaged
  • Typ pole ref má vliv na nekonečná obecná rozšiřující pravidla. Proto pokud typ pole ref obsahuje parametr typu, který musí být zachován.

Na základě těchto pravidel je zde platná referenční transformace sestavení pro 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
}

Anotace

Životní doby jsou nejpřirozeněji vyjadřovány pomocí typů. Životnost daného programu je bezpečná, když kontrola typů životnosti. I když syntaxe jazyka C# implicitně přidává životnosti k hodnotám, existuje základní systém typů, který zde popisuje základní pravidla. Často je jednodušší diskutovat o implikaci změn návrhu z hlediska těchto pravidel, aby byly zahrnuty do diskuze.

Všimněte si, že to není 100% kompletní dokumentace. Dokumentování každého chování není tady cílem. Místo toho je vhodné vytvořit obecné porozumění a společnou verzi, pomocí které lze model a potenciální změny modelu prodiskutovat.

Obvykle není nutné přímo mluvit o typech života. Výjimky jsou místa, kde se životnosti můžou lišit v závislosti na konkrétním místě vytváření instance. Jedná se o druh polymorfismu a tyto různé životnosti nazýváme "obecné životnosti", které jsou reprezentovány jako obecné parametry. Jazyk C# neposkytuje syntaxi pro vyjádření obecných typů životnosti, takže definujeme implicitní "překlad" z jazyka C# do rozšířeného dolního jazyka, který obsahuje explicitní obecné parametry.

Následující příklady využívají pojmenované doby života. Syntaxe $a odkazuje na dobu životnosti pojmenovanou a. Jedná se o život, který sám o sobě nemá žádný význam, ale může být dán do vztahu s jinými životy prostřednictvím syntaxe where $a : $b. To stanoví, že $a je konvertibilní na $b. Může pomoci si představit, že $a má životnost alespoň tak dlouhou jako $b.

Existuje několik předdefinovaných životností pro usnadnění a stručnost níže:

  • $heap: jedná se o životnost jakékoli hodnoty, která existuje v haldě. Je k dispozici ve všech kontextech a signaturách metod.
  • $local: jedná se o dobu života jakékoli hodnoty, která existuje v zásobníku metody. Je to vlastně zástupce názvu pročlena funkce . Je implicitně definován v metodách a může se objevit v podpisech metod s výjimkou jakékoli výstupní pozice.
  • $ro: držitel jmen pro vrácení pouze
  • $cm: jmenný zástupce pro volajícího-kontext

Mezi životnostmi existuje několik předdefinovaných relací:

  • where $heap : $a pro všechny životnosti $a
  • where $cm : $ro
  • where $x : $local pro všechny předdefinované doby životnosti. Uživatelem definované životnosti nemají žádný vztah k místnímu prostředí, pokud nejsou explicitně definovány.

Proměnné životnosti, pokud jsou definovány u typů, mohou být invariantní nebo kovariantní. Jsou vyjádřeny pomocí stejné syntaxe jako obecné parametry:

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

Parametr životnosti $this definic typu je není předdefinovaný, ale má k němu přidružená několik pravidel, když je definován:

  • Musí to být první parametr životnosti.
  • Musí být kovariantní: out $this.
  • Životnost polí ref musí být konvertibilní na $this
  • Životnost $this všech polí, která nejsou ref, musí být $heap nebo $this.

Životnost reference se vyjadřuje poskytnutím argumentu životnosti referenci. Například ref, která odkazuje na heap, je vyjádřena jako ref<$heap>.

Při definování konstruktoru v modelu se název new použije pro metodu. Je nutné mít seznam parametrů pro vrácenou hodnotu a také argumenty konstruktoru. To je nezbytné k vyjádření vztahu mezi vstupy konstruktoru a konstruovanou hodnotou. Místo Span<$a><$ro> bude model místo toho používat Span<$a> new<$ro>. Typ this v konstruktoru, včetně životnosti, bude určovat definovanou návratovou hodnotu.

Základní pravidla životnosti jsou definována takto:

  • Všechny životní doby jsou vyjádřeny syntakticky jako generické argumenty, které se nacházejí před argumenty typu. To platí pro předdefinované životnosti s výjimkou $heap a $local.
  • Všechny typy T, které nejsou ref struct implicitně mají životnost T<$heap>. To je implicitní, není nutné psát int<$heap> v každé ukázce.
  • Pro pole ref definované jako ref<$l0> T<$l1, $l2, ... $ln>:
    • Všechny životnosti $l1$ln musí být invariantní.
    • Životnost $l0 musí být konvertibilní na $this
  • U ref definovaného jako ref<$a> T<$b, ...>musí být $b konvertibilní na $a
  • ref proměnné má dobu života definovanou:
    • U ref místního, parametru, pole nebo návratu typu ref<$a> T je životnost $a
    • $heap pro všechny odkazové typy a pole referenčních typů
    • $local pro všechno ostatní
  • Přiřazení nebo vrácení je legální, pokud je konverze základního typu legální.
  • Životnost výrazů lze explicitně vyjádřit pomocí přetypovacích anotací:
    • (T<$a> expr) hodnota má explicitní životnost $a pro T<...>
    • ref<$a> (T<$b>)expr životnost hodnoty je $b pro T<...> a životnost ref je $a.

Pro účely pravidel životnosti je ref považováno za součást typu výrazu v rámci konverzí. Logicky je reprezentována převodem ref<$a> T<...> na ref<$a, T<...>>, kde $a je kovariantní a T je invariantní.

Teď nadefinujme pravidla, která nám umožňují namapovat syntaxi jazyka C# na podkladový model.

V zájmu stručnosti se s typem, který nemá žádné explicitní parametry životnosti, zachází, jako by měl definovaný out $this, který se aplikuje na všechna pole typu. Typ s polem ref musí definovat explicitní parametry životnosti.

Tato pravidla podporují naši stávající invariantní funkci, kterou lze T přiřadit ke scoped T pro všechny typy. To odpovídá tomu, že T<$a, ...> lze přiřadit T<$local, ...> pro všechny doby životnosti, které jsou známy jako konvertibilní na $local. Dále to podporuje další položky, jako je možnost přiřazovat Span<T> z haldy těm, které jsou v zásobníku. Tím se vyloučí typy, u kterých pole mají odlišné životnosti pro hodnoty, které nejsou ref, ale to je realita jazyka C#. Změna by vyžadovala významnou úpravu pravidel jazyka C#, která by se musela podrobně naplánovat.

Typ this pro typ S<out $this, ...> uvnitř metody instance je implicitně definován jako následující:

  • Pro klasickou metodu instance: ref<$local> S<$cm, ...>
  • Například metoda s poznámkami [UnscopedRef]: ref<$ro> S<$cm, ...>

Nedostatek explicitního parametru this vyžaduje použití implicitních pravidel. U komplexních vzorků a diskuzí zvažte zvážit psaní jako metodu static a nastavit this jako explicitní parametr.

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) { }
}

Syntaxe metody C# je na model namapována následujícími způsoby:

  • parametry ref mají životnost ref $ro
  • Parametry typu ref struct mají takovou životnost $cm
  • Vracení hodnot ref má životnost ref $ro
  • návraty typu ref struct mají životnost hodnoty $ro
  • scoped u parametru nebo ref změní životnost odkazu tak, aby byla $local

Nyní se podívejme na jednoduchý příklad, který zde ukazuje 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;
}

Teď se podíváme na stejný příklad pomocí 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;
}

Teď se podíváme, jak to pomůže s cyklickým problémem vlastního přiřazení:

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;
    }
}

Dále se podíváme, jak to pomůže s hloupým problémem ohledně parametrů zachycení:

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;
    }
}

Otevřené problémy

Změna návrhu tak, aby nedocházelo k přerušení kompatibility

Tento návrh předkládá několik neslučitelných změn s našimi stávajícími pravidly pro bezpečný kontext referencí. I když se předpokládá, že změny mají minimální dopad, byla věnována značná pozornost návrhu, který neobsahoval žádné zásadní změny.

Návrh zachovávající kompatibilitu byl ale mnohem složitější než tento. Aby bylo možné zachovat kompatibilitu, pole ref potřebují jedinečné životnostní hodnoty, aby se mohly vrátit prostřednictvím pole ref a prostřednictvím pole ref. V podstatě to vyžaduje, abychom poskytli ref-field-safe-context sledování všech parametrů pro metodu. To je potřeba vypočítat pro všechny výrazy a sledovat ve všech hodnotách prakticky všude, kde ref-safe-context je sledován dnes.

Dále má tato hodnota vztahy s kontextem bezpečného odkazu . Například není smysluplné mít hodnotu, která může být vrácena jako pole ref, ale ne přímo jako ref. Je to proto, že ref pole mohou být triviálně vrácena ref (ref stav v ref struct lze vrátit ref, i když obsahující hodnotu nelze vrátit). Pravidla proto dále potřebují konstantní úpravy, aby se zajistilo, že tyto hodnoty jsou rozumné vzhledem k sobě navzájem.

Také to znamená, že jazyk potřebuje syntaxi k reprezentaci ref parametrů, které se mohou vrátit třemi různými způsoby: podle ref pole, pomocí ref a podle hodnoty. Je možné vrátit se k výchozímu nastavení pomocí ref. V budoucnu se očekává, že přirozenější návrat, zejména když se jedná o ref struct, bude přes pole ref nebo ref. To znamená, že nová rozhraní API vyžadují dodatečnou syntaktickou anotaci, aby byla ve výchozím nastavení správná. To je nežádoucí.

Tyto změny kompatibility však ovlivní metody s následujícími vlastnostmi.

  • Mít Span<T> nebo ref struct
    • Kde je ref struct návratový typ, ref nebo parametr out
    • Má další parametr in nebo ref (s výjimkou příjemce).

Abychom pochopili dopad, je užitečné rozdělit rozhraní API do kategorií:

  1. Chceme, aby spotřebitelé zohlednili, že ref je zachyceno jako pole ref. Příkladem jsou konstruktory Span(ref T value)
  2. Nepřejeme si, aby zákazníci brali v úvahu ref, které bylo zachyceno jako pole ref. Ty se ale dělí do dvou kategorií.
    1. Nebezpečná rozhraní API Jedná se o rozhraní API uvnitř typů Unsafe a MemoryMarshal, přičemž nejvýraznější je MemoryMarshal.CreateSpan. Tato rozhraní API zachytávají ref nebezpečně, ale jsou také známá jako nebezpečná rozhraní API.
    2. Bezpečná rozhraní API. Jedná se o rozhraní API, která přijímají ref parametrů pro efektivitu, ale ve skutečnosti nejsou nikde zaznamenány. Příklady jsou malé, ale jeden je AsnDecoder.ReadEnumeratedBytes

Tato změna primárně přináší výhody (1) výše. Očekává se, že většinu rozhraní API, která přijímají ref a vracejí ref struct, budou tvořit v budoucnu. Změny negativně ovlivňují (2.1) a (2.2), protože narušují stávající sémantiku volání v důsledku změny pravidel životnosti.

Rozhraní API v kategorii (2.1) jsou z velké části tvořená Microsoftem nebo vývojáři, kteří nejvíce těží z výhod polí ref (těmi, kteří jsou jako Tanner ve světě). Je rozumné předpokládat, že tato třída vývojářů by byla přívětivá k dani z kompatibility při upgradu na C# 11 ve formě několika anotací pro zachování stávající sémantiky, pokud by byla na oplátku poskytována pole ref.

Největší problém jsou rozhraní API v kategorii (2.2). Není známo, kolik takových rozhraní API existuje, a není jasné, jestli by to bylo více nebo méně časté v kódu třetí strany. Očekává se, že jejich počet je velmi malý, obzvláště pokud dojde k narušení kompatibility v případě out. Dosavadní hledání odhalilo, že na ploše public existuje velmi malý počet těchto. Je to těžké hledat, protože vyžaduje sémantickou analýzu. Před provedením této změny by byl potřeba přístup založený na nástrojích k ověření předpokladů souvisejících s tímto dopadem na malý počet známých případů.

U obou případů v kategorii (2) je oprava jednoduchá. Parametry ref, které nechtějí být považovány za zachytitelné, musí do scopedpřidat ref . V (2.1) to pravděpodobně také přiměje vývojáře použít Unsafe nebo MemoryMarshal, což se očekává u nejištěných rozhraní stylu API.

V ideálním případě může jazyk snížit dopad neznatelných změn způsobujících problémy tím, že vydá upozornění, když rozhraní API nepozorovaně se dostane do problematického chování. To by byla metoda, která vezme ref, vrátí ref struct, ale ve skutečnosti nezachytí ref v ref struct. Kompilátor by mohl v takovém případě vystavit diagnostiku informující vývojáře, ref by měla být místo toho označena jako scoped ref.

rozhodnutí Tento návrh lze dosáhnout, ale výsledná funkce je obtížnější použít do té míry, že bylo rozhodnuto přistoupit k narušení kompatibility.

Rozhodnutí Kompilátor zobrazí upozornění, když metoda splňuje kritéria, ale nezachytí parametr ref jako pole ref. To by mělo zákazníky při upgradu vhodně upozornit na potenciální problémy, které vytvářejí.

Klíčová slova vs. atributy

Tento návrh požaduje použití atributů pro anotaci nových pravidel životnosti. To se také dalo udělat stejně snadno pomocí kontextových klíčových slov. Například [DoesNotEscape] může být mapováno na scoped. Ale klíčová slova, i kontextová, obecně musí splňovat velmi vysokou laťku pro zahrnutí. Zabírají cenné místo v jazyce a jsou jeho výraznějšími součástmi. Tato funkce, i když je cenná, bude sloužit menšině vývojářů v jazyce C#.

Na první pohled se zdá, že není třeba používat klíčová slova, ale je třeba zvážit dva důležité body:

  1. Poznámky budou mít vliv na sémantiku programu. Mít atributy, které ovlivňují sémantiku programu, je hranice, kterou se C# zdráhá překročit, a není jasné, zda by tato funkce měla být dostatečným důvodem k tomu, aby jazyk tento krok učinil.
  2. Pravděpodobně vývojáři, kteří používají tuto funkci, se významně překrývají se skupinou vývojářů používajících ukazatele na funkce. Tato funkce, která je zároveň používána menšinou vývojářů, vyžadovala novou syntaxi a toto rozhodnutí je stále považováno za opodstatněné.

Celkově to znamená, že syntaxe by měla být zvážena.

Hrubá skica syntaxe by byla:

  • [RefDoesNotEscape] odpovídá scoped ref
  • [DoesNotEscape] odpovídá scoped
  • [RefDoesEscape] odpovídá unscoped

Rozhodnutí Použít syntaxi pro scoped a scoped ref; použít atribut pro unscoped.

Povolit pevné místní vyrovnávací paměti

Tento návrh umožňuje bezpečné fixed buffery, které podporují jakýkoli typ. Jedním z možných rozšíření je povolit deklaraci takových fixed vyrovnávacích pamětí jako místních proměnných. To by umožnilo nahrazení několika existujících operací stackalloc vyrovnávací pamětí fixed. Také by se rozšířila sada scénářů, kdy bychom mohli provádět alokace ve stylu zásobníku, protože stackalloc je omezený na nespravované typy prvků, zatímco fixed vyrovnávací paměť není.

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

To je celistvé, ale vyžaduje, abychom trochu rozšířili syntaxi pro lokály. Nejasné, jestli je to nebo nestojí za další složitost. Mohli bychom se prozatím rozhodnout pro ne a vrátit se k tomu později, pokud bude prokázána dostatečná potřeba.

Příklad, kde by to bylo výhodné: https://github.com/dotnet/runtime/pull/34149

Rozhodnutí zatím odložte

Použít modreqs nebo ne

Je třeba rozhodnout, zda by metody označené novými atributy životnosti měly nebo neměly být přeloženy jako modreq při výstupu v emit. Kdyby byl tento přístup použit, existovalo by v podstatě mapování 1:1 mezi poznámkami a modreq.

Odůvodněním přidání modreq jsou atributy, které mění sémantiku pravidel bezpečného kontextu ref. Příslušné metody by měly volat pouze jazyky, které těmto sémantikům rozumí. Při použití ve scénářích OHI se životnosti stanou kontraktem, který musí implementovat všechny odvozené metody. Pokud existují anotace bez modreq, může to vést k situacím, kdy jsou načteny řetězce metod virtual s konfliktními anotacemi životnosti (což může nastat, pokud je zkompilována jen jedna část řetězce virtual a druhá není).

Počáteční práce s bezpečným kontextem ref nepoužíla modreq, ale místo toho využívala jazyky a architekturu k pochopení. Ve stejnou dobu jsou všechny prvky, které přispívají k pravidlům bezpečného kontextu ref, silnou součástí podpisu metody: ref, in, ref structatd ... Proto jakákoli změna stávajících pravidel metody již vede k binární změně podpisu. Pokud chcete novým poznámkám o životnosti dát stejný dopad, bude potřeba modreq vynucení.

Otázkou je, zda je to přehnané. Negativní dopad spočívá v tom, že zvýšení flexibility podpisů, například přidáním [DoesNotEscape] do parametru, povede ke změně binární kompatibility. Tento kompromis znamená, že postupem času pravděpodobně rámce jako BCL nebudou moci takové podpisy uvolnit. Lze to zmírnit do určité míry použitím přístupu podobného tomu, jak jazyk pracuje s parametry in, a použitím modreq pouze ve virtuálních pozicích.

Rozhodnutí Nepoužívejte modreq v metadatech. Rozdíl mezi out a ref není modreq, ale nyní mají různé hodnoty bezpečné ref kontextu. Neexistuje žádná skutečná výhoda částečného vynucování pravidel s modreq zde.

Povolit multidimenzionální pevné vyrovnávací paměti

Má být návrh vyrovnávacích pamětí fixed rozšířen tak, aby zahrnoval vícerozměrná pole stylů? V podstatě umožňuje deklarace jako následující:

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

rozhodnutí Prozatím nepovolit

Porušení vymezeného rozsahu

Úložiště modulu runtime obsahuje několik neveřejných rozhraní API, která zaznamenávají parametry ref jako pole ref. Jsou nebezpečné, protože životnost výsledné hodnoty není sledována. Například konstruktor Span<T>(ref T value, int length).

Většina těchto rozhraní API se pravděpodobně rozhodne, že bude mít správné sledování životnosti u vrácené hodnoty, čehož lze dosáhnout tím, že se jednoduše aktualizuje na C# 11. Několik z nich ale bude chtít zachovat jejich aktuální sémantiku, aby nesledovaly návratovou hodnotu, protože jejich celý záměr je nebezpečný. Nejdůležitějšími příklady jsou MemoryMarshal.CreateSpan a MemoryMarshal.CreateReadOnlySpan. Toho dosáhnete tak, že parametry označíte jako scoped.

To znamená, že modul runtime potřebuje zavedený vzor pro nebezpečné odebrání scoped z parametru:

  1. Unsafe.AsRef<T>(in T value) by mohl rozšířit svůj stávající účel změnou na scoped in T value. To by umožnilo odebrat in i scoped z parametrů. Pak se stane univerzální metodou "remove ref safety" (odebrat bezpečnost ref).
  2. Zavést novou metodu, jejíž celý účel je odstranit scoped: ref T Unsafe.AsUnscoped<T>(scoped in T value). Tím se odebere i in, protože kdyby tomu tak nebylo, volající by stále potřebovali kombinaci volání metod ke "zrušení bezpečnosti ref", a v tom okamžiku je stávající řešení pravděpodobně postačující.

Zruší se výchozí rozsah?

Návrh má ve výchozím nastavení pouze dvě umístění, která jsou scoped:

  • this je scoped ref
  • out je scoped ref

Rozhodnutí o out je výrazně snížit kompatní zátěž ref polí a zároveň je přirozenějším výchozím nastavením. Umožňuje vývojářům vnímat out jako tok dat směřující pouze ven, zatímco pokud jde o ref, pravidla musí zohlednit tok dat v obou směrech. To vede k významným nejasnostem vývojářů.

Rozhodnutí o this je nežádoucí, protože to znamená, že struct nemůže vrátit pole pomocí ref. Jedná se o důležitý scénář pro vývojáře s vysokým výkonem a atribut [UnscopedRef] byl v podstatě přidán pro tento scénář.

Klíčová slova mají vysoké nároky a jejich přidání pouze pro jeden scénář je podezřelé. Uvažovalo se, zda bychom se mohli vyhnout tomuto klíčovému slovu tím, že by se this stalo jednoduše ref ve výchozím nastavení a ne scoped ref. Všichni členové, kteří potřebují, aby this bylo scoped ref, by to mohli udělat označením metody scoped (stejně jako je dnes možné označit metodu readonly pro vytvoření readonly ref).

Za normálních okolností u struct je to většinou pozitivní změna, protože představuje pouze problémy s kompatibilitou, když má člen vracející se ref. "Těchto metod je velmi málo a nástroj by je mohl rychle odhalit a převést na scoped členy."

Na ref struct tato změna přináší výrazně větší problémy s kompatibilitou. Vezměte v úvahu následující skutečnosti:

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;
    }
}

V podstatě by to znamenalo, že všechna volání instanční metody na proměnné místní proměnné by byla neplatná, pokud by místní proměnná nebyla dále označena jako . Pravidla musí vzít v úvahu případ, kdy byla pole znovu přiřazena jiným polím v this. readonly ref struct tento problém nemá, protože readonly příroda brání opětovnému přiřazení odkazu. Stále by to byla významná zpětně nekompatibilní změna, protože by ovlivnila prakticky všechny existující proměnlivé ref struct.

A readonly ref struct je stále problematické, jakmile rozšíříme pole ref až po ref struct. Umožňuje řešení stejného základního problému tím, že jednoduše přenáší zachycení do hodnoty pole 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);
    }
}

Bylo uvažováno o myšlence, že this má různé výchozí hodnoty na základě typu struct nebo člena. Například:

  • this jako ref: struct, readonly ref struct nebo readonly member
  • this jako scoped ref: ref struct nebo readonly ref struct s polem ref pro ref struct

Tím se minimalizuje přerušení kompatibility a maximalizuje flexibilita, ale za cenu komplikování scénáře pro zákazníky. Tento problém rovněž neřeší do plné míry, protože budoucí funkce, jako jsou bezpečné vyrovnávací paměti fixed, vyžadují, aby proměnlivé ref struct měly návraty ref pro pole, která nefungují pouze dle tohoto návrhu, jelikož by spadala do kategorie scoped ref.

Rozhodnutí zachovat this jako scoped ref. To znamená, že předchozí záludné příklady vytvářejí chyby kompilátoru.

ref pole na ref struktura

Tato funkce otevírá novou sadu bezpečnostních pravidel pro referenční kontext, protože umožňuje poli ref odkazovat na ref struct. Tato obecná povaha ByReference<T> znamenala, že runtime až dosud nemohl mít takovou konstrukci. V důsledku toho jsou všechna naše pravidla napsána za předpokladu, že to není možné. Funkce pole ref do značné míry nesděluje nová pravidla, ale kodifikuje stávající pravidla v našem systému. Povolení polí ref do ref struct vyžaduje, abychom zakotvili nová pravidla, protože je několik nových scénářů, které je potřeba zvážit.

První věc je, že readonly ref je nyní schopný uložit stav ref. Například:

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

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

To znamená, že když uvažujeme o argumentech metody, které musí odpovídat pravidlům, musíme vzít v úvahu, že readonly ref T je potenciální výstup metody, pokud T potenciálně obsahuje pole ref směrem k ref struct.

Druhý problém je, že jazyk musí zvážit nový typ bezpečného kontextu: ref-field-safe-context. Všechny ref struct, které tranzitivně obsahují pole ref, mají jiný únikový obor představující hodnotu (hodnoty) v polích ref. Pokud existuje více polí ref, mohou být sledována souhrnně jako jedna hodnota. Výchozí hodnota pro tyto parametry je kontext volajícího.

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

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

Tato hodnota nesouvisí s bezpečným kontextem kontejneru; to znamená, že když se kontext kontejneru zmenšuje, nemá to žádný vliv na ref-field-safe-context hodnot ref polí. Dále kontext ref-pole nemůže být nikdy menší než bezpečný kontext kontejneru.

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];
}

Tato kontextová ref-field-safe-context v podstatě existovala. Až dosud ref pole mohla odkazovat pouze na normální struct, proto byla jednoduše zredukována na kontext volajícího. Aby bylo možné podporovat pole ref pro ref struct, musí být naše stávající pravidla aktualizována, aby zohledňovala tento nový ref-safe-context .

Za třetí je potřeba aktualizovat pravidla pro opětovné přiřazení odkazu, abychom zajistili, že nedojde k porušení kontextu ref-field-context pro hodnoty. V podstatě pro x.e1 = ref e2, kde je typ e1ref struct, musí být ref-field-safe-context stejný.

Tyto problémy jsou velmi řešitelné. Tým kompilátoru navrhl několik různých verzí těchto pravidel, které z velké části vyplývají z naší stávající analýzy. Problém spočívá v tom, že neexistuje žádný kód pro taková pravidla, která pomáhají prokázat správnost a použitelnost. To nás velmi zdráhá přidat podporu kvůli obavám, že zvolíme nesprávné výchozí hodnoty a dostaneme runtime do problému s použitelností, když to bude využito. Tento problém je obzvláště závažný, protože .NET 8 nás pravděpodobně povede tímto směrem s allow T: ref struct a Span<Span<T>>. Bylo by lepší, kdyby byla pravidla napsána ve spojení s kódem spotřeby.

rozhodnutí Zpoždění umožňující ref pole ref struct až do .NET 8, kdy máme scénáře, které vám pomůžou řídit pravidla kolem těchto scénářů. Toto nebylo implementováno ve verzi .NET 9.

Co bude součástí C# 11.0?

Funkce popsané v tomto dokumentu není nutné implementovat v jediném průchodu. Místo toho je možné je implementovat ve fázích v několika jazykových verzích v následujících kontejnerech:

  1. ref polí a scoped
  2. [UnscopedRef]
  3. ref polí pro ref struct
  4. Typy s omezenými západy slunce
  5. vyrovnávací paměti s pevnou velikostí

Které funkce se implementují v jaké verzi, je pouze proces určování rozsahu.

Rozhodnutí Pouze (1) a (2) se dostaly do C# 11.0. Zbytek se bude brát v úvahu v budoucích verzích jazyka C#.

Důležité informace o budoucnosti

Rozšířené poznámky k životnímu cyklu

Poznámky k životnosti v tomto návrhu jsou omezené v tom, že umožňují vývojářům změnit výchozí chování hodnot, zda mají být nebo nemají být unikátně upraveny (escape). To do našeho modelu přidává silnou flexibilitu, ale nijak výrazně nemění sadu relací, které je možné vyjádřit. V jádru je model jazyka C# v zásadě stále binární: může být hodnota vrácena, nebo ne?

To umožňuje pochopení relací s omezenou životností. Například hodnota, kterou nelze vrátit z metody, má menší životnost než ta, která může být vrácena z metody. Neexistuje způsob, jak popsat vztah trvání mezi hodnotami, které mohou být vráceny z metody. Konkrétně neexistuje způsob, jak říci, že jedna hodnota má delší životnost než druhá, jakmile je zjištěno, že obě mohou být vráceny z metody. Dalším krokem v našem životním vývoji by bylo umožnit, aby tyto vztahy byly popsány.

Jiné metody, jako Rust, umožňují vyjádřit tento typ relace a proto dokážou implementovat složitější operace stylu scoped. Náš jazyk by mohl mít podobné výhody, pokud by byla taková funkce zahrnuta. V tuto chvíli není žádný motivační tlak dělat to, ale pokud v budoucnu bude, náš model scoped by mohl být rozšířen tak, aby to zahrnul poměrně přímočarým způsobem.

Každé scoped může být přiděleno pojmenovaná doba trvání přidáním obecného stylového argumentu do syntaxe. Například scoped<'a> je hodnota, která má životnost 'a. Omezení, jako je where, se pak dají použít k popisu vztahů mezi těmito životnostmi.

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

Tato metoda definuje dvě životnosti 'a a 'b a jejich vztah, konkrétně že 'b je větší než 'a. Toto umožňuje místu volání mít podrobnější pravidla pro bezpečné předávání hodnot do metod, oproti hrubším odstupňovaným pravidlům, která jsou dnes k dispozici.

Problémy

Všechny tyto otázky souvisejí s tímto návrhem:

Návrhy

K tomuto návrhu se vztahují následující návrhy:

Existující ukázky

Utf8JsonReader

Tento konkrétní fragment kódu vyžaduje nebezpečné, protože narazí na problémy s předáváním Span<T>, které lze přidělit metodě instance v ref struct. I když tento parametr není zachycený, jazyk musí předpokládat, že tomu tak je, a proto zbytečně způsobuje tření.

Utf8JsonWriter

Tento fragment kódu chce změnit parametr odstraněním speciálních znaků z dat. Uvolněná data mohou být umístěna na zásobník pro zvýšení efektivity. I když parametr není řídicím znakem, kompilátor ho přiřadí bezpečné kontextové mimo uzavřenou metodu, protože se jedná o parametr. To znamená, že aby bylo možné použít přidělení zásobníku, musí implementace používat unsafe, aby se po zapouzdření dat vrátila k parametru.

Zábavné ukázky

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;
    }
}

Šetrný seznam

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;
            }
        }
    }
}

Příklady a poznámky

Níže najdete sadu příkladů, které ukazují, jak a proč pravidla fungují tak, jak fungují. Součástí je několik příkladů, které ukazují nebezpečné chování a jak jim pravidla brání v jejich vzniku. Při úpravách návrhu je důležité je mít na paměti.

Přiřazení reference a místa volání

Demonstrující, jak opětovné přiřazení ref a vyvolání metody vzájemně spolupracují.

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;
    }
}

Opětovné přiřazení reference a nebezpečné úniky

Důvod pro následující řádek v pravidlech přeřazení odkazů nemusí být na první pohled zřejmý:

e1 musí mít stejné bezpečné kontextové jako e2

Důvodem je to, že životnost hodnot, na které odkazuje ref umístění, je invariantní. Zprostředkování nám brání povolit jakoukoli variabilitu, dokonce i směrem k užším životnostem. Pokud je zúžení povoleno, otevře se následující nebezpečný kód:

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];
}

Pro ref a ne ref struct je toto pravidlo triviálně splněno, protože všechny hodnoty mají stejný bezpečný kontext. Toto pravidlo skutečně přichází do hry pouze v případě, že hodnota je ref struct.

Toto chování ref bude také důležité v budoucnu, kdy povolíme pole refref struct.

místní prostředí s vymezeným oborem

Použití scoped u místních proměnných bude zvlášť užitečné pro vzory kódu, které podmíněně přiřazují hodnoty s různým bezpečným kontextem těmto proměnným. Znamená to, že kód už nemusí spoléhat na inicializační triky, jako je třeba použití = stackalloc byte[0] pro definování lokálního bezpečného kontextu , ale nyní může jednoduše použít 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];
}

Tento vzor se často objevuje v kódu nízké úrovně. Když je ref struct zapojen do Span<T>, lze použít výše uvedený trik. Nevztahuje se ale na jiné typy ref struct a může vést k tomu, že se kód nízké úrovně musí uchýlit k unsafe, aby se obešlou nemožnost správně určit životnost.

hodnoty parametrů s vymezeným oborem

Jedním ze zdrojů opakovaného tření v programovém kódu nízké úrovně je, že výchozí unik pro parametry je povolující. Jsou bezpečné kontextové dokontextu volajícího . Toto je rozumné výchozí nastavení, protože je v souladu se vzory kódování .NET jako celku. V nízkoúrovňovém kódu je větší využití ref struct a toto výchozí nastavení může způsobit konflikt s jinými částmi pravidel bezpečného kontextu ref.

K hlavnímu třecímu bodu dochází, protože argumenty metody musí odpovídat pravidlu. Toto pravidlo se nejčastěji uplatňuje u metod instance na ref struct, kde je alespoň jeden parametr také ref struct. Jedná se o běžný vzor v kódu nízké úrovně, kde ref struct typy běžně využívají parametry Span<T> ve svých metodách. Například k tomu dojde v jakémkoli stylu zápisu ref struct, který používá Span<T> pro předávání vyrovnávacích pamětí.

Toto pravidlo existuje, aby se zabránilo scénářům, jako jsou následující:

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);
    }
}

V podstatě toto pravidlo existuje, protože jazyk musí předpokládat, že všechny vstupy do metody uniknou do svého maximálního povoleného bezpečného kontextu. Pokud existují parametry ref nebo out, včetně přijímačů, je možné, že vstupy se objeví ve formě polí těchto hodnot ref, jak je znázorněno výše u RS.Set.

V praxi existuje mnoho takových metod, které předávají ref struct jako parametry, které je nikdy nechystají zachytit ve výstupu. Jedná se pouze o hodnotu, která se používá v rámci aktuální metody. Například:

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 bylo možné obejít tento nízkoúrovňový kód, bude se uchylovat k trikům unsafe, které podvádějí kompilátor ohledně životnosti jejich ref struct. Tím se výrazně sníží hodnota ref struct, protože jsou to prostředky, které umožňují vyhnout se unsafe zatímco pokračujete v psaní vysoce výkonného kódu.

To je místo, kde scoped je účinný nástroj pro parametry ref struct, protože je vyloučí z úvah o návratu z metody, v souladu s aktualizovanými pravidly pro argumenty metody musí odpovídat pravidlu. Parametr ref struct, který se spotřebovává, ale nikdy nevrací, je možné označit jako scoped, aby byly weby volání flexibilnější.

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))
        {
            ...
        }
    }
}

Zabránění složitému přiřazení odkazu z mutace omezené pouze na čtení

Když je ref přenesen do pole readonly v konstruktoru nebo členu init, typ je ref a ne ref readonly. Toto je dlouhodobé chování, které umožňuje kód podobný následujícímu:

struct S
{
    readonly int i; 

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

    static void M(ref int i) { }
}

To ale představuje potenciální problém, pokud by takový ref mohl být uložen do pole ref stejného typu. Umožnilo by přímou mutaci readonly struct z člena instance:

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++;
    }
}

Návrh to ale brání, protože porušuje pravidla bezpečného kontextu ref. Vezměte v úvahu následující skutečnosti:

  • bezpečného kontextuthis je člen funkce a bezpečného kontextu je kontext volajícího. Oba jsou standardní pro this ve struct členu.
  • ref-safe-context z i je členem funkce. To vyplývá z pravidel životnosti pole . Konkrétně pravidlo 4.

V tomto okamžiku je řádek r = ref i neplatný podle pravidel opětovného přiřazení .

Tato pravidla nebyla určena k zabránění tomuto chování, ale jako vedlejší účinek. Je důležité mít na paměti, že každá budoucí aktualizace pravidel vyhodnotí dopad na takové scénáře.

Hloupé cyklické přiřazení

Jeden aspekt, se kterým se tento návrh potýkal, je to, jak volně lze vrátit ref z metody. Pravděpodobně většina vývojářů intuitivně očekává, že vrácení všech ref jako běžných hodnot bude povoleno. Umožňuje však patologickým scénářům, které kompilátor musí při výpočtu bezpečnosti ref zvážit. Vezměte v úvahu následující skutečnosti:

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;
    }
}

Nejedná se o vzor kódu, který očekáváme, že budou používat všichni vývojáři. I když je možné vrátit ref se stejnou životností jako hodnota, je to legální podle pravidel. Kompilátor musí při vyhodnocování volání metody zvážit všechny právní případy, což vede k efektivnímu nepoužitelnému rozhraní API.

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 bylo možné tato rozhraní API použít, kompilátor zajistí, aby ref životnost parametru ref byl menší než životnost odkazů v přidružené hodnotě parametru. To je důvod, proč mít ref-safe-context být návratové a být kontextu volajícího. To brání cyklickému přiřazení z důvodu rozdílu v životnosti.

Upozorňujeme, že [UnscopedRef]podporuje přenos kontextu ref-safe-context libovolných ref až na ref struct hodnoty do kontextu volajícího. Proto to umožňuje cyklické přiřazení a nutí k virálnímu použití [UnscopedRef] v rámci volacího řetězce.

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;
    }
}

Podobně umožňuje cyklické přiřazení, protože parametr má bezpečného kontextu i návratových.

Zvýšení úrovně [UnscopedRef] ref na v kontextu volajícího je užitečné, pokud typ neníref struct (mějte na paměti, že chceme zachovat jednoduchá pravidla, aby nerozlišovaly odkazy na odkaz a struktury bez odkazu):

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;
    }
}

Z hlediska pokročilých poznámek vytvoří návrh [UnscopedRef] následující:

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

Pole označená jen pro čtení nelze zpracovávat do hloubky přes referenční pole.

Podívejte se na následující ukázku kódu:

ref struct S
{
    ref int Field;

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

Při navrhování pravidel pro ref pole na instancích readonly ve vakuu lze platná pravidla navrhnout tak, aby to, co je výše uvedeno, bylo legální nebo nelegální. V podstatě readonly může být platný hluboko v rámci ref pole nebo se může vztahovat pouze na ref. Použití pouze pro ref zabraňuje opětovnému přiřazení odkazu, ale umožňuje normální přiřazení, které změní odkazovanou hodnotu.

Tento návrh však neexistuje izolovaně, ale navrhuje pravidla pro typy, které již fakticky mají pole ref. Nejvýznamnější z nich, Span<T>, již má silnou závislost na tom, že readonly zde není příliš hluboko. Primárním scénářem je možnost přiřadit pole ref přes instanci 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;
        }
    }
}

To znamená, že musíme zvolit povrchní výklad readonly.

Konstruktory modelování

Jednou z drobných otázek návrhu je, jak jsou těla konstruktorů modelována pro bezpečnost ref. Jak se v podstatě analyzuje následující konstruktor?

ref struct S
{
    ref int field;

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

Existují zhruba dva přístupy:

  1. Model jako metoda static, kde this je místní, kde bezpečný kontext je kontext volajícího
  2. Model jako metoda static, kde this je parametr out.

Dále musí konstruktor splňovat následující invarianty:

  1. Ujistěte se, že ref parametry lze zachytit jako pole ref.
  2. Ujistěte se, že ref k polím this nelze uvést pomocí parametrů ref. To by porušilo komplikované přiřazení odkazu.

Záměrem je vybrat formulář, který splňuje naše invarianty bez zavedení jakýchkoli zvláštních pravidel pro konstruktory. Vzhledem k tomu, že nejlepší model pro konstruktory zobrazuje this jako parametr out. Pouze vrací charakter out nám umožňuje uspokojovat všechny výše uvedené invarianty bez nutnosti zvláštního zpracování.

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 se musí shodovat.

Argumenty metody musí odpovídat pravidlu, což je běžným zdrojem nejasností pro vývojáře. Jedná se o pravidlo, které má řadu zvláštních případů, které jsou obtížně pochopitelné, pokud neznáte odůvodnění pravidla. Pro lepší pochopení důvodů pro toto pravidlo zjednodušíme ref-bezpečný-kontext a bezpečný-kontext na jednoduše kontext.

Metody mohou poměrně svobodně vracet stav předaný jim jako parametry. V podstatě jakýkoli dosažitelný stav, který je bez rozsahu, může být vrácen (včetně vrácení pomocí ref). To lze vrátit přímo příkazem return nebo nepřímo přiřazením k hodnotě ref.

Přímé návraty nepředstavují mnoho problémů s bezpečností ref. Kompilátor se jednoduše musí podívat na všechny návratové vstupy metody a pak efektivně omezuje návratovou hodnotu na minimální kontext vstupu. Tato vrácená hodnota pak prochází normálním zpracováním.

Nepřímé návraty představují významný problém, protože všechny ref jsou vstupem i výstupem metody. Tyto výstupy již mají známý kontext . Kompilátor nemůže odvodit nové, musí je zvážit na aktuální úrovni. To znamená, že kompilátor se musí podívat na každé ref, které lze přiřadit v volané metodě, vyhodnotit jejich kontext , a pak ověřit, že žádný vstup do metody nemá menší kontext než jakýkoli ref. Pokud takový případ existuje, volání metody musí být nezákonné, protože by mohlo narušit ref bezpečnost.

Argumenty metody se musí shodovat s procesem, pomocí kterého kompilátor tuto bezpečnostní kontrolu používá.

Dalším způsobem, jak to vyhodnotit, což je pro vývojáře často jednodušší, je provést následující cvičení:

  1. Podívejte se na definici metody identifikujte všechna místa, kde může být stav nepřímo vrácen: a. Proměnlivé parametry ref ukazující na ref struct b. Proměnlivé parametry ref s poli přiřaditelnými pomocí klíčového slova ref ref. Přidělitelné parametry ref nebo pole ref, které ukazují na ref struct (vezměte v úvahu rekurzi).
  2. Podívejte se na místo volání a. Identifikujte kontexty, které jsou v souladu s umístěními identifikovanými nad b. Identifikujte kontexty všech vstupů metody, které je možné vrátit (nezarovnávejte se s parametry scoped).

Pokud je jakákoli hodnota v 2.b menší než 2.a, volání metody musí být neplatné. Podívejme se na několik příkladů, které ilustrují pravidla:

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);
    }
}

Podívejme se na volání F0 a projděme si body (1) a (2). Parametry s potenciálem nepřímého vrácení jsou a a b, protože je možné je přímo přiřadit. Argumenty, které odpovídají těmto parametrům, jsou:

  • , které se mapuje na , má kontext volajícího-kontextu
  • b, který je mapován na y, a který má kontext členu funkce

Sada návratového vstupu metody je

  • s řídicího rozsahu kontextu volajícího
  • s řídicího rozsahu kontextu volajícího
  • y s únikovým-rozsahem člena funkce

Hodnota ref y není vrátitelná, protože se mapuje na scoped ref proto se nepovažuje za vstup. Vzhledem k tomu, že existuje alespoň jeden vstup s menším únikovým rozsahem (y argument) než jeden z výstupů (x argument), je volání metody neplatné.

Jiná varianta je následující:

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);
    }
}

Opět jsou parametry s potenciálem nepřímého vrácení a a b, protože oba můžou být přímo přiřazeny. Ale b lze vyloučit, protože neodkazuje na ref struct proto nelze použít k ukládání ref stavu. Máme tedy:

  • , které se mapuje na , má kontext volajícího-kontextu

Sada vstupů, které lze vrátit metodou, jsou:

  • s kontextem kontextu volajícího
  • s kontextem kontextu volajícího
  • ref y s kontext funkce-člen

Vzhledem k tomu, že existuje alespoň jeden vstup s menším řídicím rozsahem (ref y argument) než jeden z výstupů (x argument), volání metody je neplatné.

Toto je logika, kterou argumenty metody musí shodovat s pravidlem, se snaží zahrnout. V této souvislosti se uvažuje jak scoped jako způsob odstranění vstupů z úvahy, tak readonly jako způsob odstranění ref jako výstupu (nelze přiřadit do readonly ref, takže nemůže být zdrojem výstupu). Tyto zvláštní případy přidávají do pravidel složitost, ale dělají se tak pro výhodu vývojáře. Kompilátor se snaží odebrat všechny vstupy a výstupy, které ví, že nemůže přispět k výsledku, aby vývojáři při volání člena dosáhli maximální flexibility. Podobně jako řešení přetížení stojí za to, aby naše pravidla byla složitější, když vytváří větší flexibilitu pro uživatele.

Příklady bezpečného kontextu odvozených výrazů deklarace

Související s odvozením bezpečného kontextu výrazů deklarace.

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

Všimněte si, že místní kontext, který je výsledkem modifikátoru scoped, je nejužší, který by mohl být použit pro proměnnou, což by znamenalo, že výraz odkazuje na proměnné deklarované pouze v užším kontextu než samotný výraz.