Dela via


Förbättringar av låg nivå-strukturer

Not

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader återspeglas i de relevanta LDM-anteckningarna (Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Sammanfattning

Det här förslaget är en sammanställning av flera olika förslag för struct prestandaförbättringar: ref fält och förmågan att åsidosätta standardvärden för livslängd. Målet är en design som tar hänsyn till de olika förslagen för att skapa en enda övergripande uppsättning av funktioner för lågnivåförbättringar för struct.

Obs! Tidigare versioner av den här specifikationen använde termerna "ref-safe-to-escape" och "safe-to-escape", som introducerades i Span safety funktionalitetsbeskrivning. ECMA-standardkommittén ändrat namnen till "ref-safe-context" respektive "safe-context". Värdena för den säkra kontexten har förfinats så att de använder "declaration-block", "function-member" och "caller-context" konsekvent. Specletarna hade använt olika formuleringar för dessa termer och även använt "safe-to-return" som synonym för "caller-context". Den här specifikationen har uppdaterats för att använda termerna i C# 7.3-standarden.

Alla funktioner som beskrivs i det här dokumentet har inte implementerats i C# 11. C# 11 innehåller:

  1. ref fält och scoped
  2. [UnscopedRef]

Dessa funktioner är fortfarande öppna förslag för en framtida version av C#:

  1. ref fält till ref struct
  2. Begränsade typer av solnedgångar

Motivation

Tidigare versioner av C# har lagt till ett antal prestandafunktioner på låg nivå till språket: ref returnerar, ref struct, funktionspekare osv. ... Dessa gjorde det möjligt för .NET-utvecklare att skriva kod med hög prestanda samtidigt som de fortsatte att använda C#-språkreglerna för typ- och minnessäkerhet. Det gjorde det också möjligt att skapa grundläggande prestandatyper i .NET-bibliotek som Span<T>.

Eftersom dessa funktioner har fått fäste i .NET-ekosystemet har utvecklare, både interna och externa, gett oss information om återstående friktionspunkter i ekosystemet. Platser där de fortfarande behöver gå ner till unsafe-kod för att få sitt arbete gjort, eller där körningen behöver hantera specialfall för typer som Span<T>.

I dag utförs Span<T> med hjälp av den internal typ ByReference<T> som körningen effektivt behandlar som ett ref fält. Detta ger fördelen med ref fält men med nackdelen att språket inte ger någon säkerhetsverifiering för det, som det gör för andra användningar av ref. Dessutom kan endast dotnet/runtime använda den här typen eftersom det är internal, så att tredje part inte kan utforma sina egna primitiver baserat på ref fält. En del av motivationen för det här arbetet är att ta bort ByReference<T> och använda korrekta ref fält i alla kodbaser.

Det här förslaget planerar att ta itu med dessa problem genom att bygga vidare på våra befintliga lågnivåfunktioner. Mer specifikt syftar det till att:

  • Tillåt ref struct typer att deklarera ref fält.
  • Tillåt körningen att helt definiera Span<T> med hjälp av C#-typsystemet och ta bort specialfallstyp som ByReference<T>
  • Tillåt att struct typer returnerar ref till sina fält.
  • Tillåt körningstid att ta bort unsafe användningsområden som orsakas av begränsningar i livstidens standardinställningar
  • Tillåt deklaration av säkra fixed buffertar för hanterade och ohanterade typer i struct

Detaljerad design

Reglerna för ref struct-säkerhet definieras i säkerhetsdokumentet med hjälp av tidigare termer. Dessa regler har införlivats i C# 7-standarden i §9.7.2 och §16.4.12. Det här dokumentet beskriver de nödvändiga ändringarna i det här dokumentet som ett resultat av det här förslaget. När de har godkänts som en godkänd funktion införlivas ändringarna i dokumentet.

När den här designen är klar blir vår Span<T> definition följande:

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

Ange referensfält och avgränsningar

Språket gör det möjligt för utvecklare att deklarera ref fält inom en ref struct. Till exempel kan detta vara användbart när du kapslar in stora ändringsbara struct-instans eller definierar högprestandatyper som Span<T> i bibliotek utöver körningen.

ref struct S 
{
    public ref int Value;
}

Ett ref-fält kommer att emitteras i metadata med ELEMENT_TYPE_BYREF-signaturen. Det här skiljer sig inte från hur vi emitterar ref lokaler eller ref argument. Till exempel genereras ref int _field som ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. Detta kräver att vi uppdaterar ECMA335 för att tillåta den här posten, men det bör vara ganska enkelt.

Utvecklare kan fortsätta att initiera en ref struct med ett ref-fält genom default-uttrycket, och i så fall kommer alla deklarerade ref-fält att ha värdet null. Alla försök att använda sådana fält resulterar i att en NullReferenceException genereras.

ref struct S 
{
    public ref int Value;
}

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

Medan C#-språket låtsas att en ref inte kan vara null är detta tillåtet på körningsnivå och har väldefinierad semantik. Utvecklare som introducerar ref fält i sina typer måste vara medvetna om den här möjligheten och bör starkt avrådas från att läcka den här informationen till koden som använder dem. I stället bör ref fält valideras som icke-null med hjälp av runtime-hjälparna och utlösa när en oinitierad struct används felaktigt.

ref struct S1 
{
    private ref int Value;

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

        return Value;
    }
}

Ett ref fält kan kombineras med readonly modifierare på följande sätt:

  • readonly ref: det här är ett fält som inte kan tilldelas om utanför en konstruktör eller init-metoder. Det kan tilldelas ett värde men utanför dessa sammanhang
  • ref readonly: det här är ett fält vars referens kan tilldelas om, men som inte kan tilldelas något värde vid något tillfälle. Så här kan en in-parameter omrefereras till ett ref-fält.
  • readonly ref readonly: en kombination av ref readonly och 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)
    }
}

En readonly ref struct kommer att kräva att ref fält deklareras som readonly ref. Det finns inget krav på att de deklareras readonly ref readonly. Detta gör det möjligt för en readonly struct att ha indirekta mutationer via ett sådant fält, men det skiljer sig inte från ett readonly fält som pekade på en referenstyp idag (mer information)

En readonly ref skickas till metadata med flaggan initonly, precis som vilket annat fält som helst. Ett fält ref readonly tillskrivs System.Runtime.CompilerServices.IsReadOnlyAttribute. En readonly ref readonly genereras med båda objekten.

Den här funktionen kräver körningsstöd och ändringar i ECMA-specifikationen. Därför aktiveras dessa endast när motsvarande funktionsflagga anges i corelib. Problemet med att spåra det exakta API:et spåras här https://github.com/dotnet/runtime/issues/64165

Den uppsättning ändringar av våra säkra kontextregler som krävs för att tillåta ref fält är små och målinriktade. Reglerna tar redan hänsyn till att ref-fält existerar och konsumeras via API:er. Ändringarna behöver bara fokusera på två aspekter: hur de skapas och hur de tilldelas om.

Först måste reglerna som fastställer referenssäker kontext värden för fält uppdateras för ref fält på följande sätt:

Ett uttryck i formuläret ref e.Fref-safe-context på följande sätt:

  1. Om F är ett ref-fält är dess ref-säkra-kontextsäkra-kontext för e.
  2. Annars om e är av en referenstyp, har den referenssäker kontext av uppringningskontext
  3. Annars tas dess ref-safe-context från ref-safe-context av e.

Detta representerar dock inte en regeländring eftersom reglerna alltid har redovisat ref tillstånd som ska finnas i en ref struct. Detta är i själva verket hur ref-tillståndet i Span<T> alltid har fungerat, och förbrukningsreglerna beaktar detta korrekt. Ändringen här handlar bara om att utvecklare ska kunna komma åt ref fält direkt och se till att de gör det enligt befintliga regler som implicit tillämpas på Span<T>.

Detta innebär dock att ref-fält kan returneras som ref från en ref struct, men normala fält kan inte returneras.

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

Detta kan verka som ett fel vid första anblicken, men det här är en avsiktlig designpunkt. Även om detta inte är en ny regel som skapas av det här förslaget, erkänner det i stället de befintliga reglerna som Span<T> har följt hittills, nu när utvecklare kan deklarera sina egna ref-tillstånd.

Därefter måste reglerna för omtilldelning av referens justeras för förekomsten av ref fält. Det primära scenariot för referenstilldelning är ref struct konstruktorer som lagrar ref parametrar i ref fält. Stödet blir mer allmänt, men det här är kärnscenariot. För att stödja detta kommer reglerna för omtilldelning att justeras för att inkludera ref-fält på följande sätt:

Referensregler för omtilldelning

Den vänstra operanden för operatorn = ref måste vara ett uttryck som binder till en lokal referensvariabel, en referensparameter (förutom this), en out-parameter, eller ett referensfält.

För en referenstilldelning i formuläret e1 = ref e2 måste båda följande vara sanna:

  1. e2 måste ha referenssäker kontext minst lika stor som referenssäker kontext för e1
  2. e1 måste ha samma safe-context som e2Note

Det innebär att den önskade Span<T> konstruktorn fungerar utan någon extra anteckning:

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

Ändringen av reglerna för återtilldelning innebär att ref-parametrar nu kan undkomma från en metod som ett ref-fält i ett ref struct-värde. Som diskuterats i avsnittet kompatibilitetsöverväganden, kan detta ändra reglerna för befintliga API:er som aldrig var avsedda för ref-parameter att läcka som ett ref-fält. Livslängdsreglerna för parametrar baseras enbart på deras deklaration, inte på deras användning. Alla parametrar för ref och in har referenssäker kontext av anroparkontext och kan därför nu returneras av ref eller ett ref fält. För att stötta API:er som har ref-parametrar som kan undkomma eller inte kan undkomma, och därmed återställa C# 10-anropsställetik, kommer språket att införa begränsade livstidsanteckningar.

scoped modifierare

Nyckelordet scoped används för att begränsa livslängden för ett värde. Det kan tillämpas på en ref eller ett värde som är en ref struct och har effekten att begränsa referenssäker kontext eller säker kontext livslängd till funktionsmedlem. Till exempel:

Parameter eller lokal ref-safe-context säker kontext
Span<int> s funktionsmedlem anropskontext
scoped Span<int> s funktionsmedlem funktionsmedlem
ref Span<int> s anropskontext anropskontext
scoped ref Span<int> s funktionsmedlem anropskontext

I den här relationen kan referenssäker kontext av ett värde aldrig vara bredare än den säker kontext.

Detta gör att API:er i C# 11 kan kommenteras så att de har samma regler som 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]);
}

Den scoped kommentaren innebär också att parametern this för en struct nu kan definieras som scoped ref T. Tidigare behövde detta hanteras som ett specialfall i reglerna som ref parameter som hade andra ref-safe-context regler än de övriga ref parametrarna (se alla referenser till hur mottagaren inkluderas eller exkluderas i reglerna för säker kontext). Nu kan det uttryckas som ett allmänt begrepp i de regler som ytterligare förenklar dem.

Den scoped annoteringen kan också tillämpas på följande platser:

  • locals(locals): Denna annotering anger livslängd till safe-context, eller referenssäker kontext i händelse av en ref lokal, för en funktionsmedlem oavsett livslängden för initieraren.
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];
}

Andra användningsområden för scoped på lokalbefolkningen beskrivs nedan.

Den scoped-anteckningen kan inte tillämpas på några andra platser, inklusive returvärden, fält, matriselement osv. Även om scoped har inverkan när den tillämpas på alla ref, in eller out, har den endast effekt när den tillämpas på värden som är ref struct. Att ha deklarationer som scoped int har ingen inverkan eftersom en icke-ref struct alltid är säker att återvända. Kompilatorn skapar en diagnostik för sådana fall för att undvika utvecklarförvirring.

Ändra beteendet för out parametrar

För att ytterligare begränsa påverkan av kompatibilitetsförändringen där ref och in parametrar görs returnerbara som ref fält, kommer språket att ändra standardvärdet för ref-safe-context för out parametrar till att vara funktionsmedlem. I praktiken är out parametrar underförstått scoped out framöver. Ur ett kompatibilitetsperspektiv innebär det att de inte kan returneras av ref:

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

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

Detta ökar flexibiliteten för API:er som returnerar ref struct värden och har out parametrar eftersom den inte längre behöver överväga att parametern registreras med referens. Detta är viktigt eftersom det är ett vanligt mönster i API:er i läsarformat:

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

Språket anser inte längre att argument som skickas till en out parameter kan returneras. Att behandla indata till en out parameter som returnerbar var mycket förvirrande för utvecklare. Det undergräver i huvudsak avsikten med out genom att tvinga utvecklare att ta hänsyn till det värde som den som anropar skickar, vilket aldrig används utom i språk som inte respekterar out. Framöver måste språk som stöder ref struct se till att det ursprungliga värdet som skickas till en out-parameter aldrig läsas.

C# uppnår detta via sina bestämda tilldelningsregler. Att uppnå våra regler för en referenssäker kontext samt tillåta befintlig kod som tilldelar och sedan returnerar värden i out-parametrar.

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

Tillsammans innebär dessa ändringar att argumentet till en out-parameter inte bidrar med safe-context- eller ref-safe-context-värden till metodanrop. Detta minskar avsevärt den övergripande kompatibilitetseffekten för ref fält samt förenklar hur utvecklare tänker på out. Ett argument till en out parameter bidrar inte till returen, det är bara utdata.

Härled säker kontext av deklarationsuttryck

säker kontext för en deklarationsvariabel från ett out argument (M(x, out var y)) eller dekonstruering ((var x, var y) = M()) är den snävaste av följande:

  • uppringarens sammanhang
  • om utvariabeln är markerad scoped, då är deklarationsblock (dvs. funktionsmedlem eller smalare).
  • om ut-variabelns typ är ref structbör du överväga alla argument för det innehållande anropet, inklusive mottagaren:
    • säker kontext av alla argument, där motsvarande parameter inte är out och har säker kontext av endast retur eller bredare
    • referenssäker kontext för alla argument där motsvarande parameter har referenssäker kontext av eller bredare

Se också exempel på härledd säker kontext i deklarationsuttryck.

Implicita scoped parametrar

Totalt finns det två ref platser som implicit deklareras som scoped:

  • this på en struct-instansmetod
  • out parametrar

Referenssäkerhetskontextreglerna skrivs i termer av scoped ref och ref. I referenssäker kontext motsvarar en in parameter ref och out motsvarar scoped ref. Både in och out kommer endast att framhävas när det är viktigt för regelns semantik. Annars betraktas de bara som ref respektive scoped ref.

När du diskuterar ref-safe-context, i argument som motsvarar in parametrar, generaliseras de som ref argument i specifikationen. Om argumentet är en lvalue, är ref-safe-context lvaluen, annars är det function-member. Återigen kommer in bara att anropas här när det är viktigt för den aktuella regelns semantik.

Säkerhetskontext endast för retur

Designen kräver dessutom att en ny säker kontext införs: återfinns endast. Detta liknar anroparkontext eftersom det kan returneras, men det kan bara returneras via en return-instruktion.

Informationen om returendast är att det är en kontext som är större än funktionselement men mindre än anroparekontext. Ett uttryck som tillhandahålls till en return-instruktion måste vara minst endast returnera. Som sådan blir de flesta av de befintliga reglerna irrelevanta. Tilldelning till en ref-parameter från ett uttryck med en säker kontext av returneringsbegränsad misslyckas eftersom den är mindre än ref parameterns säker kontext som är anroparkontext. Behovet av den här nya flyktkontexten diskuteras nedan.

Det finns tre platser där standarden är för endast retur:

  • En ref- eller in-parameter har en referenssäker kontext. Detta görs delvis för ref struct för att förhindra problem med fåniga cykliska tilldelningar. Det görs dock enhetligt för att förenkla modellen samt minimera kompatibilitetsändringar.
  • En out-parameter för en ref struct kommer att ha säker kontext av endast för retur. Detta gör att retur och out kan vara lika uttrycksfulla. Detta lider inte av det fåniga cykliska tilldelningsproblemet eftersom out implicit är scoped, vilket innebär att ref-safe-context fortfarande är mindre än safe-context.
  • En this parameter för en struct konstruktor har en säker kontext. Detta faller ut på grund av att modelleras som out parametrar.

Alla uttryck eller uttryck som uttryckligen returnerar ett värde från en metod eller lambda måste ha en safe-context, och om tillämpligt en ref-safe-context, med minst return-only. Det inkluderar return-instruktioner, medlemmar med uttryckskropp och lambda-uttryck.

På samma sätt måste alla tilldelningar till en out ha en säker kontext som är åtminstone return-only. Detta är dock inte ett specialfall, detta följer bara från de befintliga tilldelningsreglerna.

Obs! Ett uttryck vars typ inte är en ref struct typ har alltid en säker kontextanroparkontext.

Regler för metodanrop

Referenssäkerhetskontextreglerna för metodanrop uppdateras på flera sätt. Den första är genom att känna igen den inverkan som scoped har på argument. För ett visst argument expr som skickas till parametern p:

  1. Om p är scoped ref bidrar expr inte referenssäker kontext när du överväger argument.
  2. Om p är scoped så bidrar expr inte till en säker kontext när du överväger argument.
  3. Om p är out bidrar expr inte till referenssäkerhetskontext eller säkerhetskontextfler detaljer

Språket "bidrar inte" innebär att argumenten helt enkelt inte beaktas vid beräkning av ref-safe-context eller safe-context-värdet för metodens returvärde. Det beror på att värdena inte kan bidra till den livslängden eftersom scoped-annotationen förhindrar detta.

Metodanropsreglerna kan nu förenklas. Mottagaren behöver inte längre behandlas som ett speciellt fall, i fallet med struct är det nu bara en scoped ref T. Värdereglerna behöver ändras för att ta hänsyn till ref fältretur:

Ett värde som härrör från metodanropet e1.M(e2, ...), där M() inte returnerar ref-to-ref-struct, har en säkerhetskontext hämtad från den snävaste av följande:

  1. samtalskontext
  2. När returen är en ref structsafe-context som har bidragit med alla argumentuttryck
  3. När returvärdet är en ref struct så är ref-safe-context bidragit av alla ref argument

Om M() returnerar ref-to-ref-struct är safe-context densamma som safe-context för alla argument som är ref-to-ref-struct. Det är ett fel om det finns flera argument med olika safe-context på grund av metodargument måste matcha.

Du kan förenkla ref anropsregler till:

Ett värde som härrör från en metodanrop ref e1.M(e2, ...), där M() inte returnerar ref-to-ref-struct, är ref-safe-context den smalaste av följande kontexter:

  1. samtalskontext
  2. Det safe-context som alla argumentuttryck bidrar till
  3. referenssäker kontext bidragit med alla ref argument

Om M() returnerar ref-to-ref-struct är ref-safe-context den smalaste ref-safe-context som alla argument som är ref-to-ref-struct bidrar med.

Med den här regeln kan vi nu definiera de två varianterna av önskade metoder:

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

Regler för objektinitierare

Det safe-context- för ett objektinitieringsuttryck är smalast av:

  1. för konstruktoranropet.
  2. safe-context och ref-safe-context argument till medlemsinitieringsindexerare som kan komma undan till mottagaren.
  3. safe-context av RHS för tilldelningar i medlemsinitialiserare till icke-readonly inställningar eller ref-safe-context vid referenstilldelning.

Ett annat sätt att modellera detta är att tänka på alla argument till en medlemsinitierare som kan tilldelas mottagaren som ett argument till konstruktorn. Det beror på att medlemsinitieraren i praktiken är ett konstruktoranrop.

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

Den här modelleringen är viktig eftersom den visar att vår MAMM- måste ta särskilt hänsyn till medlemsinitierare. Tänk på att det här specifika fallet måste vara olagligt eftersom det tillåter att ett värde med en smalare säker kontext tilldelas till ett högre.

Metodargumenten måste överensstämma

Förekomsten av ref fält innebär att reglerna kring metodargument måste matcha måste uppdateras eftersom en ref parameter nu kan lagras som ett fält i ett ref struct argument till metoden. Tidigare behövde regeln bara ta hänsyn till att lagra en annan ref struct som ett fält. Effekten av detta diskuteras i kompatibilitetsövervägandena. Den nya regeln är ...

För alla metodanrop e.M(a1, a2, ... aN)

  1. Beräkna den smalaste säkra -kontexten från:
    • anropskontext
    • den säkra kontexten för alla argument
    • referenssäker kontext för alla referensargument vars motsvarande parametrar har en referenssäker kontext av anroparkontext
  2. Alla ref argument av typerna ref struct måste kunna tilldelas ett värde i den -säkra kontexten. Det här är ett fall där ref inte generalisera för att inkludera in och out

För alla metodanrop e.M(a1, a2, ... aN)

  1. Beräkna den smalaste säkra -kontexten från:
    • anropskontext
    • den säkra kontexten för alla argument
    • referenssäker kontext för alla referensargument vars motsvarande parametrar inte är scoped
  2. Alla out argument av typerna ref struct måste kunna tilldelas ett värde i den -säkra kontexten.

Förekomsten av scoped gör det möjligt för utvecklare att minska friktionen som den här regeln skapar genom att markera parametrar som inte returneras som scoped. Detta tar bort deras argument från (1) i båda fallen ovan och ger större flexibilitet för anropare.

Effekten av den här ändringen beskrivs djupare nedan. Sammantaget gör detta att utvecklare kan göra anropsplatser mer flexibla genom att ange icke-eskaperande, referensliknande värden med scoped.

Parameteromfångsavvikelse

Attributet scoped och [UnscopedRef]-modifieraren (se nedan) på parametrar påverkar även åsidosättandet av objekt, implementering av gränssnitt och delegate-konverteringsregler. Signaturen för en åsidosättning, gränssnittsimplementering eller delegate-konvertering kan:

  • Lägg till scoped i en ref- eller in-parameter
  • Lägga till scoped i en ref struct-parameter
  • Ta bort [UnscopedRef] från en out-parameter
  • Ta bort [UnscopedRef] från en ref parameter av ref struct typ

Alla andra skillnader när det gäller scoped eller [UnscopedRef] anses vara en matchningsfel.

Kompilatorn rapporterar ett diagnostiskt meddelande för osäkra omfångsskillnader vid åsidosättningar, gränssnittsimplementeringar och delegeringskonverteringar.

  • Metoden har en ref- eller out-parameter av ref struct typ med ett matchningsfel för att lägga till [UnscopedRef] (tar inte bort scoped). (I det här fallet är en fånig cyklisk tilldelning möjlig, därför behövs inga andra parametrar.)
  • Eller båda dessa är sanna:
    • Metoden returnerar en ref struct eller returnerar en ref eller ref readonly, eller så har metoden en ref eller out parameter av ref struct typ.
    • Metoden har minst en ytterligare ref, ineller out parameter eller en parameter av ref struct typ.

Diagnostiken rapporteras inte i andra fall eftersom:

  • Metoder med sådana signaturer kan inte fånga referenserna som skickas in, så eventuella omfångsfel är inte farliga.
  • Dessa omfattar mycket vanliga och enkla scenarier (t.ex. vanliga gamla out-parametrar som används i TryParse-metodsignaturer) och att rapportera matchningar inom scope bara för att de används i språkversion 11 (och därmed har out-parametern olika scope) skulle vara förvirrande.

Diagnostiken rapporteras som ett fel om både de felmatchade signaturerna använder C#11 referenssäkra kontextregler. Annars är diagnostiken en varning.

Den begränsade felmatchningsvarningen kan rapporteras på en modul som kompilerats med C#7.2 ref säkra kontextregler där scoped inte är tillgängligt. I vissa sådana fall kan det vara nödvändigt att ignorera varningen om den andra felaktiga signaturen inte kan ändras.

Attributet scoped modifier och [UnscopedRef] har också följande effekter på metodsignaturer:

  • Modifiern scoped och attributet [UnscopedRef] påverkar inte döljande
  • Överbelastningar kan inte bara skilja sig åt på scoped eller [UnscopedRef]

Avsnittet om ref-fältet och scoped är långt, och jag ville avsluta med en kort sammanfattning av de föreslagna ändringarna med brytande karaktär.

  • Ett värde som har referenssäker kontext från till anroparkontext kan returneras av fälten ref eller ref.
  • En out parameter skulle ha en säker kontext av funktionsmedlem.

Detaljerade anteckningar:

  • Ett ref fält kan bara deklareras i en ref struct
  • Ett ref fält kan inte deklareras static, volatile eller const
  • Ett ref fält får inte ha en typ som är ref struct
  • Referenssammansättningens genereringsprocess måste bevara förekomsten av ett ref-fält i en ref struct-enhet
  • En readonly ref struct måste deklarera sina ref fält som readonly ref
  • För by-ref-värden måste scoped-modifieraren visas före in, outeller ref
  • Dokumentet om säkerhetsregler för spännvidd kommer att uppdateras enligt beskrivningen i det här dokumentet.
  • De nya reglerna för referenssäkra kontexter kommer att gälla när antingen
    • Kärnbiblioteket innehåller funktionsflaggan som anger stöd för ref fält
    • Värdet langversion är 11 eller högre

Syntax

13.6.2 Lokala variabeldeklarationer: har lagts till 'scoped'?.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 for-instruktionen: lade till 'scoped'?indirekt från local_variable_declaration.

13.9.5 Uttalandet foreach: har lagts till 'scoped'?.

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

12.6.2 Argumentlistor: lade till 'scoped'? för out deklarationsvariabel.

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

12.7 Dekonstruktionsuttryck:

[TBD]

15.6.2 Metodparametrar: lade till 'scoped'? i 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 Delegatdeklarationer: läggs till 'scoped'?indirektfixed_parameter.

12.19 Anonyma funktionsuttryck: har lagts till 'scoped'?.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

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

Begränsade typer av solnedgångar

Kompilatorn har ett koncept för en uppsättning "begränsade typer" som till stor del är odokumenterade. Dessa typer fick en särskild status eftersom det i C# 1.0 inte fanns något allmänt sätt att uttrycka sitt beteende. Framför allt faktumet att typerna kan innehålla referenser till exekveringsstacken. Istället hade kompilatorn särskild kunskap om dem och begränsade deras användning på sätt som alltid är säkra: tillät inte återvändande, kan inte användas som matriselement, kan inte användas i generiska typer, etc.

När ref fält är tillgängliga och utökade för att stödja ref struct kan dessa typer definieras korrekt i C# med hjälp av en kombination av ref struct och ref fält. Därför, när kompilatorn upptäcker att en körtid stöder ref-fält, kommer den inte längre att ha någon uppfattning om begränsade typer. Den använder i stället typerna som de definieras i koden.

För att stödja detta kommer våra referenssäkra kontextregler att uppdateras på följande sätt:

  • __makeref behandlas som en metod med signaturen static TypedReference __makeref<T>(ref T value)
  • __refvalue behandlas som en metod med signaturen static ref T __refvalue<T>(TypedReference tr). Uttrycket __refvalue(tr, int) använder effektivt det andra argumentet som typparameter.
  • __arglist som parameter har en ref-safe-context och safe-context för funktionsmedlem.
  • __arglist(...) som uttryck kommer att ha en referenssäker kontext och en säker kontext för funktionsmedlem .

För att säkerställa att körningarna överensstämmer ser du till att TypedReference, RuntimeArgumentHandle och ArgIterator definieras som ref struct. Ytterligare TypedReference måste anses ha ett ref fält till en ref struct för alla möjliga typer (det kan lagra valfritt värde). I kombination med ovanstående regler kommer att säkerställa att referenser till stacken inte går utanför deras livslängd.

Obs! Strikt sett är detta en implementeringsinformation för kompilatorn jämfört med en del av språket. Men med tanke på relationen med ref fält tas det med i språkförslaget för enkelhetens skull.

Ange utan begränsning

En av de mest anmärkningsvärda friktionspunkterna är oförmågan att returnera fält av ref i instansmedlemmar i en struct. Det innebär att utvecklare inte kan skapa ref returnerande metoder och egenskaper och måste istället exponera fält direkt. Detta minskar nyttan med ref i struct, där det ofta är som mest önskvärt.

struct S
{
    int _field;

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

Den motiveringen för den här standardinställningen är rimlig, men det är inget fel med en struct som flyr this som referens, det är helt enkelt den standard som väljs av referenssäkerhetskontextreglerna.

För att åtgärda detta kommer språket att ge motsatsen till scoped livstidsannotering genom att stödja en UnscopedRefAttribute. Detta kan tillämpas på alla ref och det kommer att ändra referenssäkerhetskontext till att vara en nivå bredare än standardinställningen. Till exempel:

UnscopedRef tillämpas på Ursprunglig referenssäker kontext Ny referenssäker kontext
instansmedlem funktionsmedlem endast retur
in / ref parameter endast retur uppringarens sammanhang
out-parametern funktionsmedlem endast retur

När [UnscopedRef] tillämpas på en instansmetod för en struct påverkas det av att den implicita this parametern ändras. Det innebär att this fungerar som en ej kommenterad ref av samma typ.

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

Anteckningen kan också placeras på out parametrar för att återställa dem till C# 10-beteende.

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

För ändamålet med säkra kontextregler anses en [UnscopedRef] out helt enkelt vara en ref. Liknar hur in anses vara ref för livslängdsändamål.

Annoteringen [UnscopedRef] kommer inte att tillåtas på init medlemmar och konstruktorer i struct. Dessa medlemmar är redan speciella när det gäller ref semantik eftersom de anser att readonly medlemmar är föränderliga. När ref tas till dessa medlemmar, visas det som en enkel refoch inte ref readonly. Detta tillåts inom gränserna för konstruktorer och init. Att tillåta [UnscopedRef] skulle tillåta en sådan ref att felaktigt fly utanför konstruktorn och tillåta mutation efter att readonly semantik hade ägt rum.

Attributtypen har följande definition:

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

Detaljerade anteckningar:

  • En instansmetod eller egenskap som har kommenterats med [UnscopedRef] har referenssäker kontext för this inställd på anroparkontext.
  • En medlem som har kommenterats med [UnscopedRef] kan inte implementera ett gränssnitt.
  • Det är ett fel att använda [UnscopedRef]
    • En medlem som inte är angiven på en struct
    • En static medlem, init medlem eller konstruktor på en struct
    • Parametern är markerad scoped
    • En parameter som passerades som värde
    • En parameter som skickas via referens som inte är implicit ändringsbegränsad

ScopedRefAttribute

De scoped anteckningarna skickas till metadata via attributet type System.Runtime.CompilerServices.ScopedRefAttribute. Attributet matchas med namnområdeskvalificerat namn, så definitionen behöver inte visas i någon specifik sammansättning.

Den ScopedRefAttribute typen är endast för kompilatoranvändning – den är inte tillåten i källan. Typdeklarationen syntetiseras av kompilatorn om den inte redan ingår i kompilatorn.

Typen har följande definition:

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

Kompilatorn genererar det här attributet på parametern med scoped syntax. Detta genereras endast när syntaxen gör att värdet skiljer sig från standardtillståndet. Till exempel orsakar scoped out att inget attribut genereras.

RefSafetyRulesAttribute

Det finns flera skillnader i referenssäker kontext regler mellan C#7.2 och C#11. Någon av dessa skillnader kan leda till icke-bakåtkompatibla ändringar vid omkompilering med C#11 mot referenser som kompilerats med C#10 eller tidigare.

  1. unscoped ref/in/out parametrar kan undkomma ett metodanrop som ett ref fält i en ref struct i C#11, inte i C#7.2
  2. out parametrar är implicit avgränsade i C#11 och utan avgränsning i C#7.2
  3. ref / in parametrar för ref struct typer är implicit begränsade inom skopet i C#11 och ospecificerade i C#7.2

För att minska risken för icke-bakåtkompatibla ändringar vid omkompilering med C#11 uppdaterar vi C#11-kompilatorn för att använda referenssäkra kontextregler för metodanrop som matchar de regler som användes för att analysera metoddeklarationen. När du analyserar ett anrop till en metod som kompilerats med en äldre kompilator, använder C#11-kompilatorn i princip säkerhetskontextreglerna för C#7.2.

För att aktivera detta genererar kompilatorn ett nytt [module: RefSafetyRules(11)]-attribut när modulen kompileras med -langversion:11 eller högre eller kompileras med en corlib som innehåller funktionsflaggan för ref fält.

Argumentet till attributet anger språkversionen av referenssäker kontext regler som användes när modulen kompilerades. Versionen är för närvarande fast på 11 oavsett den faktiska språkversion som skickas till kompilatorn.

Förväntningen är att framtida versioner av kompilatorn uppdaterar referenssäkra kontextregler och genererar attribut med distinkta versioner.

Om kompilatorn läser in en modul som innehåller en [module: RefSafetyRules(version)]med en annan version än 11rapporterar kompilatorn en varning för den okända versionen om det finns några anrop till metoder som deklarerats i modulen.

När C#11-kompilatorn analyserar ett metodanrop:

  • Om modulen som innehåller metoddeklarationen innehåller [module: RefSafetyRules(version)], oavsett version, analyseras metodanropet med C#11-regler.
  • Om modulen som innehåller metoddeklarationen kommer från källan och kompileras med -langversion:11 eller med en corlib som innehåller funktionsflaggan för ref fält analyseras metodanropet med C#11-regler.
  • Om modulen som innehåller metoddeklarationen refererar till System.Runtime { ver: 7.0 }analyseras metodanropet med C#11-regler. Den här regeln är en tillfällig åtgärd för moduler som kompilerats med tidigare förhandsversioner av C#11/.NET 7 och tas bort senare.
  • Annars analyseras metodanropet med C#7.2-regler.

En pre-C#11-kompilator ignorerar alla RefSafetyRulesAttribute och analyserar endast metodanrop med C#7.2-regler.

RefSafetyRulesAttribute matchas med namnområdeskvalificerat namn, så definitionen behöver inte visas i någon specifik sammansättning.

Den RefSafetyRulesAttribute typen är endast för kompilatoranvändning – den är inte tillåten i källan. Typdeklarationen syntetiseras av kompilatorn om den inte redan ingår i kompilatorn.

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

Säkra buffertar med fast storlek

Säkra buffertar med fast storlek levererades inte i C# 11. Den här funktionen kan implementeras i en framtida version av C#.

Språket minskar begränsningarna för matriser med fast storlek så att de kan deklareras i säker kod och elementtypen kan hanteras eller ohanteras. Detta gör typer som följande juridiska:

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

Dessa deklarationer, ungefär som deras unsafe motsvarigheter, definierar en sekvens med N element i den innehållande typen. Dessa medlemmar kan nås med en indexerare och kan också konverteras till Span<T> och ReadOnlySpan<T> instanser.

När du indexerar till en fixed buffert av typen T måste containerns readonly tillstånd beaktas. Om containern är readonly returnerar indexeraren ref readonly T annars returneras ref T.

Åtkomst till en fixed buffert utan en indexerare har ingen naturlig typ, men den kan konverteras till Span<T> typer. Om containern är readonly kan bufferten implicit konverteras till ReadOnlySpan<T>, annars kan den implicit konverteras till Span<T> eller ReadOnlySpan<T> (konverteringen Span<T> anses bättre).

Den resulterande Span<T>-instansen har en längd som är lika med den storlek som deklareras i fixed bufferten. safe-context för det returnerade värdet är lika med safe-context för containern, precis som om den underliggande datan skulle användas som ett fält.

För varje fixed deklaration i en typ där elementtypen är T genererar språket en motsvarande get endast indexeringsmetod vars returtyp är ref T. Indexeraren kommenteras med attributet [UnscopedRef] eftersom implementeringen returnerar fält av deklareringstypen. Tillgängligheten för medlemmen matchar tillgängligheten i fältet fixed.

Till exempel är indexerarens signatur för CharBuffer.Data följande:

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

Om det angivna indexet ligger utanför de deklarerade gränserna för den fixed matrisen genereras en IndexOutOfRangeException. Om ett konstant värde anges ersätts det med en direkt referens till lämpligt element. Om inte konstanten ligger utanför de deklarerade gränserna skulle ett kompileringstidsfel inträffa.

Det kommer också att genereras en namngiven tillgångsmetod för varje fixed buffert som tillhandahåller värdebaserade get och set operationer. Det innebär att fixed-buffertar kommer att mer likna befintliga matrissemantiker genom att ha en ref-accessor samt byval get- och set-operationer. Det innebär att kompilatorer har samma flexibilitet när de genererar kod som förbrukar fixed buffertar som när de använder matriser. Detta bör göra operationer som await över fixed buffertar enkelt att generera.

Detta har också den extra fördelen att det gör fixed buffertar enklare att använda från andra språk. Namngivna indexerare är en funktion som har funnits sedan 1.0-versionen av .NET. Även språk som inte direkt kan generera en namngiven indexerare kan i allmänhet använda dem (C# är faktiskt ett bra exempel på detta).

Den tillhörande lagringen för bufferten genereras med hjälp av attributet [InlineArray]. Det här är en mekanism som beskrivs i ärende 12320 som specifikt möjliggör att en sekvens av fält av samma typ deklareras effektivt. Den här frågan är fortfarande under aktiv diskussion och förväntningen är att implementeringen av den här funktionen kommer att ske beroende på hur den diskussionen utspelar sig.

Initierare med värden på ref i uttryck new och with

I avsnittet 12.8.17.3 Object initializersuppdaterar vi grammatiken till:

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

I avsnittet för with uttryckuppdaterar vi grammatiken till:

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

Den vänstra operanden i tilldelningen måste vara ett uttryck som binder till ett referensfält.
Den högra operanden måste vara ett uttryck som ger en lvalue som anger ett värde av samma typ som den vänstra operanden.

Vi lägger till en liknande regel i lokal referenstilldelning:
Om den vänstra operanden är en skrivbar referens (dvs. den anger något annat än ett ref readonly fält), måste den högra operanden vara en skrivbar lvalue.

Undantagsreglerna för konstruktoranrop kvarstår:

Ett new uttryck som anropar en konstruktor följer samma regler som ett metodanrop som anses returnera den typ som skapas.

Det vill säga reglerna för metodanrop uppdaterade ovan:

Ett rvalue som härrör från ett metodanrop e1.M(e2, ...) har säker kontext från den minsta av följande kontexter:

  1. samtalskontext
  2. Det safe-context som alla argumentuttryck bidrar till
  3. När returen är ref struct och "referenssäker kontext" som alla ref argument har bidragit till

För ett new uttryck med initierare räknas initieraruttrycken som argument (de bidrar med sina safe-context) och ref initializer-uttryck räknas som ref argument (de bidrar med sina ref-safe-context), rekursivt.

Ändringar i osäker kontext

Pekartyper (avsnitt 23.3) utökas för att tillåta hanterade typer som referenstyp. Sådana pekartyper skrivs som en hanterad typ följt av en * token. De skapar en varning.

Adressoperatorn (avsnitt 23.6.5) utökas för att acceptera en variabel med en hanterad typ som operand.

Instruktionen fixed (avsnitt 23.7) görs flexibel för att tillåta fixed_pointer_initializer som är adressen till en variabel av hanterad typ T eller som är ett uttryck av en array_type med element av en hanterad typ T.

Stackallokeringsinitieraren (avsnitt 12.8.22) är lika avslappnad.

Överväganden

Det finns överväganden som andra delar av utvecklingsstacken bör tänka på när du utvärderar den här funktionen.

Överväganden för kompatibilitet

Utmaningen i detta förslag är konsekvenserna för kompatibiliteten som denna design medför för våra befintliga säkerhetsreglernas tillämpningsområde, eller §9.7.2. Även om dessa regler helt stöder konceptet att ref struct har ref fält, tillåter de inte att API:er, annat än stackalloc, samlar in tillstånd för ref som hänför sig till stacken. Reglerna för en säker referenskontext har ett hårt antagande, eller §16.4.12.8 att en konstruktor av typen Span(ref T value) inte finns. Det innebär att säkerhetsreglerna inte tar hänsyn till att en ref parameter kan komma undan som ett ref fält, vilket innebär att den tillåter kod som följande.

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

Det finns i själva verket tre sätt för en ref parameter att fly från ett metodanrop:

  1. Efter värderetur
  2. Retur senast ref
  3. Genom ref fält i ref struct som returneras eller passeras som ref / out parameter

De befintliga reglerna står endast för (1) och (2). De tar inte hänsyn till (3), och på grund av detta redovisas inte luckor, såsom att återkommande lokala värden kommer tillbaka som ref-fält. Den här designen måste ändra reglerna för att ta hänsyn till (3). Detta har en liten inverkan på kompatibiliteten för befintliga API:er. Mer specifikt påverkar det API:er som har följande egenskaper.

  • Ha en ref struct i signaturen
    • Om ref struct är en returtyp, ref eller out parameter
    • Har ytterligare en in- eller ref parameter förutom mottagaren

I C# 10 behövde anropare av sådana API:er aldrig överväga att ref tillståndsindata till API:et kunde samlas in som ett ref fält. Det gjorde det möjligt för flera mönster att existera, säkert i C# 10, som kommer att vara osäkra i C# 11 på grund av möjligheten att ref-tillståndet kan läcka som ett ref-fält. Till exempel:

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

Effekten av den här kompatibilitetsbrytningen förväntas vara mycket liten. Den påverkade API-formen var föga meningsfull i avsaknad av ref fält, därför är det osannolikt att kunderna skapade många av dessa. Experiment som använder verktyg för att identifiera denna API-struktur i befintliga lagringsplatser stöder den försäkran. Den enda lagringsplatsen med betydande antal av den här formen är dotnet/runtime och det beror på att lagringsplatsen kan skapa ref fält via den ByReference<T> inbyggda typen.

Trots detta måste designen ta hänsyn till sådana API:er som finns eftersom den uttrycker ett giltigt mönster, men inte ett vanligt. Därför måste designen ge utvecklare verktygen för att återställa befintliga livslängdsregler vid uppgradering till C# 10. Mer specifikt måste det tillhandahålla mekanismer som gör det möjligt för utvecklare att märka ref-parametrar som inte kan undkomma via ref eller ref-fält. Det gör att kunder kan definiera API:er i C# 11 som har samma C# 10-anropswebbplatsregler.

Referenssammansättningar

En referenssammansättning för en kompilering med funktioner som beskrivs i det här förslaget måste underhålla de element som förmedlar referenssäker kontextinformation. Det innebär att alla livstidsanteckningsattribut måste bevaras i sin ursprungliga position. Alla försök att ersätta eller utelämna dem kan leda till ogiltiga referenssammansättningar.

Det är mer nyanserat att representera ref-fältet. Helst skulle ett ref fält visas i en referenssammansättning, precis som andra fält. Ett ref-fält representerar dock en ändring av metadataformatet och som kan orsaka problem med verktygskedjor som inte uppdateras för att förstå den här metadataändringen. Ett konkret exempel är C++/CLI som sannolikt kommer att fela om det använder ett ref fält. Därför är det fördelaktigt om ref fält kan utelämnas från referenssammansättningar i våra kärnbibliotek.

Ett ref-fält i sig har ingen inverkan på referenssäkra kontextregler. Ett konkret exempel är att det inte påverkar förbrukningen att ändra den befintliga Span<T> definitionen för att använda ett ref fält. Därför kan själva ref utelämnas på ett säkert sätt. Ett ref fält har dock andra effekter på förbrukningen som måste bevaras:

  • Ett ref struct som har ett ref fält anses aldrig vara unmanaged
  • Typen av ref-fältet påverkar oändliga allmänna expansionsregler. Om typen av ett ref fält innehåller en typparameter som måste bevaras

Med dessa regler i åtanke, här är en giltig referensmonteringstransformation för en 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
}

Anteckningar

Livslängder uttrycks mest naturligt med hjälp av typer. Livslängderna i ett givet program är säkra när livslängdstyperna klarar typkontrollen. Även om syntaxen för C# implicit lägger till livslängder till värden, finns det ett underliggande typsystem som beskriver de grundläggande reglerna här. Det är ofta lättare att diskutera konsekvenserna av ändringar i designen när det gäller dessa regler så att de tas med här för diskussions skull.

Observera att detta inte är avsett att vara en 100% fullständig dokumentation. Att dokumentera varje enskilt beteende är inte ett mål här. I stället är det tänkt att upprätta en allmän förståelse och ett gemensamt verbiage som modellen, och potentiella ändringar av den, kan diskuteras med.

Vanligtvis är det inte nödvändigt att direkt prata om livslängdstyper. Undantagen är platser där livslängden kan variera beroende på specifika instansieringsplatser. Detta är ett slags polymorfism och vi kallar dessa varierande livslängder för "generiska livslängder", som representeras som generiska parametrar. C# tillhandahåller inte syntax för att uttrycka generiska livstidsparametrar, så vi definierar en implicit "översättning" från C# till ett utökat och förenklat språk som innehåller explicita generiska parametrar.

I exemplen nedan används namngivna livslängder. Syntaxen $a refererar till en livslängd med namnet a. Det är en livslängd som inte har någon betydelse av sig själv men som kan ges en relation till andra livslängder via where $a : $b syntax. Detta fastställer att $a är konvertibel till $b. Det kan hjälpa att tänka på detta som att etablera att $a har en livslängd åtminstone så lång som $b.

Det finns några fördefinierade livslängder för bekvämlighet och korthet nedan:

  • $heap: det här är livslängden för vilket värde som helst som finns på heap. Den är tillgänglig i alla kontexter och metodsignaturer.
  • $local: det här är livslängden för alla värden som finns i metodstacken. Det är i själva verket en platshållare för namnet på funktionens medlem . Den definieras implicit i metoder och kan visas i metodsignaturer förutom för alla utdatapositioner.
  • $ro: namnplatshållare för returnerar endast
  • $cm: namnplatshållare för uppringarkontext

Det finns några fördefinierade relationer mellan livslängder:

  • where $heap : $a för alla livslängder $a
  • where $cm : $ro
  • where $x : $local för alla fördefinierade livslängder. Användardefinierade livslängder har ingen relation till lokal om inte uttryckligen definierats.

Livslängdsvariabler när de definieras för typer kan vara invarianta eller covarianta. Dessa uttrycks med samma syntax som generiska parametrar:

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

Livslängdsparametern $this för typdefinitioner är inte fördefinierad, men den har några regler som är associerade med den när den definieras:

  • Det måste vara den första livslängdsparametern.
  • Den måste vara covariant: out $this.
  • Livslängden för ref fält måste konverteras till $this
  • Den $this livslängden för alla icke-referensfält måste vara $heap eller $this.

En referens livslängd uttrycks genom att ett livstidsargument ges till referensen. Till exempel uttrycks en ref som refererar till högen som ref<$heap>.

När du definierar en konstruktor i modellen används namnet new för metoden. Det är nödvändigt att ha en parameterlista för det returnerade värdet samt konstruktorargumenten. Detta är nödvändigt för att uttrycka relationen mellan konstruktorindata och det konstruerade värdet. I stället för att ha Span<$a><$ro> använder modellen Span<$a> new<$ro> i stället. Typen av this i konstruktorn, tillsammans med livslängder, kommer att vara det definierade returvärdet.

De grundläggande reglerna för livslängden definieras som:

  • Alla livstider uttrycks syntaktiskt som generiska argument, som kommer före typargument. Detta gäller för fördefinierade livslängder förutom $heap och $local.
  • Alla typer T som inte är en ref struct har implicit livslängden för T<$heap>. Detta är implicit, det finns inget behov av att skriva int<$heap> i varje exempel.
  • För ett ref fält som definierats som ref<$l0> T<$l1, $l2, ... $ln>:
    • Alla livslängder $l1 genom $ln måste vara invarianta.
    • Livslängden för $l0 måste konverteras till $this
  • För en ref som definieras som ref<$a> T<$b, ...>måste $b konverteras till $a
  • ref för en variabel har en livslängd som definieras av:
    • För en ref lokal, parameter, fält eller retur av typen ref<$a> T livslängden är $a
    • $heap för alla referenstyper och fält för referenstyper
    • $local för allt annat
  • En tilldelning eller retur är laglig när den underliggande typkonverteringen är laglig
  • Livslängder för uttryck kan göras tydliga med hjälp av cast-annoteringar.
    • (T<$a> expr) värdets livslängd uttryckligen $a för T<...>
    • ref<$a> (T<$b>)expr värdets livslängd är $b för T<...> och referenslivslängden är $a.

I fråga om livslängdsregler anses en ref vara en del av uttryckets typ för konverteringsändamål. Det representeras logiskt genom att konvertera ref<$a> T<...> till ref<$a, T<...>> där $a är covariant och T är invariant.

Nu ska vi definiera de regler som gör att vi kan mappa C#-syntaxen till den underliggande modellen.

För korthets skull är en typ som inte har några explicita livslängdsparametrar behandlas som om det finns en definierad out $this som tillämpas på alla fält av typen. En typ med ett ref fält måste definiera explicita livslängdsparametrar.

Dessa regler finns för att stödja vår befintliga invariant som T kan tilldelas till scoped T för alla typer. Det mappar ned till T<$a, ...> som kan tilldelas till T<$local, ...> för alla livslängder som är kända för att vara konvertibla till $local. Dessutom stöder detta andra objekt som att kunna tilldela Span<T> från heapen till dem på stacken. Detta exkluderar typer där fält har olika livslängder för icke-ref-värden, men det är verkligheten för C# idag. Ändringar som skulle kräva en betydande ändring av C#-regler som skulle behöva mappas ut.

Typen av this för en typ S<out $this, ...> i en instansmetod definieras implicit som följande:

  • För normal instansmetod: ref<$local> S<$cm, ...>
  • Till exempel en metod som har kommenterats med [UnscopedRef]: ref<$ro> S<$cm, ...>

Avsaknaden av en explicit this parameter tvingar fram implicita regler här. För komplexa exempel och diskussioner bör du överväga att skriva som en static metod och göra this till en explicit parameter.

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

C#-metodens syntax mappar till modellen på följande sätt:

  • ref parametrar har en referenslivslängd på $ro
  • parametrar av typen ref struct har denna livslängd på $cm
  • referensreturer har en referenslivslängd på $ro
  • Resultat av typen ref struct har en livstid av värdet $ro
  • scoped på en parameter eller ref ändrar referenslivslängden till $local

Med tanke på att vi ska utforska ett enkelt exempel som visar modellen här:

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

Nu ska vi utforska samma exempel med hjälp av en 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;
}

Nu ska vi se hur detta hjälper till med problemet med cyklisk självtilldelning:

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

Nu ska vi se hur detta hjälper till med problemet med den fåniga insamlingsparametern:

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

Öppna problem

Ändra designen för att undvika kompatibilitetsbrytningar

Den här designen föreslår flera kompatibilitetsbrytningar med våra befintliga regler för referenssäker kontext. Även om ändringarna tros ha minimal påverkan beaktades en design som inte hade några kritiska ändringar.

Den kompatibilitetsbevarande designen var dock betydligt mer komplex än den här. För att upprätthålla kompatibilitet måste ref-fält ha olika livslängder för att kunna returneras via ref- respektive ref-fältet. I stort sett kräver det att vi tillhandahåller referensfältsäker kontext spårning för alla parametrar till en metod. Detta måste beräknas för alla uttryck och följas upp i alla värden, praktiskt taget överallt där ref-safe-context för närvarande spåras.

Dessutom har det här värdet relationer med ref-safe-context. Det är till exempel icke-sensiskt att ha ett värde som kan returneras som ett ref fält men inte direkt som ref. Det beror på att ref fält kan returneras enkelt av ref redan (ref tillstånd i en ref struct kan returneras av ref även när det innehållande värdet inte kan). Därför behöver reglerna ytterligare en ständig justering för att säkerställa att dessa värden är förnuftiga när det gäller varandra.

Det innebär också att språket behöver syntax för att representera ref parametrar som kan returneras på tre olika sätt: efter ref fält, efter ref och efter värde. Standardvärdet är returnerbart av ref. Framöver förväntas dock den mer naturliga avkastningen, särskilt när ref struct är inblandade, vara från ref-fältet eller ref. Det innebär att nya API:er kräver en extra syntaxanteckning för att vara korrekt som standard. Det här är oönskat.

Dessa kompatibilitetsändringar påverkar dock metoder som har följande egenskaper:

  • Ha en Span<T> eller ref struct
    • Om ref struct är en returtyp, ref eller out parameter
    • Har ytterligare en in eller ref parameter (exklusive mottagaren)

För att förstå effekten är det bra att dela upp API:er i kategorier:

  1. Vill att användarna ska ta hänsyn till att ref registreras som ett ref fält. Det främsta exemplet är Span(ref T value)-konstruktorer
  2. Användarna ska inte behöva ta hänsyn till att ref blir registrerat som ett ref-fält. Dessa delas dock in i två kategorier
    1. Osäkra API:er. Dessa är API:er i Unsafe- och MemoryMarshal-typerna, av vilka MemoryMarshal.CreateSpan är den mest framstående. Dessa API:er avbildar ref osäkert, men de är också kända för att vara osäkra API:er.
    2. Säkra API:er. Det här är API:er som tar ref parametrar för effektivitet, men det noteras faktiskt inte någonstans. Exemplen är små, men ett är AsnDecoder.ReadEnumeratedBytes

Den här ändringen gynnar främst (1) ovan. Dessa förväntas utgöra majoriteten av API:er som tar en ref och returnerar en ref struct framöver. Ändringarna påverkar negativt (2.1) och (2.2) eftersom det bryter mot den befintliga anropande semantiken eftersom livslängdsreglerna ändras.

API:erna i kategorin (2.1) är dock till stor del författade av Microsoft eller av utvecklare som har mest nytta av ref fält (Tanner's of the world). Det är rimligt att anta att den här klassen av utvecklare skulle vara benägen att acceptera en kompatibilitetsavgift vid uppgradering till C# 11 i form av några annotationer för att behålla den befintliga semantiken om ref-fält erbjöds som motprestation.

API:erna i kategorin (2.2) är det största problemet. Det är okänt hur många sådana API:er som finns och det är oklart om dessa skulle vara mer/mindre frekventa i tredjepartskod. Förväntningarna är att det finns ett mycket litet antal av dem, särskilt om vi tar kompatibilitetspausen på out. Sökningar hittills har upptäckt ett mycket litet antal sådana som finns i ytan public. Det här är ett svårt mönster att söka efter eftersom det kräver semantisk analys. Innan du gör den här ändringen krävs en verktygsbaserad metod för att verifiera antagandena kring detta som påverkar ett litet antal kända fall.

För båda fallen i kategori (2) så är korrigeringen enkel. De ref parametrar som inte vill anses vara avbildningsbara måste lägga till scoped i ref. I (2.1) kommer detta sannolikt också att tvinga utvecklaren att använda Unsafe eller MemoryMarshal men det förväntas för API:er med osäkert format.

Helst kan språket minska effekten av problematiska ändringar utan att det märks genom att utfärda en varning när ett API utan förvarning börjar uppvisa problematiskt beteende. Det skulle vara en metod som både tar en ref, returnerar ref struct, men inte fångar upp ref i ref struct. Kompilatorn kan i så fall utfärda en diagnostik som informerar utvecklare att ref ska anoteras som scoped ref i stället.

Beslut Den här designen kan uppnås, men den resulterande funktionen är svårare att använda, vilket ledde till beslutet att göra ett kompatibilitetsbrott.

Beslut Kompilatorn ger en varning när en metod uppfyller kriterierna men inte avbildar parametern ref som ett ref fält. Detta bör på lämpligt sätt varna kunder vid uppgradering om de potentiella problem som de skapar

Nyckelord jämfört med attribut

Den här designen kräver att du använder attribut för att kommentera de nya livslängdsreglerna. Detta kunde också ha gjorts lika enkelt med kontextuella nyckelord. Till exempel kan [DoesNotEscape] mappa till scoped. Men nyckelord, även de kontextuella, måste i allmänhet uppfylla mycket höga krav för att inkluderas. De tar upp värdefullt språkutrymme och är mer framträdande delar av språket. Den här funktionen, även om den är värdefull, kommer att tjäna en minoritet av C#-utvecklare.

På ytan kan det verka gynna att inte använda nyckelord, men det finns två viktiga punkter att tänka på:

  1. Anteckningarna påverkar programmets semantik. Att låta attribut påverka programsemantiken är en gräns som C# är ovilligt att överskrida, och det är oklart om detta är funktionen som bör motivera att språket tar det steget.
  2. De utvecklare som är mest benägna att använda den här funktionen samverkar starkt med den uppsättning utvecklare som använder funktionspekare. Den funktionen, som också används av en minoritet av utvecklare, motiverade en ny syntax och det beslutet ses fortfarande som sunt.

Sammantaget innebär det att syntaxen bör beaktas.

En grov skiss av syntaxen skulle vara:

  • [RefDoesNotEscape] mappar till scoped ref
  • [DoesNotEscape] mappar till scoped
  • [RefDoesEscape] mappar till unscoped

Decision Use syntax for scoped and scoped ref; använd attributet för unscoped.

Tillåt fasta buffertlokaler

Den här designen möjliggör säkra fixed buffertar som kan stödja alla typer. Ett möjligt tillägg här är att tillåta att sådana fixed buffertar deklareras som lokala variabler. Detta skulle göra det möjligt att ersätta ett antal befintliga stackalloc åtgärder med en fixed buffert. Det skulle också utöka uppsättningen scenarier där vi kan ha allokeringar i stackstil, eftersom stackalloc är begränsad till ohanterade elementtyper, men fixed-buffertar inte är det.

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

Detta håller ihop men kräver att vi utökar syntaxen för lokalbefolkningen lite. Oklart om detta är eller inte är värt den extra komplexiteten. Möjligt att vi kan besluta nej för tillfället och ta tillbaka senare om det finns tillräckligt med behov.

Exempel på var detta skulle vara fördelaktigt: https://github.com/dotnet/runtime/pull/34149

beslut avvakta med detta för tillfället

Att använda modreqs eller inte

Ett beslut måste fattas om metoder som har markerats med nya livslängdsattribut ska översättas till modreq i avsändare. Det skulle i praktiken finnas en 1:1-mappning mellan anteckningar och modreq om den här metoden användes.

Anledningen till att lägga till en modreq är att attributen ändrar semantiken för referenssäkra kontextregler. Endast språk som förstår dessa semantik bör anropa metoderna i fråga. När de tillämpas på OHI-scenarier blir livslängden ett kontrakt som alla härledda metoder måste implementera. Att ha anteckningarna utan modreq kan leda till situationer där virtual metodkedjor med motstridiga livslängdsanteckningar laddas (kan inträffa om endast en del av virtual kedjan kompileras och den andra inte är kompilerad).

Det inledande referenssäkra kontextarbetet använde inte modreq utan förlitade sig i stället på språk och ramverket för att förstå. Samtidigt, även om alla element som bidrar till de referenssäkra kontextreglerna är en stark del av metodens signatur: ref, in, ref struct, etc ... Därför resulterar varje ändring av de befintliga reglerna för en metod i en binär ändring av signaturen. För att ge de nya livstidsanvisningarna samma effekt behöver de modreq tillämpning.

Frågan är om det här är överdrivet eller inte. Det har den negativa effekten att när man gör signaturer mer flexibla, till exempel genom att lägga till [DoesNotEscape] till en parameter, resulterar det i en ändring av binär kompatibilitet. Den kompromissen innebär att ramverk som BCL med tiden sannolikt inte kommer att kunna lätta på sådana signaturer. Det kan mildras i viss utsträckning genom att använda några av de tillvägagångssätt språket använder för in-parametrar och endast tillämpa modreq på virtuella platser.

Beslut Använd inte modreq i metadata. Skillnaden mellan out och ref är inte modreq men de har nu olika referenssäkra kontextvärden. Det finns ingen verklig fördel med att bara tillämpa reglerna delvis i den här situationen med modreq.

Tillåt flerdimensionella fasta buffertar

Ska designen för fixed-buffertar utökas till att omfatta flerdimensionella stilmatriser? Tillåter i princip deklarationer som följande:

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

Beslut Tillåt inte för tillfället

Bryter mot tilldelad omfattning

Runtime-lagringsplatsen har flera icke-offentliga API:er som samlar in ref parametrar som ref fält. Dessa är osäkra eftersom livslängden för det resulterande värdet inte spåras. Till exempel konstruktorn Span<T>(ref T value, int length).

De flesta av dessa API:er kommer sannolikt att välja att ha korrekt spårning av livslängd för returvärdena, vilket uppnås helt enkelt genom att uppdatera till C# 11. Några få vill dock behålla sin nuvarande semantik och inte spåra returvärdet eftersom deras hela avsikt är att vara osäkra. De mest anmärkningsvärda exemplen är MemoryMarshal.CreateSpan och MemoryMarshal.CreateReadOnlySpan. Detta uppnås genom att parametrarna markeras som scoped.

Det innebär att körningen behöver ett etablerat mönster för att ta bort scoped från en parameter på ett osäkert sätt:

  1. Unsafe.AsRef<T>(in T value) kan utöka sitt befintliga syfte genom att ändra till scoped in T value. Detta skulle göra det möjligt att både ta bort in och scoped från parametrar. Det blir sedan den universella metoden "ta bort säkerheten för referenser"
  2. Introducera en ny metod vars hela syfte är att ta bort scoped: ref T Unsafe.AsUnscoped<T>(scoped in T value). Detta tar också bort in eftersom, om det inte gjorde det, skulle anropare fortfarande behöva en kombination av metodanrop för att "ta bort referenssäkerhet", och då är den befintliga lösningen sannolikt tillräcklig.

Ska detta vara ospecificerat som standard?

Designen har bara två platser som är scoped som standard:

  • this är scoped ref
  • out är scoped ref

Beslutet om out är att avsevärt minska belastningen av kompatibilitet för ref-fält och samtidigt är det en mer naturlig standardinställning. Det gör att utvecklare faktiskt kan tänka på out som data som flödar utåt, medan om det är ref, måste reglerna överväga data som flödar i båda riktningarna. Detta leder till betydande utvecklarförvirring.

Beslutet om this är oönskat eftersom det innebär att en struct inte kan returnera ett fält med ref. Det här är ett viktigt scenario för utvecklare med hög perf och attributet [UnscopedRef] har lagts till i huvudsak för det här scenariot.

Nyckelord har en hög standard och att lägga till det för ett enskilt scenario är misstänkt. Som sådant övervägdes det huruvida vi kunde undvika detta nyckelord helt och hållet genom att helt enkelt göra this till ref som standard istället för scoped ref. Alla medlemmar som behöver att this ska vara scoped ref kan uppnå detta genom att markera metoden scoped (då en metod kan markeras readonly för att skapa en readonly ref idag).

På ett normalt struct är detta till största delen en positiv förändring eftersom det bara introducerar kompatibilitetsproblem när en medlem har en ref återkomst. Det finns mycket få av dessa metoder och ett verktyg kan upptäcka dessa och konvertera dem till scoped medlemmar snabbt.

På en ref struct introducerar den här ändringen betydligt större kompatibilitetsproblem. Tänk på följande:

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

I princip skulle det innebära att alla anrop av instansmetoder på föränderligaref struct lokala variablerna skulle vara ogiltiga om inte den lokala variabeln markerades ytterligare som scoped. Reglerna måste ta hänsyn till det fall där fälten har omtilldelats till andra fält i this. Ett readonly ref struct har inte det här problemet eftersom readonly:s natur förhindrar referensompekning. Ändå skulle detta vara en betydande bakåtkompatibel ändring eftersom det skulle påverka praktiskt taget alla befintliga föränderliga ref struct.

Ett readonly ref struct är dock fortfarande problematiskt när vi utökar med ref-fält till ref struct. Det löser det grundläggande problemet genom att flytta fångsten till värdet för fältet 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);
    }
}

Man övervägde tanken på att this skulle ha olika standardinställningar beroende på typen av struct eller medlem. Till exempel:

  • this som ref: struct, readonly ref struct eller readonly member
  • this som scoped ref: ref struct eller readonly ref struct med ref fält till ref struct

Detta minimerar kompatibilitetsavbrott och maximerar flexibiliteten, men på bekostnad av att komplicera berättelsen för kunderna. Det löser inte heller problemet helt eftersom framtida funktioner, till exempel säkra fixed buffertar, kräver att en föränderlig ref struct har ref returnerar för fält som inte fungerar enbart med den här designen eftersom den skulle tillhöra kategorin scoped ref.

Beslut Behåll this som scoped ref. Det innebär att de föregående lömska exemplen skapar kompilatorfel.

referensfält till ref-struktur

Den här funktionen öppnar en ny uppsättning referenssäkra kontextregler eftersom det gör att ett ref fält kan referera till en ref struct. Den generiska naturen hos ByReference<T> innebar att körningstiden hittills inte kunde ha en sådan konstruktion. Som ett resultat är alla våra regler skrivna under antagandet att detta inte är möjligt. Den ref fältfunktionen handlar till stor del inte om att skapa nya regler utan att kodifiera befintliga regler i vårt system. För att tillåta ref fält att ref struct måste vi kodifiera nya regler eftersom det finns flera nya scenarier att överväga.

Det första är att en readonly ref nu kan lagra ett ref-tillstånd. Till exempel:

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

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

Det innebär att när vi tänker på att metodargument måste överensstämma med regler, måste vi överväga att readonly ref T är en potentiell metodutdata när T potentiellt har ett ref-fält till en ref struct.

Det andra problemet är att språket måste ha en ny typ av säker kontext: referensfältsäker kontext. Alla ref struct som transitivt innehåller ett ref-fält har ett annat flyktomfång som representerar värdena i ref-fält. När det gäller flera ref fält kan de spåras kollektivt som ett enda värde. Standardvärdet för detta för parametrar är anroparkontext.

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

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

Det här värdet är inte relaterat till containerns safe-context-. eftersom containerkontexten blir mindre påverkar den inte referensfältsäker kontext för ref fältvärden. Dessutom kan ref-field-safe-context aldrig vara mindre än containerns safe-context-.

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

Det här ref-field-safe-context har i princip alltid funnits. Fram tills nu kunde ref fälten bara peka på vanliga struct därför sammanföll det enkelt med anroparkontext. För att stödja ref fält i ref struct behöver våra befintliga regler uppdateras för att beakta detta nya ref-safe-context.

För det tredje måste reglerna för omtilldelning av referens uppdateras för att säkerställa att vi inte bryter mot referensfältkontext för värdena. För x.e1 = ref e2 där typen av e1 är en ref struct måste ref-field-safe-context vara lika.

Dessa problem är mycket lösbara. Kompilatorteamet har skissat upp några versioner av dessa regler och de faller till stor del ut från vår befintliga analys. Problemet är att det inte finns någon användningskod för sådana regler som hjälper till att bevisa att det finns korrekthet och användbarhet. Detta gör oss väldigt tveksama att lägga till stöd på grund av rädslan för att vi ska välja fel standardvärden och backa körningen i användbarhetshörnet när det drar nytta av detta. Denna oro är särskilt stark eftersom .NET 8 sannolikt driver oss i den här riktningen med allow T: ref struct och Span<Span<T>>. Reglerna skulle skrivas bättre om det görs tillsammans med konsumtionskoden.

Beslut Fördröjning som möjliggör ref fältet att ref struct till .NET 8, där vi har scenarier som bidrar till att utforma reglerna kring dessa scenarier. Detta har inte implementerats från och med .NET 9

Vad innebär C# 11.0?

De funktioner som beskrivs i det här dokumentet behöver inte implementeras i ett enda pass. I stället kan de implementeras i faser i flera språkversioner i följande bucketar:

  1. ref fält och scoped
  2. [UnscopedRef]
  3. ref fält till ref struct
  4. Begränsade typer av solnedgångar
  5. buffertar med fast storlek

Det som implementeras i vilken version är bara en avgränsningsövning.

Endast beslut (1) och (2) gjorde C# 11.0. Resten kommer att beaktas i framtida versioner av C#.

Framtida överväganden

Avancerade livstidsanteckningar

Livslängdsanteckningarna i det här förslaget är begränsade eftersom de gör det möjligt för utvecklare att ändra standardbeteendet escape/don't escape för värden. Detta ger kraftfull flexibilitet i vår modell, men det ändrar inte radikalt den uppsättning relationer som kan uttryckas. I grunden är C#-modellen fortfarande i praktiken binär: kan ett värde returneras eller inte?

Det gör att begränsade livslängdsrelationer kan förstås. Till exempel har ett värde som inte kan returneras från en metod en mindre livslängd än ett som kan returneras från en metod. Det finns dock inget sätt att beskriva livslängdsrelationen mellan värden som kan returneras från en metod. Mer specifikt finns det inget sätt att säga att ett värde har en större livslängd än det andra när det har upprättats kan båda returneras från en metod. Nästa steg i vår livsutveckling skulle vara att tillåta att sådana relationer beskrivs.

Andra metoder som Rust tillåter att den här typen av relation uttrycks och kan därför implementera mer komplexa scoped stilåtgärder. Vårt språk skulle på liknande sätt kunna dra nytta av en sådan funktion. För närvarande finns det inget motiverande tryck att göra detta, men om det finns i framtiden kan vår scoped modell utökas till att omfatta den på ett ganska rakt framåt sätt.

Varje scoped kan tilldelas en namngiven livslängd genom att lägga till ett allmänt formatargument i syntaxen. Till exempel är scoped<'a> ett värde som har livslängden 'a. Begränsningar som where kan sedan användas för att beskriva relationerna mellan dessa livslängder.

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

Den här metoden definierar två livslängder 'a och 'b och deras relation, särskilt att 'b är större än 'a. På så sätt kan anropsplatsen ha mer detaljerade regler för hur värden på ett säkert sätt kan överföras till metoder jämfört med de mer grova korniga regler som finns i dag.

Frågor

Följande problem är alla relaterade till det här förslaget:

Förslag

Följande förslag gäller detta förslag:

Befintliga exempel

Utf8JsonReader

Just det här kodavsnittet kräver användning av 'unsafe' eftersom det kan stöta på problem med att skicka vidare en Span<T> som kan stackallokeras till en instansmetod på en ref struct. Även om den här parametern inte fångas in måste språket anta att det är det och därmed orsakar friktion i onödan här.

Utf8JsonWriter

Det här kodfragmentet vill mutera en parameter genom att ta bort dataelement. Undantagna data kan stackallokeras för effektivitet. Även om parametern inte är undantagen tilldelar kompilatorn den en säker kontext utanför omslutningsmetoden eftersom det är en parameter. Detta innebär att för att använda stackallokering måste implementeringen använda unsafe för att tilldela tillbaka till parametern efter att datan har avslutats.

Roliga exempel

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

Sparsam lista

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

Exempel och anteckningar

Nedan visas en uppsättning exempel som visar hur och varför reglerna fungerar som de ska. Det finns flera exempel som visar farliga beteenden och hur reglerna förhindrar att de inträffar. Det är viktigt att ha dessa i åtanke när du gör justeringar i förslaget.

Referenstilldelnings- och samtalswebbplatser

Visar hur referenstilldelning och metodanrop fungerar tillsammans.

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

Referenstilldelning och osäkra flyktsekvenser

Orsaken till följande rad i referenstilldelningsregler kanske inte är uppenbar vid första anblicken:

e1 måste ha samma säkra kontext som e2

Det beror på att livslängden för de värden som pekas på av ref platser är invarianta. Indirektionen hindrar oss från att tillåta någon form av varians här, även till smalare livslängder. Om en begränsning är tillåten öppnar det följande osäkra kod:

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

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

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

För en ref till icke-ref struct är den här regeln enkelt uppfylld eftersom värdena alla har samma säker kontext. Den här regeln spelar egentligen bara in när värdet är en ref struct.

Det här beteendet hos ref kommer också att vara viktigt i framtiden där vi tillåter ref-fälten att ref struct.

begränsade lokala platser

Användningen av scoped på lokala variabler är särskilt användbar för kodmönster som villkorligt tilldelar värden med olika säker-kontext till lokala variabler. Det innebär att kod inte längre behöver förlita sig på initieringstricks som = stackalloc byte[0] för att definiera en lokal säker kontext utan kan nu helt enkelt använda 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];
}

Det här mönstret visas ofta i lågnivåkod. När ref struct är Span<T>, kan ovanstående trick användas. Det är dock inte tillämpligt för andra ref struct typer och kan leda till att kod på låg nivå behöver tillgripa unsafe för att kringgå oförmågan att korrekt ange livslängden.

parametervärden inom ett specifikt område

En källa till upprepad friktion i kod på låg nivå är att standard escape-sekvensen för parametrar är förlåtande. De är i en säker kontext till anroparkontexten . Detta är en lämplig standard eftersom det stämmer med kodningsmönstren för .NET som helhet. I lågnivåkod finns det dock en större användning av ref struct och den här standardinställningen kan orsaka friktion med andra delar av referenssäkerhetskontextreglerna.

Den huvudsakliga friktionspunkten inträffar på grund av metodargument måste matcha regel. Den här regeln spelar oftast in med instansmetoder på ref struct där minst en parameter också är en ref struct. Det här är ett vanligt mönster i lågnivåkod där ref struct typer ofta använder Span<T> parametrar i sina metoder. Det sker till exempel på alla skrivstilar ref struct som använder Span<T> för att skicka runt buffertar.

Den här regeln finns för att förhindra scenarier som följande:

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

I princip finns den här regeln eftersom språket måste förutsätta att alla indata till en metod inte uppfyller deras högsta tillåtna säker kontext. När det finns ref eller out parametrar, inklusive mottagarna, är det möjligt för indata att fly som fält i dessa ref värden (som händer i RS.Set ovan).

I praktiken finns det många sådana metoder som skickar ref struct som parametrar som aldrig har för avsikt att samla in dem i utdata. Det är bara ett värde som används i den aktuella metoden. Till exempel:

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

För att kunna kringgå denna lågnivåkod används unsafe-knep för att lura kompilatorn om livstiden för ref struct. Detta minskar avsevärt värdeförslaget för ref struct eftersom de är avsedda att vara ett sätt att undvika unsafe samtidigt som du fortsätter att skriva kod med höga prestanda.

Det är här scoped är ett effektivt verktyg för ref struct parametrar eftersom det tar bort dem från övervägandet eftersom de returneras från metoden enligt de uppdaterade metodargumenten måste matcha regeln. En ref struct parameter som förbrukas, men aldrig returneras, kan märkas som scoped för att göra anropswebbplatser mer flexibla.

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

Förhindra knepig referenstilldelning från readonly mutation

När en ref tas till ett readonly fält i en konstruktor eller init medlem är typen ref inte ref readonly. Det här är ett långvarigt beteende som tillåter kod som följande:

struct S
{
    readonly int i; 

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

    static void M(ref int i) { }
}

Det innebär dock ett potentiellt problem om en sådan ref kunde lagras i ett ref fält på samma typ. Det skulle möjliggöra direkt mutation av en readonly struct från en instansmedlem:

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

Förslaget förhindrar dock detta eftersom det strider mot reglerna för referenssäker kontext. Tänk på följande:

  • referenssäker kontext för this är funktionsmedlem och säker kontext är anroparkontext. Dessa är båda standard för this i en struct medlem.
  • referenssäker kontext för i är funktionsmedlem. Detta faller ut från fältlivslängdsregler. Specifikt regel 4.

Då är raden r = ref i ogiltig av referenstilldelningsregler.

Dessa regler var inte avsedda att förhindra det här beteendet men gör det som en bieffekt. Det är viktigt att ha detta i åtanke för alla framtida regeluppdateringar för att utvärdera påverkan på scenarier som detta.

Fånig cyklisk tilldelning

En aspekt som den här designen hade problem med är hur en ref kan returneras fritt från en metod. Att tillåta att alla ref returneras lika fritt som normala värden är förmodligen vad de flesta utvecklare intuitivt förväntar sig. Det möjliggör dock patologiska scenarier som kompilatorn måste tänka på vid beräkning av referenssäkerhet. Tänk på följande:

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

Det här är inte ett kodmönster som vi förväntar oss att utvecklare ska använda. Men när en ref kan returneras med samma livslängd som ett värde är det lagligt enligt reglerna. Kompilatorn måste ta hänsyn till alla rättsfall vid utvärdering av ett metodanrop och detta leder till att sådana API:er i praktiken är oanvändbara.

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

För att göra dessa API:er användbara säkerställer kompilatorn att den ref livslängden för en ref parameter är mindre än livslängden för alla referenser i det associerade parametervärdet. Det här är anledningen till att ska vara en referenssäker kontext för ref till ref struct ska vara endast för retur och out ska vara anroparkontext. Det förhindrar cyklisk tilldelning på grund av skillnaden i livslängd.

Observera att [UnscopedRef]främjarref-safe-context för alla ref till ref struct värden till anroparkontext, och därmed möjliggör det cyklisk tilldelning och tvingar fram en viral användning av [UnscopedRef] upp genom samtalskedjan:

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

På samma sätt tillåter [UnscopedRef] out en cyklisk tilldelning eftersom parametern har både safe-context och ref-safe-context för return-only.

Det är användbart att uppgradera [UnscopedRef] ref till anropskontexten när typen är inte en ref struct (observera att vi vill hålla reglerna enkla så att de inte skiljer mellan referenser till ref kontra icke-ref strukturer):

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

När det gäller avancerade anteckningar skapar [UnscopedRef] design följande:

ref struct S { }

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

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

readonly kan inte vara djupt genom referensfält

Överväg kodexemplet nedan:

ref struct S
{
    ref int Field;

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

När du utformar reglerna för ref-fält på readonly-instanser i ett vakuum kan reglerna utformas giltiga så att det ovanstående är tillåtet eller förbjudet. I princip kan readonly vara djup genom ett ref-fält eller endast gälla för ref. Att endast tillämpa på ref förhindrar att referensen omplaceras, men tillåter normal tilldelning, vilket förändrar det refererade värdet.

Den här designen finns dock inte i ett vakuum, den utformar regler för typer som redan har ref fält. Den mest framträdande av vilka, Span<T>, har redan ett starkt beroende av att readonly inte är djup här. Det primära scenariot är möjligheten att tilldela till fältet ref via en readonly instans.

readonly ref struct SpanOfOne
{
    readonly ref int Field;

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

Det innebär att vi måste välja den ytliga tolkningen av readonly.

Modelleringskonstruktorer

En subtil designfråga är: Hur modelleras konstruktorkroppar för referenssäkerhet? I princip hur analyseras följande konstruktor?

ref struct S
{
    ref int field;

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

Det finns ungefär två metoder:

  1. Modell som en static-metod där this är en lokal där dess är
  2. Modellera som en static metod där this är en out parameter.

Ytterligare en konstruktor måste uppfylla följande invarianter:

  1. Kontrollera att ref parametrar kan samlas in som ref fält.
  2. Säkerställ att ref till fälten i this inte kan undkommas via ref parametrarna. Det skulle strida mot komplicerad referenstilldelning.

Avsikten är att välja det formulär som uppfyller våra invarianter utan att införa några särskilda regler för konstruktorer. Med tanke på att den bästa modellen för konstruktorer visar this som en out parameter. returnerar endast typ av out tillåter oss att tillfredsställa alla invarianter ovan utan något särskilt hölje:

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

Metodargumenten måste överensstämma

Metodargumenten måste matcha regeln är en vanlig källa till förvirring för utvecklare. Det är en regel som har ett antal specialfall som är svåra att förstå om du inte är bekant med resonemanget bakom regeln. För att bättre förstå orsakerna till regeln förenklar vi referenssäker kontext och säker kontext att helt enkelt kontext.

Metoder kan ganska liberalt returnera tillstånd som skickas till dem som parametrar. I stort sett kan alla nåbara tillstånd som inte är scoped returneras (inklusive återvändandet av ref). Detta kan returneras direkt via en return-instruktion eller indirekt genom att tilldela till ett ref värde.

Direktreturer utgör inte några större problem för referenssäkerheten. Kompilatorn behöver helt enkelt titta på alla returnerbara indata till en metod och sedan effektivt begränsa returvärdet till det minsta kontext av indata. Det returnerade värdet genomgår sedan normal bearbetning.

Indirekta returer utgör ett betydande problem eftersom alla ref både är indata och utdata till metoden. Dessa utdata har redan en känd kontext. Kompilatorn kan inte härleda nya, utan måste överväga dem på den aktuella nivån. Det innebär att kompilatorn måste titta på varje enskild ref som kan tilldelas i den anropade metoden, utvärdera kontextoch sedan kontrollera att inga returnerbara indata till metoden har en mindre kontext än den ref. Om något sådant fall finns måste metodanropet vara olagligt eftersom det kan strida mot ref säkerhet.

Metodargumenten måste matcha, vilket är en säkerhetskontroll som kompilatorn utför.

Ett annat sätt att utvärdera detta som ofta är enklare för utvecklare att tänka på är att göra följande övning:

  1. Titta på metoddefinitionen och identifiera alla platser där tillståndet indirekt kan returneras: a. Föränderliga ref parametrar som pekar på ref struct b. Föränderliga ref parametrar med referenstilldelade ref fält c. Tilldelningsbara ref-parametrar eller ref-fält som pekar på ref struct (överväg detta rekursivt)
  2. Titta på samtalswebbplatsen a. Identifiera de kontexter som överensstämmer med de platser som identifieras ovan b. Identifiera sammanhangen för alla indata till metoden som kan returneras (alignera inte med scoped-parametrar)

Om något värde i 2.b är mindre än 2.a måste metodanropet vara ogiltigt. Låt oss titta på några exempel för att illustrera reglerna:

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

Genom att titta på samtalet till F0, låt oss gå igenom (1) och (2). Parametrarna med potential för indirekt avkastning är a och b eftersom båda kan tilldelas direkt. Argumenten som överensstämmer med dessa parametrar är:

  • a som mappas till x och har kontexten av samtalskontexten
  • b som mappas till y som har med kontext av funktionsmedlem

Uppsättningen returnerbara indata till metoden är

  • x med escape-scope av i uppringarkontext
  • ref x med escape-scope av i uppringarkontext
  • y med undflykt-område för funktionsmedlem

Värdet ref y kan inte returneras eftersom det mappas till en scoped ref därför betraktas det inte som indata. Men med tanke på att det finns minst en indata med ett mindre escape-omfång (y argument) än en av utdata (x argument) så är metodanropet olagligt.

En annan variant är följande:

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

Återigen är parametrarna med potential för indirekt retur a och b eftersom båda kan tilldelas direkt. Men b kan undantas eftersom det inte pekar på en ref struct, och därför inte kan användas för att lagra ref tillstånd. Därför har vi:

  • a som mappas till x och har kontexten av samtalskontexten

Uppsättningen returnerbara indata till metoden är:

  • x med kontext av anropar-kontext
  • ref x med kontext av anropar-kontext
  • ref y med kontext av funktionsmedlem

Eftersom det finns minst en indata med ett mindre escape-omfång (ref y argument) än ett av utdata (x argument) är metodanropet ogiltigt.

Det här är den logik som regeln att metodargumenten måste matcha försöker omfatta. Det går längre genom att använda både scoped och readonly som sätt att ta bort indata från övervägande respektive ta bort ref som utdata (kan inte tilldela till en readonly ref så det inte kan vara en källa till utdata). De här specialfallen lägger till komplexitet i reglerna, men det görs till förmån för utvecklaren. Kompilatorn försöker ta bort alla indata och utdata som den vet inte kan bidra till resultatet för att ge utvecklarna maximal flexibilitet när de anropar en medlem. Precis som överbelastningslösning är det värt att göra våra regler mer komplexa när det skapar mer flexibilitet för konsumenterna.

Exempel på härledda säker kontext av deklarationsuttryck

Relaterat till Slutsats säker kontext av deklarationsuttryck.

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

Observera att den lokala kontext som är resultatet av den scoped modifieraren är den smalaste som kan användas för variabeln– att vara smalare skulle innebära att uttrycket refererar till variabler som endast deklareras i en smalare kontext än uttrycket.