Consenti "ref" e "unsafe" negli iteratori e nel codice asincrono.

Nota

Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tal differenze vengono acquisite nelle note pertinenti del language design meeting (LDM) .

Nell'articolo sulle specifiche di puoi trovare ulteriori informazioni riguardanti il processo di adozione degli speclet delle caratteristiche nello standard del linguaggio C#.

Problema del campione: https://github.com/dotnet/csharplang/issues/1331

Sommario

Unificare il comportamento tra iteratori e metodi asincroni. Specificamente:

  • Consenti ref/ref struct locali e blocchi unsafe negli iteratori e nei metodi asincroni, a condizione che siano usati in segmenti di codice senza yield o await.
  • Avvisa yield all'interno di lock.

Motivazione

Non è necessario impedire ref/ref struct locali e blocchi in unsafe nei metodi asincroni/iteratori se non vengono usati in yield o await, perché non devono essere sollevati.

async void M()
{
    await ...;
    ref int x = ...; // error previously, proposed to be allowed
    x.ToString();
    await ...;
    // x.ToString(); // still error
}

Modifiche che rompono

Non sono state apportate modifiche significative nella specifica del linguaggio, ma c'è una modifica di rilievo nell'implementazione di Roslyn (a causa di una violazione della specifica).

Roslyn viola la parte della specifica che indica che gli iteratori introducono un contesto sicuro (§13.3.1). Ad esempio, se è presente un unsafe class con un metodo iteratore che contiene una funzione locale, tale funzione locale eredita il contesto non sicuro dalla classe , anche se dovrebbe essere stato in un contesto sicuro in base alla specifica a causa del metodo iteratore. In effetti, l'intero metodo iteratore ha ereditato il contesto non sicuro in Roslyn, è stato semplicemente non consentito usare eventuali costrutti non sicuri negli iteratori. In LangVersion >= 13, gli iteratori introdurranno correttamente un contesto sicuro perché vogliamo consentire costrutti non sicuri negli iteratori.

unsafe class C // unsafe context
{
    System.Collections.Generic.IEnumerable<int> M() // an iterator
    {
        yield return 1;
        local();
        async void local()
        {
            int* p = null; // allowed in C# 12; error in C# 13 (breaking change)
            await Task.Yield(); // error in C# 12, allowed in C# 13
        }
    }
}

Nota:

  • Il problema dell'interruzione può essere aggirato semplicemente aggiungendo il modificatore unsafe alla funzione locale.
  • Ciò non influisce sulle espressioni lambda perché "ereditano" il "contesto iteratore" e pertanto non è stato possibile usare costrutti non sicuri all'interno di essi.

Progettazione dettagliata

Le modifiche seguenti sono legate a LangVersion, ovvero, C# 12 e versioni precedenti continueranno a non consentire variabili locali simili a ref e blocchi unsafe nei metodi asincroni e iteratori, mentre C# 13 rimuoverà queste restrizioni come descritto di seguito. Tuttavia, i chiarimenti delle specifiche che corrispondono all'implementazione esistente di Roslyn devono essere applicati in tutte le "LangVersions".

§13.3.1 Blocchi > generale:

Un blocco che contiene una o più istruzioni yield (§13.15) viene chiamato blocco iteratore, anche se tali istruzioni yield sono contenute solo indirettamente in blocchi annidati (esclusi espressioni lambda annidate e funzioni locali).

[...]

È un errore di compilazione che un blocco iteratore contenga un contesto unsafe (§23.2). Un blocco iteratore definisce sempre un contesto sicuro, anche quando la relativa dichiarazione è annidata in un contesto non sicuro. Il blocco iteratore usato per implementare un iteratore (§15.14) definisce sempre un contesto sicuro, anche quando la dichiarazione dell'iteratore è annidata in un contesto non sicuro.

Da questa specifica deriva anche:

  • Se una dichiarazione di iteratore è contrassegnata con il modificatore unsafe, la firma si trova in un ambito non sicuro, ma il blocco iteratore usato per implementare tale iteratore definisce ancora un ambito sicuro.
  • La funzione di accesso set di una proprietà iteratore o di un indicizzatore (ad esempio, la relativa funzione di accesso get viene implementata tramite un blocco iteratore) "eredita" il relativo ambito sicuro/non sicuro dalla dichiarazione.
  • Ciò non influisce sulle dichiarazioni parziali senza implementazione perché sono solo firme e non possono avere un corpo iteratore.

Si noti che in C# 12 si tratta di un errore per avere un metodo iteratore contrassegnato con il modificatore unsafe, ma consentito in C# 13 a causa della modifica della specifica.

Per esempio:

using System.Collections.Generic;
using System.Threading.Tasks;

class A : System.Attribute { }
unsafe partial class C1
{ // unsafe context
    [/* unsafe context */ A]
    IEnumerable<int> M1(
        /* unsafe context */ int*[] x)
    { // safe context (this is the iterator block implementing the iterator)
        yield return 1;
    }
    IEnumerable<int> M2()
    { // safe context (this is the iterator block implementing the iterator)
        unsafe
        { // unsafe context
            { // unsafe context (this is *not* the block implementing the iterator)
                yield return 1; // error: `yield return` in unsafe context
            }
        }
    }
    [/* unsafe context */ A]
    unsafe IEnumerable<int> M3(
        /* unsafe context */ int*[] x)
    { // safe context
        yield return 1;
    }
    [/* unsafe context */ A]
    IEnumerable<int> this[
        /* unsafe context */ int*[] x]
    { // unsafe context
        get
        { // safe context
            yield return 1;
        }
        set { /* unsafe context */ }
    }
    [/* unsafe context */ A]
    unsafe IEnumerable<int> this[
        /* unsafe context */ long*[] x]
    { // unsafe context (the iterator declaration is unsafe)
        get
        { // safe context
            yield return 1;
        }
        set { /* unsafe context */ }
    }
    IEnumerable<int> M4()
    {
        yield return 1;
        var lam1 = async () =>
        { // safe context
          // spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
            await Task.Yield(); // error in C# 12, allowed in C# 13
            int* p = null; // error in both C# 12 and C# 13 (unsafe in iterator)
        };
        unsafe
        {
            var lam2 = () =>
            { // unsafe context, lambda cannot be an iterator
                yield return 1; // error: yield cannot be used in lambda
            };
        }
        async void local()
        { // safe context
          // spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
            await Task.Yield(); // error in C# 12, allowed in C# 13
            int* p = null; // allowed in C# 12, error in C# 13 (breaking change in Roslyn)
        }
        local();
    }
    public partial IEnumerable<int> M5() // unsafe context (inherits from parent)
    { // safe context
        yield return 1;
    }
}
partial class C1
{
    public partial IEnumerable<int> M5(); // safe context (inherits from parent)
}
class C2
{ // safe context
    [/* unsafe context */ A]
    unsafe IEnumerable<int> M(
        /* unsafe context */ int*[] x)
    { // safe context
        yield return 1;
    }
    unsafe IEnumerable<int> this[
        /* unsafe context */ int*[] x]
    { // unsafe context
        get
        { // safe context
            yield return 1;
        }
        set { /* unsafe context */ }
    }
}

dichiarazioni di variabili locali di riferimento (13.6.2.4):

Si tratta di un errore in fase di compilazione per dichiarare una variabile locale ref o una variabile di un tipo ref struct, all'interno di un metodo dichiarato con il method_modifierasynco all'interno di un iteratore (§15.14).Si tratta di un errore in fase di compilazione per dichiarare e usare (anche in modo implicito nel codice sintetizzato dal compilatore) una variabile locale ref o una variabile di un tipo ref struct tra espressioni await o istruzioni yield return. Più precisamente, l'errore è basato sul meccanismo seguente: dopo un'espressione await (§12.9.8) o un'istruzione yield return (§13.15), tutte le variabili locali di riferimento e le variabili di un tipo di ref struct nell'ambito vengono considerate sicuramente non firmate (§9,4).

Si noti che questo errore non viene eseguito il downgrade a un avviso in contesti di unsafe come altri errori di sicurezza di riferimento. Ciò è dovuto al fatto che queste variabili locali di tipo ref non possono essere modificate nei contesti di unsafe senza basarsi sui dettagli di implementazione di come funziona la riscrittura della macchina a stati, pertanto questo errore non rientra nei limiti di ciò che si vuole declassare a avvisi nei contesti di unsafe.

§15.14.1 Iteratori > Generale:

Quando un membro della funzione viene implementato usando un blocco iteratore, è un errore di compilazione se l'elenco di parametri formali del membro della funzione specifica qualsiasi in, ref readonly, out, o un parametro ref oppure un parametro di un tipo ref structo di un tipo di puntatore.

Non è necessaria alcuna modifica nella specifica per consentire blocchi unsafe che non contengono awaitnei metodi asincroni, perché la specifica non ha mai consentito blocchi unsafe nei metodi asincroni. Tuttavia, la specifica avrebbe sempre dovuto vietare await all'interno di blocchi unsafe (aveva già vietato yield in unsafe in §13.3.1 come indicato in precedenza), quindi proponiamo la seguente modifica alla specifica:

§15.15.1 Funzioni asincrone > General:

Si tratta di un errore in fase di compilazione per l'elenco di parametri formali di una funzione asincrona per specificare qualsiasi parametro in, outo ref o qualsiasi parametro di un tipo ref struct.

È un errore in fase di compilazione per un contesto non sicuro (§23.2) per contenere un'espressione await (§12.9.8) o un'istruzione yield return (§13.15).

§23.6.5 L'operatore di indirizzamento:

Verrà segnalato un errore in fase di compilazione per l'acquisizione dell'indirizzo di una variabile locale o di un parametro in un iteratore.

Attualmente, prendere l'indirizzo di un locale o di un parametro in un metodo asincrono è un avviso nella nuova ondata di avvisi C# 12.


Si noti che più costrutti possono funzionare grazie a ref consentiti all'interno di segmenti, senza await e yield, nei metodi asincroni/iteratori, anche se non è necessaria alcuna modifica specifica per loro, poiché tutto deriva dalle modifiche specifiche precedentemente indicate.

using System.Threading.Tasks;

ref struct R
{
    public ref int Current { get { ... }};
    public bool MoveNext() => false;
    public void Dispose() { }
}
class C
{
    public R GetEnumerator() => new R();
    async void M()
    {
        await Task.Yield();
        using (new R()) { } // allowed under this proposal
        foreach (var x in new C()) { } // allowed under this proposal
        foreach (ref int x in new C()) { } // allowed under this proposal
        lock (new System.Threading.Lock()) { } // allowed under this proposal
        await Task.Yield();
    }
}

Alternative

  • ref / ref struct variabili locali possono essere consentite solo in blocchi (§13.3.1) che non contengono await/yield:

    // error always since `x` is declared/used both before and after `await`
    {
        ref int x = ...;
        await Task.Yield();
        x.ToString();
    }
    // allowed as proposed (`x` does not need to be hoisted as it is not used after `await`)
    // but alternatively could be an error (`await` in the same block)
    {
        ref int x = ...;
        x.ToString();
        await Task.Yield();
    }
    
  • yield return all'interno di lock potrebbe essere un errore (ad esempio await all'interno di lock) o un avviso di tipo warning, ma causerebbe un'interruzione: https://github.com/dotnet/roslyn/issues/72443. Si noti che il nuovo Lock-object-based lock segnala errori in fase di compilazione per yield returnnel suo corpo, perché questa istruzione lock è equivalente a un using su un ref struct che non consente yield returnnel suo corpo.

  • Le variabili all'interno di metodi asincroni o iteratori non devono essere "fisse", ma piuttosto "spostabili" se devono essere spostate nei campi della macchina a stati (in modo analogo alle variabili acquisite). Nota che si tratta di un bug preesistente nella specifica indipendente dal resto della proposta, perché i blocchi unsafe all'interno dei metodi async sono sempre stati consentiti. Attualmente c'è un avviso per questo nell'ondata di avvisi C# 12 e renderlo un errore sarebbe un cambiamento che causerebbe un'interruzione.

    Variabili fisse e mobili: §23.4

    In termini precisi, una variabile fissa è una delle seguenti:

    • Variabile risultante da un simple_name (§12.8.4) che fa riferimento a una variabile locale, un parametro value, o matrice di parametri, a meno che la variabile non venga acquisita da una funzione anonima (§12.19.6.2) una funzione locale (§13.6.4) o la variabile deve essere spostata come parte di un metodo asincrono (§15.15) o un iteratore (§15.14).
    • [...]
    • Attualmente, nella wave degli avvisi di C# 12, abbiamo un avviso esistente per l'uso di address-of nei metodi asincroni e un errore proposto per l'uso di address-of negli iteratori segnalato per LangVersion 13+ (non è necessario segnalarlo nelle versioni precedenti poiché era impossibile utilizzare codice non sicuro negli iteratori). Possiamo rilassare entrambi questi requisiti per applicarli solo alle variabili effettivamente promosse, non a tutte le variabili locali e ai parametri.

    • Potrebbe essere possibile usare fixed per ottenere l'indirizzo di una variabile ospitata o acquisita, anche se il fatto che tali campi sono un dettaglio di implementazione, quindi in altre implementazioni potrebbe non essere possibile usare fixed su di essi. Si noti che si propone solo di considerare anche le variabili sollevate come "spostabili", ma le variabili acquisite erano già "spostabili" e fixed non era consentito per loro.

  • È possibile consentire await/yield all'interno di unsafe tranne all'interno di istruzioni fixed (il compilatore non può aggiungere variabili attraverso i limiti del metodo). Ciò potrebbe comportare un comportamento imprevisto, ad esempio intorno a stackalloc, come descritto nel punto elenco annidato riportato di seguito. Tuttavia, il sollevamento dei puntatori è supportato anche oggi in alcuni scenari (esiste un esempio riportato di seguito in relazione ai puntatori come argomenti), quindi non dovrebbero esserci altre limitazioni nel permetterlo.

    • È possibile disabilitare la variante non sicura di stackalloc nei metodi asincroni/iteratori, perché il buffer allocato nello stack non persiste attraverso le istruzioni await/yield. Non si ritiene necessario perché il codice non sicuro per progettazione non impedisce l'uso dopo il libero. Si noti che si potrebbe anche consentire l'uso di stackalloc in modo non sicuro, a condizione che non venga utilizzato in await/yield, ma potrebbe essere difficile da analizzare (il puntatore risultante può essere passato in qualsiasi variabile puntatore). In alternativa, potremmo richiedere che sia fixed nei metodi asincroni/iteratori. Ciò scoraggiare usarlo in await/yield, ma non corrisponderebbe alla semantica di fixed perché l'espressione stackalloc non è un valore spostabile. Si noti che non sarebbe impossibile usare il risultato del stackalloc in await/yield in modo analogo come è possibile salvare qualsiasi puntatore fixed oggi in un'altra variabile puntatore e usarlo all'esterno del blocco di fixed.
  • Gli iteratori e i metodi asincroni possono avere parametri puntatore. Avrebbero bisogno di essere sollevati, ma questo non dovrebbe essere un problema in quanto il sollevamento dei puntatori è supportato anche oggi, ad esempio:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • La proposta mantiene (ed estende/chiarisce) la specifica preesistente che i metodi iteratori iniziano un contesto sicuro anche se si trovano in un contesto non sicuro. Ad esempio, un metodo iteratore non è un contesto non sicuro anche se è definito in una classe con il modificatore unsafe. In alternativa, è possibile che gli iteratori ereditino il modificatore unsafe come fanno altri metodi.

    • Vantaggio: rimuove la complessità dalla specifica e dall'implementazione.
    • Vantaggio: allinea gli iteratori ai metodi asincroni (una delle motivazioni della funzionalità).
    • Svantaggio: gli iteratori all'interno di classi non sicure non possono contenere istruzioni yield return, tali iteratori devono essere definiti in una dichiarazione di classe parziale separata senza il modificatore unsafe.
    • Svantaggio: si tratta di una modifica radicale in LangVersion=13 (gli iteratori nelle classi non sicure sono consentiti in C# 12).
  • Anziché un iteratore che definisce un contesto sicuro solo per il corpo, l'intera firma potrebbe essere un contesto sicuro. Ciò non è coerente con il resto del linguaggio in cui i corpi normalmente non influiscono sulle dichiarazioni, ma qui una dichiarazione sarebbe sicura o non sicura a seconda che il corpo sia un iteratore o meno. Si tratterebbe anche di una modifica che causa un'interruzione con LangVersion=13, poiché in C# 12 le firme degli iteratori non sono sicure (ad esempio, possono contenere parametri di array pointer).

  • Applicazione del modificatore unsafe a un iteratore:

    • Potrebbe influire sul corpo e sulla firma. Questi iteratori non sarebbero molto utili perché i loro corpi non sicuri non potrebbero contenere yield returnma solo yield break.
    • Potrebbe esserci un errore in LangVersion >= 13 come in LangVersion <= 12, perché non è molto utile avere un membro iteratore non sicuro, dato che consente solo di avere parametri di matrice puntatore o setter non sicuri senza un blocco non sicuro aggiuntivo. Ma in futuro potrebbero essere consentiti argomenti puntatore normali.
  • Modifica importante di Roslyn:

    • È possibile mantenere il comportamento corrente (e anche modificare la specifica in modo che corrisponda) ad esempio introducendo il contesto sicuro nel metodo iteratore, ma ripristinando quindi il contesto non sicuro nella funzione locale.
    • Oppure potremmo interrompere tutte le LangVersions, non solo la 13 e le versioni di Lang successive.
    • È anche possibile semplificare più drasticamente le regole facendo sì che gli iteratori ereditino il contesto non sicuro, come fanno tutti gli altri metodi. Come discusso in precedenza. Potrebbe essere eseguito in tutte le versioni linguistiche o solo per LangVersion >= 13.

Riunioni di progettazione

  • 2024-06-03: revisione post-implementazione dello speclet