Miglioramenti delle strutture di basso livello
Nota
Questo articolo è una specifica delle 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. Tali differenze vengono acquisite nelle note
Altre informazioni sul processo per l'adozione delle speclette di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche e.
Sommario
Questa proposta è un'aggregazione di diverse proposte differenti per struct
miglioramenti delle prestazioni: campi ref
e la possibilità di ignorare le impostazioni predefinite per la durata. L'obiettivo è una progettazione che tiene conto delle varie proposte per creare un unico set di caratteristiche generali per miglioramenti di basso livello struct
.
Nota: le versioni precedenti di questa specifica usavano i termini "ref-safe-to-escape" e "safe-to-escape", introdotti nella specifica della funzionalità span safety. Il comitato standard ECMA ha modificato i nomi rispettivamente in "ref-safe-context" e "safe-context". I valori del contesto sicuro sono stati perfezionati per un uso coerente di "declaration-block", "function-member" e "caller-context". Gli speclet avevano usato formulazioni diverse per questi termini e usavano anche "safe-to-return" come sinonimo di "contesto chiamante". Questo speclet è stato aggiornato per usare i termini nello standard C# 7.3.
Non tutte le funzionalità descritte in questo documento sono state implementate in C# 11. C# 11 include:
-
ref
campi escoped
[UnscopedRef]
Queste funzionalità rimangono proposte aperte per una versione futura di C#:
-
ref
campi daref struct
- Tipi con restrizioni Sunset
Motivazione
Le versioni precedenti di C# hanno aggiunto una serie di funzionalità di prestazioni di basso livello al linguaggio: elementi per la restituzione ref
, ref struct
, puntatori a funzione, ecc. Queste caratteristiche hanno consentito agli sviluppatori .NET di scrivere codice ad alte prestazioni, continuando a sfruttare le regole del linguaggio C# per la sicurezza dei tipi e della memoria. Ha inoltre consentito la creazione di tipi di prestazioni fondamentali nelle librerie .NET come Span<T>
.
Poiché queste funzionalità hanno guadagnato trazione negli sviluppatori dell'ecosistema .NET, sia interni che esterni, ci hanno fornito informazioni sui punti di attrito rimanenti nell'ecosistema. Luoghi in cui è ancora necessario utilizzare il codice unsafe
per svolgere il proprio lavoro, o dove il runtime deve gestire in modo speciale tipi come Span<T>
.
Attualmente Span<T>
viene eseguito usando il tipo internal
ByReference<T>
che viene considerato effettivamente dal runtime come un campo ref
. Ciò offre il vantaggio dei campi ref
, ma con lo svantaggio che il linguaggio di programmazione non fornisce alcuna verifica di sicurezza per questo, come fa per altri usi di ref
. Inoltre, solo dotnet/runtime può utilizzare questo tipo in quanto è classificato come internal
, quindi le terze parti non possono progettare le proprie primitive basate sui campi ref
. Parte della motivazione di questo lavoro consiste nel rimuovere ByReference<T>
e usare campi ref
appropriati in tutte le codebase.
Questa proposta prevede di risolvere questi problemi basandosi sulle nostre funzionalità di basso livello esistenti. In particolare, mira a:
- Consentire ai tipi
ref struct
di dichiarare i campiref
. - Consentire al runtime di definire completamente
Span<T>
usando il sistema di tipi C# e rimuovere il tipo speciale comeByReference<T>
- Consentire ai tipi di
struct
di restituireref
ai relativi campi. - Consenti al runtime di rimuovere gli utilizzi di
unsafe
causati da limitazioni dei valori predefiniti di durata - Consentire la dichiarazione di buffer
fixed
sicuri per i tipi gestiti e non gestiti instruct
Progettazione dettagliata
Le regole per la sicurezza ref struct
sono definite nel documento di sicurezza utilizzando i termini precedenti. Tali regole sono state incorporate nello standard C# 7 in §9.7.2 e §16.4.12. Questo documento descriverà le modifiche necessarie a questo documento in seguito a questa proposta. Una volta accettata come funzionalità approvata, queste modifiche verranno incorporate in tale documento.
Una volta completata la progettazione, la definizione di Span<T>
sarà la seguente:
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;
}
}
Specificare i campi di riferimento e l'ambito
Il linguaggio permetterà agli sviluppatori di dichiarare campi ref
all'interno di un ref struct
. Ciò può essere utile, ad esempio, quando si incapsulano grandi istanze modificabili struct
o si definiscono tipi ad alte prestazioni come Span<T>
nelle librerie oltre al runtime.
ref struct S
{
public ref int Value;
}
Un campo ref
verrà emesso nei metadati usando la firma ELEMENT_TYPE_BYREF
. Questo non è diverso da come emettiamo le variabili locali ref
o gli argomenti ref
. Ad esempio, ref int _field
verrà emesso come ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. Questo ci richiederà di aggiornare ECMA335 per consentire questa voce, ma questo dovrebbe essere piuttosto semplice.
Gli sviluppatori possono continuare a inizializzare un ref struct
con un campo ref
usando l'espressione default
, nel qual caso tutti i campi ref
dichiarati avranno il valore null
. Qualsiasi tentativo di utilizzare tali campi comporterà il lancio di un NullReferenceException
.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Anche se il linguaggio C# fa finta che un ref
non possa essere null
questo è legale a livello di runtime e ha una semantica ben definita. Gli sviluppatori che introducono campi ref
nei loro tipi devono essere consapevoli di questa possibilità e devono essere fortemente scoraggiati dalla perdita di questi dettagli nel codice di utilizzo. I campi ref
devono invece essere verificati come non nulli usando gli helper di runtime e segnalando un'eccezione quando un struct
non inizializzato viene usato in modo scorretto.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Un campo ref
può essere combinato nei modi seguenti con i modificatori readonly
:
-
readonly ref
: si tratta di un campo il cui riferimento non può essere riassegnato al di fuori di un costruttore o nei metodiinit
. Può essere assegnato un valore pure al di fuori di tali contesti -
ref readonly
: si tratta di un campo che può essere riassegnato come riferimento, ma non può essere assegnato come valore in alcun momento. In questo modo, un parametroin
potrebbe essere riassegnato a un camporef
. -
readonly ref readonly
: combinazione diref readonly
ereadonly 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)
}
}
Un readonly ref struct
richiederà che i campi di ref
siano dichiarati readonly ref
. Non è richiesto che essi siano dichiarati readonly ref readonly
. Ciò consente a un readonly struct
di avere mutazioni indirette tramite tale campo, ma non è diverso da un campo readonly
che punta a un tipo di riferimento oggi (altri dettagli)
Un readonly ref
verrà generato nei metadati usando il flag initonly
, come per qualsiasi altro campo. Un campo ref readonly
verrà attribuito con System.Runtime.CompilerServices.IsReadOnlyAttribute
. Un readonly ref readonly
verrà generato con entrambi gli oggetti.
Questa funzionalità richiede il supporto di runtime e le modifiche apportate alla specifica ECMA. Di conseguenza, questi verranno abilitati solo quando il flag di funzionalità corrispondente è impostato in corelib. Il problema relativo al monitoraggio dell'API esatta è tracciato qui https://github.com/dotnet/runtime/issues/64165
L'insieme delle modifiche alle regole del contesto sicuro necessarie per consentire i campi ref
è piccolo e mirato. Le regole tengono già conto dei campi ref
esistenti e che vengono utilizzati dalle API. Le modifiche devono concentrarsi solo su due aspetti: come vengono creati e come vengono riassegnati.
Prima di tutto, le regole che stabiliscono i valori di per il contesto di riferimento sicuro per i campi devono essere aggiornate per i campi ref
come segue.
Espressione nel formato
ref e.F
contesto ref-safe come segue:
- Se
è un campo , il contesto di riferimento sicuro è il contesto sicuro di . - In caso contrario, se
e
è di un tipo di riferimento, ha contesto sicuro di riferimento nel contesto del chiamante- In caso contrario, il suo contesto ref-safe è preso dal contesto ref-safe di
e
.
Ciò non rappresenta una modifica della regola, infatti le regole hanno sempre tenuto conto dell'esistenza dello stato di ref
all'interno di un ref struct
. Questo è infatti il modo in cui lo stato di ref
in Span<T>
ha sempre funzionato e le regole di consumo tengono correttamente conto di questo. La modifica qui consiste semplicemente nel consentire agli sviluppatori di accedere direttamente ai campi ref
e assicurarsi che lo facciano in base alle regole esistenti implicitamente applicate a Span<T>
.
Ciò significa che i campi ref
possono essere restituiti come ref
da un ref struct
, ma i campi normali non possono.
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;
}
Questo può sembrare un errore a prima vista, ma questo è un punto di progettazione intenzionale. Anche se non si tratta di una nuova regola introdotta da questa proposta, si riconoscono le regole già esistenti Span<T>
finora applicate e si consente agli sviluppatori di dichiarare il proprio stato ref
.
Successivamente, è necessario modificare le regole per la riassegnazione dei riferimenti per la presenza dei campi ref
. Lo scenario principale per la riassegnazione dei riferimenti è quando i costruttori ref struct
salvano i parametri ref
nei campi ref
. Il supporto sarà più generale, ma questo è lo scenario principale. Per supportare queste regole per la riassegnazione dei riferimenti, verranno modificate in modo da tenere conto dei campi ref
come indicato di seguito:
Regole di riassegnazione ref
L'operando sinistro dell'operatore = ref
deve essere un'espressione che si associa a una variabile locale ref, un parametro ref (diverso da this
), un parametro out, o un campo ref.
Per una riassegnazione di ref nel formato
e1 = ref e2
devono essere soddisfatte entrambe le condizioni seguenti:
e2
deve avere contesto di riferimento almeno di dimensioni elevate del contesto di die1
deve avere lo stesso contesto sicuro Nota
Ciò significa che il costruttore di Span<T>
desiderato funziona senza alcuna annotazione aggiuntiva:
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;
}
}
La modifica alle regole di riassegnazione ref implica che i parametri ref
possono ora sfuggire da un metodo come un campo ref
in un valore ref struct
. Come discusso nella sezione delle considerazioni di compatibilità, questo può modificare le regole per le API esistenti che non erano mai state concepite affinché i parametri di ref
sfuggissero come campi di ref
. Le regole di durata per i parametri si basano esclusivamente sulla dichiarazione non sul relativo utilizzo. Tutti i parametri ref
e in
possiedono un contesto sicuro di riferimento () all'interno del contesto del chiamante () e quindi possono ora essere restituiti da ref
o da un campo di ref
. Per supportare le API con parametri ref
che possono essere con o senza escape e quindi ripristinare la semantica del sito di chiamata di C# 10, il linguaggio introdurrà annotazioni di durata limitate.
scoped
modificatore
La parola chiave scoped
verrà usata per limitare la durata di un valore. **
Può essere applicato a un ref
o a un valore che è un ref struct
e ha l'impatto di limitare il contesto di riferimento ref-sicuro o il contesto sicuro , rispettivamente, alla durata del membro della funzione . Per esempio:
Parametro o locale | contesto-sicuro-ref | contesto sicuro |
---|---|---|
Span<int> s |
membro della funzione | contesto del chiamante |
scoped Span<int> s |
membro della funzione | membro della funzione |
ref Span<int> s |
contesto del chiamante | contesto del chiamante |
scoped ref Span<int> s |
membro della funzione | contesto del chiamante |
In questa relazione il contesto di riferimento di un valore non può mai essere più ampio del contesto sicuro.
In questo modo, le API in C# 11 possono essere annotate in modo che abbiano le stesse regole di 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]);
}
L'annotazione scoped
significa anche che il parametro this
di un struct
può ora essere definito come scoped ref T
. In precedenza, doveva essere trattato in modo speciale nelle regole come parametro ref
, che aveva regole di contesto ref-safe diverse rispetto agli altri parametri ref
(vedere tutti i riferimenti all'inclusione o all'esclusione del ricevitore nelle regole del contesto sicuro). Ora può essere espresso come un concetto generale in tutte le regole che li semplifica ulteriormente.
L'annotazione scoped
può essere applicata anche alle posizioni seguenti:
- variabili locali: questa annotazione imposta la durata come contesto sicuroo contesto di riferimento in caso di
ref
locale, a membro funzione indipendentemente dalla durata dell'inizializzatore.
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];
}
Altri usi per le scoped
nelle variabili locali vengono illustrati di seguito.
L'annotazione scoped
non può essere applicata ad altre posizioni, inclusi i valori restituiti, i campi, gli elementi della matrice e così via... Inoltre, mentre scoped
ha un impatto quando viene applicato a qualsiasi ref
, in
o out
ha effetto solo quando viene applicato ai valori ref struct
. La presenza di dichiarazioni come scoped int
non ha alcun impatto perché un ref struct
non è sempre sicuro da restituire. Il compilatore creerà una diagnostica per questi casi per evitare confusione per gli sviluppatori.
Modificare il comportamento dei parametri di out
Per limitare ulteriormente l'impatto della modifica di compatibilità che rende i parametri ref
e in
restituiti come campi di ref
, il linguaggio cambierà il valore predefinito di contesto sicuro di riferimento per i parametri out
al membro della funzione . I parametri out
verranno scoped out
implicitamente d'ora in poi. Dal punto di vista di compatibilità, ciò significa che non possono essere restituiti da ref
:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
In questo modo si aumenta la flessibilità delle API che restituiscono valori ref struct
e hanno parametri out
perché non è più necessario considerare il parametro acquisito tramite riferimento. Questo aspetto è importante perché è un modello comune nelle API di stile lettore:
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);
}
Il linguaggio non considererà più restituibili gli argomenti passati a un parametro out
. Il trattamento dell'input in un parametro out
come restituito è stato estremamente confuso per gli sviluppatori. Sovverte essenzialmente l'intento di out
forzando gli sviluppatori a considerare il valore passato dal chiamante, che non viene mai usato eccetto in quei linguaggi che non seguono out
. In futuro, le lingue che supportano ref struct
devono assicurarsi che il valore originale passato a un parametro out
non venga mai letto.
C# ottiene questo risultato tramite regole di assegnazione definite. Consentendo sia di rispettare le nostre regole di contesto sicuro di riferimento sia di permettere al codice esistente di assegnare e poi restituire i valori dei parametri out
.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Queste modifiche indicano che l'argomento di un parametro out
non contribuisce di contesto sicuro o valori di di contesto ref-safe alle chiamate al metodo. In questo modo si riduce significativamente l'impatto globale dei campi ref
e si semplifica il modo in cui gli sviluppatori concepiscono out
. Un argomento di un parametro out
non contribuisce alla restituzione, ma è semplicemente un output.
Dedurre il contesto sicuro delle espressioni di dichiarazione
- contesto del chiamante
- Se la variabile out è contrassegnata come
scoped
, allora il blocco di dichiarazione (ad esempio, membro della funzione o più ristretto). - se il tipo della variabile out è
ref struct
, considerare tutti gli argomenti per la chiamata contenitore, incluso il ricevitore:-
contesto sicuro di qualsiasi argomento in cui il parametro corrispondente non sia
out
e abbia un contesto sicuro di solo ritorno o più ampio - contesto sicuro di riferimento di qualsiasi argomento in cui il parametro corrispondente ha contesto sicuro di riferimento di sola restituzione o più ampio
-
contesto sicuro di qualsiasi argomento in cui il parametro corrispondente non sia
Vedere anche
Parametri scoped
in modo implicito
In generale ci sono due ref
posizioni che vengono dichiarate in modo implicito come scoped
:
-
this
su un metodo di istanza distruct
- parametri
out
Le regole di contesto di riferimento sicure verranno scritte in termini di scoped ref
e ref
. Ai fini del contesto sicuro di riferimento, il parametro in
è equivalente a ref
e out
è equivalente a scoped ref
. Sia in
che out
verranno richiamati in modo specifico solo quando è importante per la semantica della regola. In caso contrario, vengono considerati rispettivamente ref
e scoped ref
.
Quando si discute del contesto di riferimento-safe- di argomenti che corrispondono ai parametri in
verranno generalizzati come argomenti ref
nella specifica. Nel caso in cui l'argomento sia un lvalue, il di contesto ref-safe è quello dell'lvalue; in caso contrario, è membro della funzione. Anche in questo caso in
verrà richiamato solo quando è importante per la semantica della regola corrente.
Contesto sicuro di sola restituzione
La progettazione richiede anche l'introduzione di un nuovo contesto sicuro: di sola restituzione. È simile a contesto chiamante in quanto può essere restituito, ma può solo essere restituito tramite un'istruzione return
.
I dettagli del rivolto solo al ritorno è che è un contesto più ampio di membro della funzione ma più ristretto del contesto del chiamante. Un'espressione fornita a un'istruzione return
deve essere almeno di sola restituzione. Di conseguenza, la maggior parte delle regole esistenti non rientra. Ad esempio, l'assegnazione in un parametro ref
da un'espressione con un contesto sicuro di restituito avrà esito negativo perché è inferiore al ref
contesto sicuro del parametro che è contesto di chiamata. La necessità di questo nuovo contesto di fuga verrà discussa sotto
Esistono tre luoghi che per impostazione predefinita sono di sola restituzione .
- Un parametro
ref
oin
avrà un contesto referenziale sicuro con di sola restituzione . Questa operazione viene eseguita in parte perref struct
per evitare stupidi problemi di assegnazione ciclica. Viene eseguita intenzionalmente in modo uniforme anche per semplificare il modello e ridurre al minimo le modifiche di compatibilità. - Un parametro di
out
per unref struct
avrà di contesto sicuro di di sola restituzione. Ciò consente a return eout
di essere ugualmente espressivi. Questo non presenta il problema sciocco di assegnazione ciclica perchéout
è implicitamentescoped
, quindi il contesto ref-sicuro è ancora più piccolo rispetto al contesto sicuro . - Un parametro
per un costruttore di avrà un contesto sicuro di , utilizzato solo per il ritorno . Ciò si verifica a causa della modellazione come parametri out
.
Qualsiasi espressione o istruzione che restituisce in modo esplicito un valore da un metodo o lambda deve avere un contesto sicuroe, se applicabile, un contesto di riferimento, con un livello di sicurezza di almeno solo ritorno. Inclusi return
istruzioni, membri con corpo di espressione ed espressioni lambda.
Analogamente, qualsiasi assegnazione a un
Nota: un'espressione di tipo diverso da ref struct
ha sempre un contesto sicuro nel contesto chiamante di .
Regole per la chiamata al metodo
Le regole di contesto di riferimento sicuro per la chiamata al metodo verranno aggiornate in diversi modi. Il primo consiste nel riconoscere l'impatto che scoped
ha sugli argomenti. Per un determinato argomento expr
passato al parametro p
:
- Se
p
èscoped ref
,expr
non contribuisce contesto di riferimento quando si considerano gli argomenti.- Se
p
èscoped
,expr
non contribuisce al contesto sicuro nel considerare gli argomenti.- Se
p
èout
,expr
non contribuisce di contesto sicuro di riferimento o contesto sicuroaltri dettagli
Il linguaggio "non contribuisce" significa che gli argomenti non vengono considerati nel calcolo del valore di contesto ref-safe o del valore safe-context del risultato del metodo, rispettivamente. Ciò è perché i valori non possono contribuire a tale durata perché l'annotazione scoped
lo impedisce.
Le regole di chiamata al metodo possono ora essere semplificate. Il ricevitore non ha più bisogno di un trattamento speciale; nel caso di struct
, è ora semplicemente un scoped ref T
. Le regole di valore devono essere modificate per tenere conto delle restituzioni del campo ref
.
Un valore risultante da una chiamata al metodo
e1.M(e2, ...)
, doveM()
non restituisce ref-to-ref-struct, dispone di un contesto sicuro ricavato dal più stretto dei seguenti elementi:
- contesto del chiamante
- Quando il valore restituito è un
ref struct
, il contesto sicuro fornito da tutte le espressioni di argomento è contribuito da.- Quando il valore restituito è un
, il contesto sicuro di riferimento fornito da tutti gli argomenti Se
M()
restituisce ref-to-ref-struct, il contesto sicuro è lo stesso del contesto sicuro di tutti gli argomenti che sono ref-to-ref-struct. Si tratta di un errore se sono presenti più argomenti con contesto sicuro diverso perché gli argomenti del metodo devono corrispondere.
Le regole di chiamata ref
possono essere semplificate per:
Un valore risultante da una chiamata al metodo
ref e1.M(e2, ...)
, doveM()
non restituisce ref-to-ref-struct, è contesto di riferimento sicuro il più piccolo dei contesti seguenti:
- contesto del chiamante
- contesto sicuro fornito da tutte le espressioni dei parametri
- Il contesto di sicurezza di riferimento fornito da tutti gli argomenti
ref
Se
restituisce ref-to-ref-struct, l' di contesto ref-safe è il più strettocontesto ref-safe-safe fornito da tutti gli argomenti che sono ref-to-ref-struct.
Questa regola consente ora di definire le due varianti dei metodi desiderati:
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);
}
Regole per gli inizializzatori di oggetti
L'contesto sicuro di un'espressione di inizializzatore di oggetto è più stretta di:
- Il contesto sicuro della chiamata al costruttore.
- Il di contesto sicuro e di argomenti agli indicizzatori dell'inizializzatore membro che possono eseguire l'escape al ricevitore.
- Il contesto sicuro del RHS delle assegnazioni negli inizializzatori membro a setter non di sola lettura o nel contesto sicuro di riferimento in caso di assegnazione di riferimento.
Un altro modo di modellazione consiste nel considerare qualsiasi argomento di un inizializzatore membro che può essere assegnato al ricevitore come argomento al costruttore. Questo perché l'inizializzatore membro è effettivamente una chiamata al costruttore.
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);
Questa modellazione è importante perché dimostra che il nostro MAMM deve tenere conto specificamente degli inizializzatori di membri. Si consideri che questo caso particolare deve essere illegale poiché consente l'assegnazione di un valore con un contesto più ristretto a un contesto sicuro a uno superiore.
Gli argomenti del metodo devono corrispondere
La presenza di campi ref
indica che le regole sugli argomenti del metodo devono essere aggiornate, dato che un parametro ref
può ora essere memorizzato come campo all'interno di un argomento ref struct
del metodo. In precedenza, la regola doveva considerare solo un altro ref struct
archiviato come campo. L'impatto di questa operazione viene illustrato in le considerazioni di compatibilità. La nuova regola è ...
Per ogni invocazione di metodo
e.M(a1, a2, ... aN)
- Calcola il di contesto sicuro più stretto da:
- contesto del chiamante
- contesto sicuro di tutti gli argomenti
- Il contesto sicuro di riferimento di tutti gli argomenti ref i cui parametri corrispondenti hanno un contesto sicuro di riferimento del contesto chiamante
- Tutti gli argomenti
ref
dei tipiref struct
devono poter essere assegnati da un valore con quel contesto sicuro. Questo è un caso in cuiref
non generalizzare per includerein
eout
Per ogni invocazione di metodo
e.M(a1, a2, ... aN)
- Calcola il di contesto sicuro più stretto da:
- contesto del chiamante
- contesto sicuro di tutti gli argomenti
- Il contesto di riferimento di tutti gli argomenti ref i cui parametri corrispondenti non sono
scoped
- Tutti gli argomenti
out
dei tipiref struct
devono poter essere assegnati da un valore con quel contesto sicuro.
La presenza di scoped
consente agli sviluppatori di ridurre l'attrito creato da questa regola contrassegnando i parametri che non vengono restituiti come scoped
. Questo rimuove i loro argomenti da (1) in entrambi i casi sopra e offre maggiore flessibilità ai chiamanti.
L'impatto di questa modifica è discusso in modo più dettagliato di seguito . In generale, gli sviluppatori potranno rendere i siti di chiamata più flessibili annotando i valori non simili ai riferimenti con scoped
.
Varianza dell'ambito dei parametri
Il modificatore scoped
e l'attributo [UnscopedRef]
(vedere sotto) sui parametri influiscono anche sull'override degli oggetti, sull'implementazione dell'interfaccia e sulle regole di conversione delegate
. La firma per un overriding, un'implementazione dell'interfaccia o una conversione di tipo delegate
può:
- Aggiungere
scoped
a un parametroref
oin
- Aggiungere
scoped
a un parametroref struct
- Rimuovere
[UnscopedRef]
da un parametroout
- Rimuovere
[UnscopedRef]
da un parametroref
di tiporef struct
Qualsiasi altra differenza rispetto a scoped
o [UnscopedRef]
è considerata una mancata corrispondenza.
La mancata corrispondenza viene segnalata come errore se le firme non corrispondenti usano entrambe le regole del contesto sicuro di riferimento C#11; in caso contrario, la diagnostica è un avviso .
L'avviso di mancata corrispondenza con ambito può essere segnalato in un modulo compilato con le regole del contesto sicuro di riferimento C#7.2 in cui scoped
non è disponibile. In alcuni casi, potrebbe essere necessario eliminare l'avviso se non è possibile modificare l'altra firma non corrispondente.
Il modificatore scoped
e l'attributo [UnscopedRef]
hanno anche gli effetti seguenti sulle firme dei metodi:
- Il modificatore
scoped
e l'attributo[UnscopedRef]
non influiscono sul nascondimento - Gli sovraccarichi non possono differire solo per
scoped
o[UnscopedRef]
La sezione relativa al campo ref
e scoped
è molto lunga, quindi si vuole chiudere con un breve riepilogo delle modifiche di rilievo proposte:
- Un valore che ha ref-safe-context per il caller-context può essere restituito dal campo
ref
oref
. - Un parametro
avrà un di contesto sicuro di membro della funzione.
Note dettagliate:
- Un campo
ref
può essere dichiarato solo all'interno di unref struct
- Un campo
ref
non può essere dichiaratostatic
,volatile
oconst
- Un campo
ref
non può avere un tiporef struct
- Il processo di generazione dell'assembly di riferimento deve mantenere la presenza di un campo
ref
all'interno di unref struct
- Un
readonly ref struct
deve dichiarare i campiref
comereadonly ref
- Per i valori per riferimento, il modificatore
scoped
deve apparire prima diin
,out
oref
. - Il documento sulle regole di sicurezza del ponte verrà aggiornato come descritto in questo documento
- Le nuove regole del contesto sicuro di riferimento saranno effettive quando
- La libreria principale contiene il flag di funzionalità che indica il supporto per i campi di
ref
- Il valore
langversion
è 11 o superiore
- La libreria principale contiene il flag di funzionalità che indica il supporto per i campi di
Sintassi
13.6.2 dichiarazioni di variabili locali: aggiunta 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 La dichiarazione for
: aggiunta 'scoped'?
indirettamente da local_variable_declaration
.
13.9.5 L'istruzione foreach
: aggiunta di 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Elenchi di argomenti: aggiunta di 'scoped'?
per out
variabile di dichiarazione.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
12.7 Espressioni di decostruzione:
[TBD]
15.6.2 Parametri del metodo: aggiunta di 'scoped'?
a 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 Dichiarazioni delegate: aggiunta 'scoped'?
indirettamente da fixed_parameter
.
12.19 Espressioni di funzione anonime: aggiunta 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Tipi con restrizioni Sunset
Il compilatore ha il concetto di un insieme di "tipi con restrizioni" che è in gran parte non documentato. A questi tipi è stato assegnato uno stato speciale perché in C# 1.0 non esiste un modo generico per esprimere il proprio comportamento. In particolare, il fatto che i tipi possano contenere riferimenti allo stack di esecuzione. Invece, il compilatore aveva una conoscenza speciale di essi e ne limitava l'uso a modi che sarebbero sempre sicuri: i ritorni non sono consentiti, non possono essere utilizzati come elementi di array, non possono essere usati nei tipi generici, ecc...
Quando i campi ref
sono disponibili ed estesi per supportare ref struct
, questi tipi possono essere definiti correttamente in C# usando una combinazione di campi ref struct
e ref
. Pertanto, quando il compilatore rileva che un runtime supporta ref
campi non avrà più una nozione di tipi con restrizioni. Userà invece i tipi definiti nel codice.
Per supportare questo, le nostre regole del contesto sicuro di riferimento verranno aggiornate come segue:
-
__makeref
verrà considerato come metodo con la firmastatic TypedReference __makeref<T>(ref T value)
-
__refvalue
verrà considerato come un metodo con la firmastatic ref T __refvalue<T>(TypedReference tr)
. L'espressione__refvalue(tr, int)
userà effettivamente il secondo argomento come parametro di tipo. come parametro avrà un di contesto e contesto sicuro di membro della funzione .-
__arglist(...)
come espressione avrà un contesto ref-safe e un contesto sicuro del membro della funzione .
I runtime conformi garantiranno che TypedReference
, RuntimeArgumentHandle
e ArgIterator
siano definiti come ref struct
. Inoltre, TypedReference
deve essere considerato come un campo ref
per un ref struct
per ogni tipo possibile (può archiviare qualsiasi valore). Ciò combinato con le regole precedenti garantisce che i riferimenti allo stack non superino la loro durata.
Nota: in senso stretto si tratta di un dettaglio dell'implementazione del compilatore rispetto a una parte del linguaggio. Tuttavia, viene inclusa nella proposta linguistica, data la relazione con i campi ref
, per ragioni di semplicità.
Fornire non specificato
Uno dei punti di attrito più importanti è l'impossibilità di restituire campi tramite ref
nei membri istanza di un struct
. Ciò significa che gli sviluppatori non possono creare metodi/proprietà che restituiscono ref
e devono ricorrere a esporre direttamente i campi. Ciò riduce l'utilità delle restituzioni di ref
in struct
, dove è spesso la più desiderata.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
Il razionale per questa impostazione predefinita è ragionevole, ma non c'è nulla di intrinsecamente sbagliato con un struct
di escape this
per riferimento, è semplicemente l'impostazione predefinita scelta dalle regole del contesto sicuro ref.
Per risolvere questo problema, la lingua fornirà l'opposto dell'annotazione della durata scoped
supportando un UnscopedRefAttribute
. Questa operazione può essere applicata a qualsiasi ref
e modificherà il contesto sicuro di riferimento affinché sia di un livello più ampio del suo valore predefinito. Per esempio:
Applicato a UnscopedRef | Originale contesto di sicurezza | Nuovo contesto sicuro di riferimento |
---|---|---|
membro dell'istanza | membro di funzione | solo ritorno |
parametro in / ref |
solo ritorno | contesto del chiamante |
parametro out |
membro di funzione | solo ritorno |
Quando si applica [UnscopedRef]
a un metodo di istanza di un struct
, esso ha l'effetto di modificare il parametro implicito this
. Ciò significa che this
funge da ref
non annotato dello stesso tipo.
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;
}
L'annotazione può anche essere inserita sui parametri out
per ripristinare il comportamento di C# 10.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Ai fini delle regole di contesto sicuro di riferimento, un [UnscopedRef] out
viene considerato semplicemente come un ref
. Analogamente al modo in cui in
viene considerato ref
per finalità di durata.
L'annotazione [UnscopedRef]
non sarà consentita nei membri e nei costruttori init
all'interno di struct
. Tali membri sono già speciali per quanto riguarda la semantica ref
poiché considerano i membri readonly
come modificabili. Ciò significa che portare ref
a quei membri appare come un semplice ref
, non ref readonly
. Ciò è consentito entro i confini dei costruttori e init
. Consentire [UnscopedRef]
permetterebbe a tale ref
di sfuggire erroneamente all'esterno del costruttore e consentirebbe la mutazione dopo che la semantica di readonly
ha avuto luogo.
Il tipo di attributo avrà la definizione seguente:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Note dettagliate:
- Un metodo di istanza o una proprietà annotata con
[UnscopedRef]
ha di contesto ref-safe dithis
impostato sul contesto del chiamante . - Un membro annotato con
[UnscopedRef]
non può implementare un'interfaccia. - È un errore usare
[UnscopedRef]
in- Membro non dichiarato in un
struct
- Membro
static
, membroinit
o costruttore su unstruct
- Parametro contrassegnato
scoped
- Parametro passato per valore
- Parametro che viene passato per riferimento senza un ambito implicito
- Membro non dichiarato in un
ScopedRefAttribute
Le annotazioni scoped
verranno emesse nei metadati tramite l'attributo di tipo System.Runtime.CompilerServices.ScopedRefAttribute
. L'attributo verrà confrontato con il nome completo dello spazio dei nomi in modo che la definizione non debba essere visualizzata in un assembly specifico.
Il tipo ScopedRefAttribute
è destinato solo all'uso del compilatore. Non è consentito nell'origine. La dichiarazione di tipo viene sintetizzata dal compilatore, se non è già inclusa nella compilazione.
Il tipo avrà la definizione seguente:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Il compilatore genererà questo attributo del parametro con la sintassi scoped
. Verrà generato solo quando la sintassi fa sì che il valore sia diverso dal relativo stato predefinito. Ad esempio, scoped out
non causerà l'emissione di alcun attributo.
RefSafetyRulesAttribute
Esistono diverse differenze nelle regole del contesto sicuro di riferimento tra C#7.2 e C#11. Una di queste differenze può comportare cambiamenti critici durante la ricompilazione con C#11 contro i riferimenti compilati con C#10 o versioni precedenti.
- I parametri
ref
/in
/out
non limitati possono sfuggire a una chiamata di metodo come camporef
di unref struct
in C#11, non in C#7.2. - I parametri
out
sono limitati implicitamente in C#11 e non limitati in C#7.2. -
ref
/in
parametri perref struct
tipi sono implicitamente limitati in C#11 e non limitati in C#7.2.
Per ridurre la probabilità di modifiche di rilievo durante la ricompilazione con C#11, il compilatore C#11 verrà aggiornato in modo da usare le regole del contesto sicuro ref per la chiamata al metodo che corrispondono alle regole usate per analizzare la dichiarazione del metodo. Essenzialmente, quando si analizza una chiamata a un metodo compilato con un compilatore precedente, il compilatore C#11 userà le regole del contesto sicuro di riferimento C#7.2.
Per abilitare questa operazione, il compilatore genererà un nuovo attributo [module: RefSafetyRules(11)]
quando il modulo è compilato con -langversion:11
o versione superiore, oppure è compilato con un corlib contenente il flag di funzionalità per i campi ref
.
L'argomento dell'attributo indica la versione della lingua delle regole del contesto di sicurezza di riferimento usate durante la compilazione del modulo.
La versione è attualmente corretta in 11
indipendentemente dalla versione effettiva del linguaggio passata al compilatore.
L'aspettativa è che le versioni future del compilatore aggiorneranno le regole del contesto sicuro ref e genereranno attributi con versioni distinte.
Se il compilatore carica un modulo che include un [module: RefSafetyRules(version)]
con un version
diverso da 11
, il compilatore segnalerà un avviso per la versione non riconosciuta se sono presenti chiamate a metodi dichiarati in tale modulo.
Quando il compilatore C#11 analizza una chiamata al metodo:
- Se il modulo contenente la dichiarazione del metodo include
[module: RefSafetyRules(version)]
, indipendentemente daversion
, la chiamata al metodo viene analizzata con le regole C#11. - Se il modulo contenente la dichiarazione del metodo proviene dall'origine e compilato con
-langversion:11
o con un corlib contenente il flag di funzionalità per i campiref
, la chiamata al metodo viene analizzata con le regole C#11. - Se il modulo contenente la dichiarazione del metodo fa riferimento
System.Runtime { ver: 7.0 }
, la chiamata al metodo viene analizzata con le regole C#11. Questa regola è una mitigazione temporanea per i moduli compilati con anteprime precedenti di C#11 / .NET 7 e verranno rimosse in un secondo momento. - In caso contrario, la chiamata al metodo viene analizzata con le regole C#7.2.
Un compilatore pre-C#11 ignorerà qualsiasi RefSafetyRulesAttribute
e analizzerà solo le chiamate ai metodi con regole C#7.2.
L'elemento RefSafetyRulesAttribute
sarà abbinato al nome qualificato dallo spazio dei nomi, così la definizione non deve essere presente in un assembly specifico.
Il tipo RefSafetyRulesAttribute
è destinato solo all'uso del compilatore. Non è consentito nell'origine. La dichiarazione di tipo viene sintetizzata dal compilatore, se non è già inclusa nella compilazione.
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;
}
}
Buffer a dimensione fissa sicura
I buffer a dimensione fissa sicuri non sono stati introdotti in C# 11. Questa funzionalità può essere implementata in una versione futura di C#.
Il linguaggio riduce le restrizioni sulle matrici a dimensione fissa in modo che possano essere dichiarate nel codice sicuro e il tipo di elemento può essere gestito o non gestito. Questo permetterà che tipi come i seguenti siano validi:
internal struct CharBuffer
{
internal char Data[128];
}
Queste dichiarazioni, analogamente alle loro controparti unsafe
, definiranno una sequenza di elementi N
nel tipo contenente. Questi membri possono essere accessibili con un indicizzatore e possono anche essere convertiti in istanze di Span<T>
e ReadOnlySpan<T>
.
Quando si esegue l'indicizzazione in un buffer di fixed
di tipo T
, bisogna tenere in considerazione lo stato readonly
del contenitore. Se il contenitore è readonly
, l'indicizzatore restituisce ref readonly T
altrimenti restituisce ref T
.
L'accesso a un buffer fixed
senza un indicizzatore non ha alcun tipo naturale, ma è convertibile in tipi Span<T>
. Nel caso in cui il contenitore sia readonly
, il buffer è convertibile in modo implicito in ReadOnlySpan<T>
, oppure può eseguire la conversione implicita in Span<T>
o ReadOnlySpan<T>
(la conversione Span<T>
viene considerata migliore).
L'istanza di Span<T>
risultante avrà una lunghezza uguale alla dimensione dichiarata nel buffer fixed
. Il contesto sicuro del valore restituito sarà uguale al contesto sicuro del contenitore, proprio come se vi si accedesse come campo ai dati di base.
Per ogni dichiarazione di fixed
in un tipo in cui il tipo di elemento è T
, il linguaggio genererà un metodo indicizzatore corrispondente a get
, il cui tipo restituito è solo ref T
. L'indicizzatore verrà annotato con l'attributo [UnscopedRef]
perché l'implementazione restituirà i campi del tipo che dichiara. L'accessibilità del membro corrisponderà all'accessibilità nel campo fixed
.
Ad esempio, la firma dell'indicizzatore per CharBuffer.Data
sarà la seguente:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Se l'indice specificato non rientra nei limiti dichiarati della matrice fixed
, verrà generata una IndexOutOfRangeException
. Nel caso in cui venga specificato un valore costante, verrà sostituito con un riferimento diretto all'elemento appropriato. A meno che la costante non si trova all'esterno dei limiti dichiarati nel qual caso si verificherà un errore di fase di compilazione.
Verrà inoltre generato un accessor denominato per ogni buffer di fixed
che fornisce operazioni get
e set
per valore. Questo significa che i buffer fixed
saranno più simili alla semantica di matrice esistente con un accessor ref
, oltre a operazioni byval get
e set
. Ciò significa che i compilatori avranno la stessa flessibilità nell'emissione di codice che utilizza i buffer fixed
come avviene con gli array. Dovrebbe rendere più facili da emettere operazioni come await
su buffer di fixed
.
Questo ha anche il vantaggio aggiunto di rendere i buffer fixed
più facili da usare da altre lingue. Gli indicizzatori denominati sono una funzionalità esistente dalla versione 1.0 di .NET. Anche i linguaggi che non possono generare direttamente un indicizzatore denominato possono in genere usarli (C# è in realtà un buon esempio di questo).
L'archiviazione di backup per il buffer verrà generata usando l'attributo [InlineArray]
. Si tratta di un meccanismo descritto nella questione 12320 che consente specificamente di dichiarare in modo efficiente una sequenza di campi dello stesso tipo. Questo particolare problema è ancora soggetto a una discussione attiva e l'aspettativa è che l'implementazione di questa funzionalità seguirà però tale discussione.
Inizializzatori con valori ref
nelle espressioni new
e with
Nella sezione 12.8.17.3 Inizializzatori di oggetti, aggiorniamo la grammatica in:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
Nella sezione relativa all'espressione with
, aggiorniamo la grammatica in:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
L'operando sinistro dell'assegnazione deve essere un'espressione che viene associata a un campo di riferimento.
L'operando destro deve essere un'espressione che restituisce un lvalue che designa un valore dello stesso tipo dell'operando sinistro.
Si aggiunge una regola simile a riassegnazione locale ref:
Se l'operando sinistro è un riferimento scrivibile (ovvero designa qualsiasi elemento diverso da un campo ref readonly
), l'operando destro deve essere un lvalue scrivibile.
Le regole di escape per le invocazioni del costruttore rimangono:
Un'espressione
new
che richiama un costruttore rispetta le stesse regole di un'invocazione di metodo che viene considerata come restituente il tipo costruito.
Cioè, le regole di invocazione del metodo , aggiornate sopra:
Un rvalue risultante da una chiamata al metodo
e1.M(e2, ...)
ha contesto sicuro dal più piccolo dei contesti seguenti:
- contesto del chiamante
- contesto sicuro fornito da tutte le espressioni dei parametri
- Quando il valore restituito è un
ref struct
, contesto di riferimento fornito da tutti gli argomentiref
Per un'espressione new
con inizializzatori, le espressioni degli inizializzatori vengono conteggiate come argomenti (contribuiscono il loro contesto sicuro) e le espressioni degli inizializzatori ref
vengono conteggiate come argomenti ref
(contribuiscono il loro contesto di riferimento sicuro), in modo ricorsivo.
Modifiche nel contesto non sicuro
I tipi di puntatore (sezione 23.3) vengono estesi per consentire i tipi gestiti come tipo referenziale.
Tali tipi di puntatore vengono scritti come tipo gestito seguito da un token *
. Generano un avviso.
L'operatore address-of (sezione 23.6.5) è stato modificato per permettere di usare una variabile con un tipo gestito come operando.
L'istruzione fixed
(sezione 23.7) è stata allentata per accettare fixed_pointer_initializer che è l'indirizzo di una variabile di tipo gestito T
o che è un'espressione di un array_type con elementi di un tipo gestito T
.
L'inizializzatore di allocazione dello stack (sezione 12.8.22) è stato ugualmente allentato.
Considerazioni
Quando si valuta questa funzionalità, è consigliabile prendere in considerazione altre parti dello stack di sviluppo.
Considerazioni sulla compatibilità
La sfida in questa proposta è rappresentata dalle implicazioni di compatibilità che questo progetto ha per le nostre regole di sicurezza esistenti , oppure §9.7.2. Anche se queste regole supportano completamente il concetto di un ref struct
con ref
campi, non consentono alle API, diverse da stackalloc
, di acquisire ref
stato che fa riferimento allo stack. Le regole del contesto sicuro di riferimento hanno un presupposto difficileo §16.4.12.8 che un costruttore del modulo Span(ref T value)
non esiste. Ciò significa che le regole di sicurezza non considerano un parametro ref
in grado di sfuggire come campo ref
, pertanto consente codici come il seguente.
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);
}
Esistono in effetti tre modi per un parametro ref
per sfuggire da un'invocazione di metodo.
- Restituzione per valore
- Per restituire
ref
- Per
ref
campo inref struct
restituito o passato come parametroref
/out
Le regole esistenti considerano solo (1) e (2). Non considerano (3) quindi le lacune, ad esempio la restituzione di variabili locali come i campi ref
non sono considerati. Questa progettazione deve modificare le regole in modo da tenere conto di (3). Questo avrà un impatto ridotto sulla compatibilità per le API esistenti. In particolare, influirà sulle API con le proprietà seguenti.
- Avere un
ref struct
nella firma- Dove il
ref struct
è un tipo di ritorno, eref
oout
sono parametri - Include un parametro aggiuntivo
in
oref
, escludendo il ricevitore.
- Dove il
In C# 10 i chiamanti di tali API non hanno mai dovuto considerare che lo stato di input ref
all'API potrebbe essere acquisito come un campo ref
. Ciò ha consentito l'esistenza di diversi modelli, in modo sicuro con C# 10, che saranno insicuri in C# 11 a causa della possibilità per lo stato ref
di sfuggire come un campo ref
. Per esempio:
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]);
}
L'impatto di questa interruzione di compatibilità dovrebbe essere molto ridotto. La forma dell'API interessata ha poco senso in assenza di campi ref
di conseguenza è improbabile che i clienti abbiano creato molti di questi campi. Gli esperimenti che utilizzano strumenti per rilevare questa forma di API nei repository esistenti supportano quell'affermazione. L'unico repository che abbia un conteggio significativo di questa forma è dotnet/runtime poiché quel repository può creare campi ref
tramite il tipo intrinseco ByReference<T>
.
Anche in questo modo la progettazione deve tenere conto di tali API esistenti perché esprime un modello valido, non solo uno comune. Di conseguenza, la progettazione deve offrire agli sviluppatori gli strumenti per ripristinare le regole di durata esistenti durante l'aggiornamento a C# 10. In particolare, deve fornire meccanismi che consentano agli sviluppatori di annotare i parametri ref
come incapaci di sfuggire da entrambi i campi ref
o ref
. Ciò consente ai clienti di definire API in C# 11 che rispettano le stesse regole di chiamata di C# 10.
Assembly di riferimento
Un'assembly di riferimento per una compilazione che utilizza le caratteristiche descritte in questa proposta deve mantenere gli elementi che forniscono informazioni sul contesto sicuro di riferimento. Ciò significa che tutti gli attributi di annotazione relativi al tempo di vita devono essere mantenuti nella loro posizione originale. Qualsiasi tentativo di sostituire o ometterli potrebbe portare alla creazione di assembly di riferimento non validi.
La rappresentazione dei campi ref
è più sfumata. Idealmente, un campo ref
verrebbe visualizzato in un assembly di riferimento come qualsiasi altro campo. Tuttavia, un campo ref
rappresenta una modifica al formato dei metadati e che può causare problemi con le catene di strumenti che non vengono aggiornate per comprendere questa modifica dei metadati. Un esempio concreto è C++/CLI che probabilmente genererà un errore se utilizza un campo ref
. Di conseguenza, è vantaggioso se i campi ref
possono essere omessi negli assembly di riferimento delle nostre librerie principali.
Un campo ref
da solo non ha alcun impatto sulle regole del contesto sicuro ref. Come esempio concreto, si consideri che modificare la definizione esistente di Span<T>
per utilizzare un campo ref
non ha alcun impatto sull'utilizzo. Di conseguenza, il ref
stesso può essere omesso in modo sicuro. Tuttavia, un campo ref
ha altri effetti sul consumo che devono essere preservati.
- Un
ref struct
con un camporef
non viene mai consideratounmanaged
- Il tipo del campo
ref
influisce sulle regole di espansione generica infinite. Di conseguenza, se il tipo di un camporef
contiene un parametro di tipo che deve essere mantenuto
Date queste regole, ecco una trasformazione di riferimento valida per un assembly 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
}
Annotazioni
Le durate vengono espresse in modo più naturale usando i tipi. Le durate di vita di un determinato programma sono sicure quando i tipi di durata di vita verificano il tipo. Sebbene la sintassi di C# aggiunga in modo implicito la durata ai valori, è presente un sistema di tipi sottostante che descrive le regole fondamentali qui. Spesso è più facile discutere l'implicazione delle modifiche apportate alla progettazione in termini di queste regole, in modo che siano incluse qui per motivi di discussione.
Si noti che questo non intende essere una documentazione completa al 100%. La documentazione di ogni singolo comportamento non è un obiettivo qui. È invece pensato per stabilire una comprensione generale e un linguaggio comune in base al quale il modello, e le potenziali modifiche, possono essere discusso.
In genere non è necessario parlare direttamente dei tipi di durata. Le eccezioni sono luoghi in cui le durate possono variare in base a determinati siti di "istanza". Si tratta di un tipo di polimorfismo e chiamiamo queste durate variabili "durate generiche", rappresentate come parametri generici. C# non fornisce la sintassi per esprimere i generics a tempo di vita, quindi definiamo una "traduzione" implicita da C# a un linguaggio semplificato espanso che contiene parametri generici espliciti.
Gli esempi seguenti usano le durate denominate. La sintassi $a
fa riferimento a una durata denominata a
. È una vita che non ha alcun significato di per sé, ma può essere messo in relazione con altre durate tramite la sintassi where $a : $b
. Ciò stabilisce che $a
è convertibile in $b
. Può essere utile pensare a questo come stabilire che $a
è un periodo di tempo che dura almeno quanto $b
.
Di seguito sono riportate alcune durate predefinite per praticità e brevità:
-
$heap
: questa è la durata di qualsiasi valore presente nell'heap. È disponibile in tutti i contesti e le signature dei metodi. -
$local
: questa è la durata di qualsiasi valore esistente nello stack di metodi. È in effetti un segnaposto per il nome per la funzione-membro . Viene definito in modo implicito nei metodi e può essere incluso nelle firme del metodo, ad eccezione di qualsiasi posizione di uscita. -
$ro
: segnaposto del nome per restituire solo -
$cm
: segnaposto del nome per contesto del chiamante
Esistono alcune relazioni predefinite tra i tempi di vita:
-
where $heap : $a
per tutte le durate di vita$a
where $cm : $ro
-
where $x : $local
per tutti i cicli di vita predefiniti. Le durate definite dall'utente non hanno alcuna relazione con quella locale, a meno che non venga definita in modo esplicito.
Le variabili di durata quando definite nei tipi possono essere invarianti o covarianti. Questi sono espressi usando la stessa sintassi dei parametri generici:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Il parametro di durata $this
nelle definizioni dei tipi è non predefinito, ma include alcune regole associate al parametro quando viene definito:
- Deve essere il primo parametro di durata.
- Deve essere covariante:
out $this
. - La durata di vita dei campi di
ref
deve essere convertibile in$this
- La durata
$this
di tutti i campi non di riferimento deve essere$heap
o$this
.
La durata di un ref viene espressa fornendo un argomento di durata al ref. Ad esempio, un ref
che si riferisce all'heap viene espresso come ref<$heap>
.
Quando si definisce un costruttore nel modello, verrà usato il nome new
per il metodo . È necessario avere un elenco di parametri per il valore restituito e gli argomenti del costruttore. Ciò è necessario per esprimere la relazione tra gli input del costruttore e il valore costruito. Invece di avere Span<$a><$ro>
il modello userà Span<$a> new<$ro>
. Il tipo di this
nel costruttore, incluse le durate, sarà il valore restituito definito.
Le regole di base per la durata della vita si definiscono come:
- Tutte le durate vengono espresse sintatticamente come argomenti generici, che precedono gli argomenti di tipo. Questo vale per le durate predefinite, ad eccezione di
$heap
e$local
. - Tutti i tipi
T
che non sonoref struct
hanno una durata implicita diT<$heap>
. Questo è implicito, non è necessario scrivereint<$heap>
in ogni esempio. - Per un campo
ref
definito comeref<$l0> T<$l1, $l2, ... $ln>
:- Tutte le durate di vita da
$l1
a$ln
devono essere invarianti. - La durata di
$l0
deve essere convertibile in$this
- Tutte le durate di vita da
- Per un
ref
definito comeref<$a> T<$b, ...>
,$b
deve essere convertibile in$a
- Il
ref
di una variabile ha una durata definita da:- Per un
ref
locale, un parametro, un campo o un ritorno di tiporef<$a> T
, la durata è$a
-
$heap
per tutti i tipi di riferimento e i campi dei tipi di riferimento -
$local
per tutto il resto
- Per un
- Un'assegnazione o una restituzione è legale quando la conversione del tipo sottostante è legale
- È possibile rendere espliciti i tempi di vita delle espressioni usando le annotazioni cast.
-
(T<$a> expr)
la durata del valore viene$a
in modo esplicito perT<...>
-
ref<$a> (T<$b>)expr
la durata del valore è$b
perT<...>
e la durata del riferimento è$a
.
-
Ai fini delle regole di durata, per scopi di conversione, un ref
è considerato parte integrante del tipo di espressione. È rappresentato logicamente convertendo ref<$a> T<...>
in ref<$a, T<...>>
dove $a
è covariante e T
è invariante.
Definire quindi le regole che consentono di eseguire il mapping della sintassi C# al modello sottostante.
Per brevità, un tipo che non ha parametri di durata espliciti è trattato come se fosse definito out $this
e applicato a tutti i campi del tipo. Un tipo con un campo ref
deve definire parametri di durata espliciti.
Queste regole esistono per supportare l'invariante esistente che T
può essere assegnata a scoped T
per tutti i tipi. Che si riduce a T<$a, ...>
può essere assegnato a T<$local, ...>
per tutte le durate note che possono essere convertite in $local
. Questo supporta altri elementi, come assegnare Span<T>
dall'heap agli elementi nello stack. Ciò esclude i tipi in cui i campi hanno durate diverse per i valori non ref, ma questa è la realtà di C#. Modificare ciò richiederebbe un cambiamento significativo delle regole di C# che dovrebbero essere pianificate.
Il tipo di this
per un tipo S<out $this, ...>
all'interno di un metodo di istanza viene definito in modo implicito come segue:
- Per il metodo di istanza normale:
ref<$local> S<$cm, ...>
- Per il metodo di istanza annotato con
[UnscopedRef]
:ref<$ro> S<$cm, ...>
La mancanza di un parametro this
esplicito forza qui le regole implicite. Per esempi complessi e discussioni è consigliabile scrivere come metodo static
e rendere this
un parametro esplicito.
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) { }
}
La sintassi del metodo C# corrisponde al modello nei modi seguenti:
- I
ref
parametri hanno una durata di riferimento di$ro
- i parametri di tipo
ref struct
hanno una durata di$cm
- I valori restituiti ref hanno una durata di riferimento di
$ro
- resi di tipo
ref struct
hanno una durata del valore di$ro
-
scoped
in un parametro oref
modifica la durata dei riferimenti in modo che sia$local
Consideriamo un semplice esempio che illustra il modello qui:
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;
}
Esploriamo ora lo stesso esempio usando un 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;
}
Adesso vediamo come questo può aiutare con il problema di auto-assegnazione ciclica.
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;
}
}
Vediamo ora come questo aiuta a risolvere il problema del parametro di acquisizione stupido:
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;
}
}
Problemi aperti
Modificare la progettazione per evitare interruzioni di compatibilità
Questo design propone diverse rotture di compatibilità con le regole ref-safe-context esistenti. Nonostante si ritenga che le modifiche abbiano un impatto minimo, è stata data notevole attenzione a una progettazione che non comportasse cambiamenti significativi.
Il design che preserva la compatibilità era però significativamente più complesso di questo. Per preservare la compatibilità, i campi ref
necessitano di durate distinte per la possibilità di restituire tramite il campo ref
e il campo ref
. Essenzialmente è necessario fornire il monitoraggio del contesto di riferimento sicuro per e per tutti i parametri di un metodo. Questa operazione deve essere calcolata per tutte le espressioni e tracciati in tutti i valori praticamente ovunque il contesto ref-safe venga monitorato oggi.
Inoltre, questo valore ha relazioni con contesto sicuro di riferimento. Ad esempio, non ha senso avere un valore che può essere restituito come campo ref
, ma non direttamente come ref
. Ciò è dovuto al fatto che i campi ref
possono essere già restituiti in modo semplice da ref
(lo statoref
in un ref struct
può essere restituito da ref
anche quando il valore che li contiene non può esserlo). Di conseguenza, le regole necessitano di un ulteriore adeguamento costante per garantire che questi valori siano ragionevoli rispetto l'uno all'altro.
Significa anche che il linguaggio necessita di una sintassi per rappresentare i parametri ref
che possono essere restituiti in tre modi diversi: dal campo ref
, dal ref
e per valore. Valore predefinito che può essere restituito da ref
. In futuro, anche se il ritorno più naturale, in particolare quando ref struct
sono coinvolti, è previsto dal campo ref
o ref
. Ciò significa che le nuove API richiedono un'annotazione sintattica aggiuntiva per essere corrette di default. Questo è indesiderato.
Queste modifiche relative alla compatibilità influenzeranno i metodi che hanno le seguenti proprietà:
- Scegli un
Span<T>
o unref struct
- Dove il
ref struct
è un tipo di ritorno, eref
oout
sono parametri - Ha un parametro aggiuntivo
in
oref
(escluso il ricevitore)
- Dove il
Per comprendere l'impatto, è utile suddividere le API in categorie:
- Si vuole che i consumatori tengano conto di
ref
venga acquisito come campo diref
. Un esempio lampante sono i costruttoriSpan(ref T value)
- Non si vuole che i consumatori tengano conto del fatto che
ref
sia acquisito come camporef
. Questi però si suddividono in due categorie- API non sicure. Si tratta di API all'interno dei tipi di
Unsafe
eMemoryMarshal
, di cuiMemoryMarshal.CreateSpan
è il più importante. Queste API acquisiscono ilref
in modo non sicuro, ma sono note anche come API non sicure. - Sicure API Si tratta di API che accettano parametri
ref
per l'efficienza, ma non vengono effettivamente registrati da nessuna parte. Gli esempi sono piccoli, ma uno èAsnDecoder.ReadEnumeratedBytes
- API non sicure. Si tratta di API all'interno dei tipi di
Questa modifica offre principalmente vantaggi (1) sopra. Si prevede che queste funzioni costituiscono la maggior parte delle API che accettano un ref
e restituiscono un ref struct
in futuro. Le modifiche influiscono negativamente su (2.1) e (2.2) poiché interrompono la semantica di chiamata esistente, in quanto le regole di durata cambiano.
Le API nella categoria (2.1) sono in gran parte create da Microsoft o dagli sviluppatori che traggono maggior vantaggio dai campi ref
(come i Tanner del mondo). È ragionevole presupporre che questa classe di sviluppatori possa accettare un'imposta di compatibilità sull'aggiornamento a C# 11 sotto forma di alcune annotazioni per mantenere la semantica esistente, se in cambio venissero forniti i campi ref
.
Le API nella categoria (2.2) sono il problema principale. È sconosciuto il numero di API esistenti e non è chiaro se questi siano più o meno frequenti nel codice di terze parti. L'aspettativa è che vi sia un numero molto ridotto di loro, in particolare se prendiamo la pausa di compatibilità su out
. Le ricerche finora hanno rivelato un numero molto ridotto di questi esistenti nell'area di superficie public
. Si tratta di un modello difficile da cercare anche se richiede l'analisi semantica. Prima di adottare questo approccio basato su uno strumento, sarebbe necessario verificare i presupposti relativi a questo impatto su un numero ridotto di casi noti.
Per entrambi i casi nella categoria (2), tuttavia, la correzione è semplice. I parametri di ref
che non devono essere considerati acquisiscibili devono aggiungere scoped
all'ref
. In (2.1), è probabile che anche lo sviluppatore debba utilizzare Unsafe
o MemoryMarshal
, il che è previsto per le API con stili non sicuri.
Idealmente, il linguaggio potrebbe ridurre l'impatto delle modifiche di rilievo invisibile all'utente inviando un avviso quando un'API rientra automaticamente nel comportamento problematico. Si tratta di un metodo che accetta un ref
, e restituisce ref struct
, ma non cattura effettivamente il ref
nella ref struct
. Il compilatore potrebbe emettere un messaggio diagnostico in questo caso per informare gli sviluppatori che tale ref
dovrebbe essere annotato come scoped ref
.
Decisione Questa progettazione può essere ottenuta, ma la caratteristica risultante è più difficile da usare, al punto da accettare una rottura della compatibilità.
Decisione Il compilatore fornirà un avviso quando un metodo soddisfa i criteri, ma non acquisisce il parametro ref
come campo ref
. Questo dovrebbe avvisare in modo adeguato i clienti, durante l'aggiornamento, sui potenziali problemi che potrebbero creare.
Parole chiave e attributi
Questa progettazione richiede l'uso degli attributi per annotare le nuove regole di durata. Questo potrebbe anche essere stato fatto altrettanto facilmente con le parole chiave contestuali. Ad esempio, [DoesNotEscape]
potrebbe essere mappato su scoped
. Tuttavia, le parole chiave, anche quelle contestuali, in genere devono soddisfare una barra molto alta per l'inclusione. Occupano uno spazio linguistico prezioso e sono parti più visibili della lingua. Questa funzionalità, pur preziosa, servirà a una minoranza di sviluppatori C#.
A prima vista, sembrerebbe favorire il non uso di parole chiave, ma ci sono due punti importanti da considerare:
- Le annotazioni avranno effetto sulla semantica del programma. Avere attributi che influiscono sulla semantica del programma è una linea che C# è riluttante ad attraversare ed è poco chiaro se questa sia la funzionalità che dovrebbe giustificare che il linguaggio compia tale passo.
- Gli sviluppatori più propensi a usare questa funzionalità si sovrappongono fortemente con l'insieme di sviluppatori che usano i puntatori a funzione. Tale caratteristica, anche se usata anche da una minoranza di sviluppatori, ha garantito una nuova sintassi e tale decisione è ancora considerata solida.
Questo significa che la sintassi deve essere presa in considerazione.
Uno schizzo approssimativo della sintassi sarebbe:
-
[RefDoesNotEscape]
mappa suscoped ref
-
[DoesNotEscape]
mappa suscoped
-
[RefDoesEscape]
mappa suunscoped
Decisione Usa la sintassi per scoped
e scoped ref
; usa l'attributo per unscoped
.
Consenti variabili locali del buffer fisso
Questo design consente buffer fixed
sicuri in grado di supportare qualsiasi tipo. Una possibile estensione in questo caso consente di dichiarare tali buffer fixed
come variabili locali. Ciò consentirebbe la sostituzione di una serie di operazioni di stackalloc
esistenti con un buffer fixed
. Espanderebbe anche il set di scenari in cui si potrebbero avere allocazioni di tipo stack, dato che stackalloc
è limitato ai tipi di dati non gestiti, mentre i buffer fixed
non lo sono.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Questo è coerente, ma ci richiede di estendere un po' la sintassi per le variabili locali. Non chiaro se questo è o non vale la pena la complessità aggiuntiva. È possibile decidere di no per il momento e riportare indietro in un secondo momento, se è stata dimostrata una necessità sufficiente.
Esempio di dove sarebbe utile: https://github.com/dotnet/runtime/pull/34149
Decisone rimandare per il momento
Per usare o meno modreqs
È necessario prendere una decisione se i metodi contrassegnati con nuovi attributi di durata devono o non devono essere convertiti in modreq
in emit. Esisterebbe effettivamente una corrispondenza 1:1 tra annotazioni e modreq
se fosse stato adottato questo approccio.
La logica per l'aggiunta di un modreq
è che gli attributi modificano la semantica delle regole del contesto sicuro di riferimento. Solo le lingue che comprendono queste semantiche devono chiamare i metodi in questione. Inoltre, se applicato agli scenari OHI, le durate di vita diventano un contratto che tutti i metodi derivati devono implementare. L'esistenza delle annotazioni senza modreq
può causare situazioni in cui catene di metodi con annotazioni di durata in conflitto vengono caricate da virtual
(può verificarsi se viene compilata solo una parte della catena di virtual
e l'altra no).
Il lavoro iniziale sul contesto di riferimento sicuro non ha utilizzato modreq
, ma si è invece basato sui linguaggi e sul framework per comprendere. Allo stesso tempo, tutti gli elementi che contribuiscono alle regole di contesto sicure di riferimento sono un elemento fondamentale della firma del metodo: ref
, in
, ref struct
e così via... Pertanto, qualsiasi modifica alle regole esistenti di un metodo comporta in ogni caso una modifica binaria alla firma. Per dare alle nuove annotazioni a vita lo stesso impatto, avranno bisogno dell'applicazione modreq
.
La preoccupazione è se questo è o meno eccessivo. C'è l'impatto negativo che il fatto di rendere le firme più flessibili, ad esempio con l'aggiunta di [DoesNotEscape]
a un parametro, comporterà una modifica della compatibilità binaria. Questo compromesso significa che nel corso del tempo framework come BCL probabilmente non saranno in grado di modificare tali firme. Potrebbe essere mitigato in una certa misura adottando alcuni approcci che il linguaggio utilizza con i parametri in
e applicando modreq
esclusivamente nelle posizioni virtuali.
Decisione Non usare modreq
nei metadati. La differenza tra out
e ref
non è modreq
, ma ora hanno valori di contesto di riferimento diversi. Non c'è alcun vantaggio reale nell'applicare solo parzialmente le regole con modreq
qui.
Consenti l'uso di buffer fissi multidimensionali
La progettazione per i buffer fixed
deve essere estesa per includere matrici di stili multidimensionali? Essenzialmente consentendo dichiarazioni come le seguenti:
struct Dimensions
{
int array[42, 13];
}
decisione Non consentire per il momento
Violazione dell'ambito
Il repository di runtime include diverse API non pubbliche che acquisiscono parametri ref
come campi ref
. Questi non sono sicuri perché la durata del valore risultante non viene rilevata. Ad esempio, il costruttore Span<T>(ref T value, int length)
.
La maggior parte di queste API sceglierà probabilmente di avere un monitoraggio adeguato della durata della vita del valore di ritorno, che verrà facilmente ottenuto aggiornando a C# 11. Alcuni, tuttavia, vogliono mantenere la semantica corrente di non tenere traccia del valore restituito perché l'intera finalità è quella di essere non sicura. Gli esempi più importanti sono MemoryMarshal.CreateSpan
e MemoryMarshal.CreateReadOnlySpan
. Questa operazione verrà ottenuta contrassegnando i parametri come scoped
.
Ciò significa che il runtime richiede un modello stabilito per rimuovere in modo non sicuro scoped
da un parametro:
-
Unsafe.AsRef<T>(in T value)
potrebbe espandere lo scopo esistente passando ascoped in T value
. In questo modo è possibile rimuoverein
escoped
dai parametri. Diventa quindi il metodo universale per "rimuovere la sicurezza dei riferimenti" - Introdurre un nuovo metodo il cui intero scopo è rimuovere
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Ciò rimuove anchein
perché, se non lo fosse, i chiamanti avrebbero ancora bisogno di una combinazione di chiamate al metodo per "rimuovere la sicurezza dei riferimenti", nel qual caso la soluzione esistente è probabilmente sufficiente.
L'ambito è stato ignorato per impostazione predefinita?
Il design ha solo due posizioni che sono scoped
per impostazione predefinita:
-
this
èscoped ref
-
out
èscoped ref
La decisione sulla out
consiste nel ridurre significativamente l'onere di compatibilità dei campi ref
ed è al contempo più naturale come impostazione predefinita. Permette agli sviluppatori di considerare effettivamente out
come un flusso di dati che scorre verso l'esterno. Al contrario, se è ref
, allora le regole devono prendere in considerazione il flusso di dati in entrambe le direzioni. Ciò porta a confusione significativa per gli sviluppatori.
La decisione sull'this
è sgradita perché significa che un struct
non può restituire un campo di ref
. Questo è uno scenario importante per gli sviluppatori di prestazioni elevate e l'attributo [UnscopedRef]
è stato aggiunto essenzialmente per questo scenario.
Le parole chiave hanno uno standard elevato e l'aggiunta di essa per un singolo scenario è sospetta. Nell’ambito di queste considerazioni, si è pensato se fosse possibile evitare del tutto questa parola chiave facendo sì che this
fosse semplicemente ref
per impostazione predefinita e non scoped ref
. Tutti i membri che necessitano che this
sia scoped ref
possono farlo contrassegnando il metodo scoped
(poiché un metodo può essere contrassegnato come readonly
per creare un readonly ref
oggi).
In una struct
normale, questo cambiamento è principalmente positivo poiché introduce solo problemi di compatibilità quando un membro ha un valore di ritorno ref
. Ci sono molto pochi di questi metodi e uno strumento potrebbe individuarli e convertirli in scoped
membri rapidamente.
Questa modifica introduce problemi di compatibilità significativamente più grandi su un ref struct
. Considerare quanto segue:
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;
}
}
Essenzialmente, significherebbe che tutte le chiamate ai metodi di istanza nelle variabili locali modificabiliref struct
sarebbero illegali a meno che la variabile locale non fosse ulteriormente contrassegnata come scoped
. Le regole devono considerare il caso in cui i campi sono stati riassegnati ad altri campi in this
. Un readonly ref struct
non presenta questo problema perché la caratteristica di readonly
impedisce la riassegnazione dei riferimenti. Si tratta comunque di una modifica significativa che rompe la compatibilità retroattiva, in quanto influirebbe praticamente su ogni ref struct
modificabile esistente.
Un readonly ref struct
tuttavia è ancora problematico quando ci si espande a includere campi ref
a ref struct
. Consente di affrontare lo stesso problema di base semplicemente spostando l'acquisizione nel valore del campo 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);
}
}
Si è pensato di dare a this
impostazioni predefinite diverse a seconda del tipo di struct
o del membro. Per esempio:
-
this
comeref
:struct
,readonly ref struct
oreadonly member
-
this
comescoped ref
:ref struct
oreadonly ref struct
con il camporef
fino aref struct
Ciò riduce al minimo le interruzioni di compatibilità e ottimizza la flessibilità, ma al costo di complicare la storia per i clienti. Inoltre, non risolve completamente il problema perché le funzionalità future, come i buffer sicuri di fixed
, richiedono che un ref struct
modificabile abbia restituzioni ref
per i campi che non funzionano solo con questo design, poiché rientrerebbero nella categoria scoped ref
.
decisione mantenere this
come scoped ref
. Ciò significa che gli esempi subdoli precedenti producono errori di compilazione.
campi di riferimento per lo struct di riferimento
Questa funzionalità apre un nuovo set di regole di contesto sicuro di riferimento perché consente a un campo di ref
di fare riferimento a un ref struct
. Questa natura generica di ByReference<T>
significava che fino ad ora il runtime non poteva avere un costrutto di questo tipo. Di conseguenza, tutte le nostre regole sono scritte sotto il presupposto che non sia possibile. La caratteristica del campo ref
non consiste principalmente nella creazione di nuove regole, ma nel codificare le regole esistenti nel nostro sistema. Per consentire ai campi ref
di ref struct
, è necessario codificare nuove regole perché ci sono diversi nuovi scenari da considerare.
Il primo è che un readonly ref
è ora in grado di memorizzare lo stato ref
. Per esempio:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Ciò significa che quando si pensa agli argomenti del metodo, questi devono corrispondere alle regole. È necessario considerare che readonly ref T
è un potenziale output del metodo quando T
ha potenzialmente un campo ref
in relazione a un ref struct
.
Il secondo problema è che il linguaggio di programmazione deve considerare un nuovo tipo di contesto sicuro: ref-field-safe-context. Tutti i ref struct
che contengono in modo transitivo un campo ref
hanno un altro ambito di escape che rappresenta i valori nei campi ref
. Nel caso di più campi ref
possono essere rilevati collettivamente come un singolo valore. Il valore predefinito di questo parametro è contesto chiamante.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Questo valore non è correlato al contesto sicuro del contenitore; ovvero, mentre il contesto del contenitore si riduce, non ha alcun impatto sul contesto sicuro di riferimento del campo dei valori dei campi ref
. Inoltre, il contesto sicuro di riferimento non può mai essere inferiore al contesto sicuro del contenitore.
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];
}
Questo ref-field-safe-context è essenzialmente sempre esistito. Fino ad ora, i campi ref
potevano puntare solo al normale struct
; di conseguenza, è stato quindi facilmente compresso nel contesto chiamante . Per supportare i campi ref
per ref struct
è necessario aggiornare le regole esistenti per tenere conto di questo nuovo contesto di riferimento.
Terzo, è necessario aggiornare le regole per la riassegnazione dei riferimenti per assicurarsi che non si violi il contesto del campo di riferimento per i valori. Essenzialmente per
Questi problemi sono molto risolvibili. Il team del compilatore ha delineato alcune versioni di queste regole e in gran parte non rientrano nell'analisi esistente. Il problema è che non esiste codice di utilizzo per tali regole che aiutano a dimostrare la correttezza e l'usabilità. Questo ci rende molto esitanti ad aggiungere il supporto per paura di scegliere impostazioni predefinite errate e costringere nuovamente il runtime in un angolo di usabilità quando ne approfitta. Questo problema è particolarmente rilevante perché .NET 8 probabilmente ci spinge in tale direzione con allow T: ref struct
e Span<Span<T>>
. Le regole sarebbero meglio scritte se eseguite in combinazione con il codice di consumo.
Decisione Ritardo che consente al ref
campo per ref struct
fino a .NET 8, dove abbiamo scenari che aiuteranno a gestire le regole relative a questi scenari. Questa operazione non è stata implementata a partire da .NET 9
Cosa definirà la versione 11.0 di C#?
Le funzionalità descritte in questo documento non devono essere implementate in un unico passaggio. Possono invece essere implementati in fasi in diverse versioni linguistiche nelle categorie seguenti:
-
ref
campi escoped
[UnscopedRef]
-
ref
campi daref struct
- Tipi con restrizioni Sunset
- buffer a dimensione fissa
Ciò che viene implementato in quale rilascio è semplicemente un esercizio di delimitazione dell'ambito.
Decisione Solo (1) e (2) hanno reso C# 11.0. Il resto verrà considerato nelle versioni future di C#.
Considerazioni future
Annotazioni avanzate sul ciclo di vita
Le annotazioni di durata in questa proposta sono limitate in quanto consentono agli sviluppatori di modificare il comportamento predefinito di escape/non escape dei valori. Ciò aggiunge una potente flessibilità al modello, ma non modifica radicalmente il set di relazioni che possono essere espresse. Alla base, il modello C# è ancora essenzialmente binario: un valore può essere restituito o meno?
Ciò consente di comprendere le relazioni di durata limitate. Ad esempio, un valore che non può essere restituito da un metodo ha una durata inferiore a quella che può essere restituita da un metodo. Non è tuttavia possibile descrivere la relazione di durata tra valori che possono essere restituiti da un metodo. In particolare, non c'è modo di dire che un valore ha una durata maggiore rispetto all'altro una volta stabilito che entrambi possono essere restituiti da un metodo. Il passaggio successivo nella nostra evoluzione vitale sarebbe consentire la descrizione di tali relazioni.
Altri metodi, ad esempio Rust, consentono di esprimere questo tipo di relazione e di conseguenza possono implementare operazioni di stile scoped
più complesse. Il nostro linguaggio potrebbe trarre vantaggio in modo analogo se tale funzionalità è stata inclusa. Al momento non c'è alcuna motivazione a farlo, ma se in futuro ci fosse, il nostro modello scoped
potrebbe essere ampliato per includerlo in modo piuttosto semplice.
A ogni scoped
è possibile assegnare una durata denominata aggiungendo un argomento di stile generico alla sintassi. Ad esempio, scoped<'a>
è un valore con durata 'a
. I vincoli come where
possono quindi essere usati per descrivere le relazioni tra questi cicli di vita.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Questo metodo definisce due durate di vita 'a
e 'b
e la loro relazione, ossia che 'b
è maggiore di 'a
. Ciò consente al callsite di avere regole più granulari per la modalità con cui i valori possono essere passati in modo sicuro nei metodi, rispetto alle attuali regole più grossolane.
Informazioni correlate
Problemi
Le seguenti questioni sono tutte correlate a questa proposta:
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Proposte
Le seguenti proposte sono correlate a questa proposta:
Esempi esistenti
Questo frammento specifico richiede unsafe perché si verificano problemi con il passaggio di un Span<T>
che può essere allocato a un metodo di istanza in un ref struct
. Anche se questo parametro non è acquisito il linguaggio deve presupporre che sia e quindi causa inutile attrito qui.
Questo frammento di codice vuole modificare un parametro applicando l'escapamento degli elementi dei dati. I dati con escape possono essere allocati in stack per garantire l'efficienza. Anche se il parametro non è preceduto da un carattere di escape, il compilatore assegna un contesto sicuro all'esterno del metodo di inclusione perché è un parametro. Ciò significa che, per usare l'allocazione dello stack, l'implementazione deve usare unsafe
per riassegnare al parametro dopo aver eseguito l'escape dei dati.
Esempi divertenti
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;
}
}
Elenco parsimonioso
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;
}
}
}
}
Esempi e note
Di seguito è riportato un set di esempi che illustrano come e perché le regole funzionano nel modo in cui funzionano. Sono inclusi diversi esempi che mostrano comportamenti pericolosi e come le regole impediscono che si verifichino. È importante tenere presente questi aspetti quando si apportano modifiche alla proposta.
Riassegnazione dei riferimenti e punti di invocazione
Illustra come funzionano insieme la riassegnazione dei riferimenti e l'invocazione del metodo .
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;
}
}
Riassegnazione dei riferimenti e escape non sicuri
Il motivo della riga seguente nelle regole di riassegnazione dei riferimenti potrebbe non essere ovvio a prima vista:
e1
deve avere lo stesso contesto sicuro die2
Ciò è dovuto al fatto che la durata dei valori a cui punta la posizione ref
è invariata. L'indirezione ci impedisce di consentire qualsiasi tipo di varianza qui, anche per durate più brevi. Se si consente una riduzione, si apre la possibilità per il seguente codice non sicuro:
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];
}
Da ref
a non ref struct
, questa regola è soddisfatta facilmente perché tutti i valori hanno lo stesso contesto sicuro. Questa regola veramente entra in gioco solo quando il valore è un ref struct
.
Questo comportamento di ref
sarà importante anche in un futuro in cui consentiamo ai campi ref
di ref struct
.
ambito locale
L'uso di scoped
nelle variabili locali sarà particolarmente utile per i modelli di codice che assegnano valori in modo condizionale in vari contesti sicuri alle variabili locali. Significa che il codice non deve più basarsi su trucchi di inizializzazione come
// 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];
}
Questo modello si presenta spesso nel codice di basso livello. Quando il ref struct
coinvolto è Span<T>
è possibile usare il trucco precedente. Non è tuttavia applicabile ad altri tipi di ref struct
e può comportare la necessità, per il codice di basso livello, di ricorrere a unsafe
per aggirare l'impossibilità di specificare correttamente la durata.
valori dei parametri con scopo
Una fonte di attrito ricorrente nel codice di basso livello è la natura permissiva dell’escape predefinito per i parametri. Sono di contesto sicuro per il contesto del chiamante . Si tratta di un valore predefinito ragionevole perché si allinea con i modelli di codifica di .NET nel suo complesso. Nel codice di basso livello, tuttavia, c'è un uso più esteso di ref struct
e questo valore predefinito può causare attriti con altre parti delle regole del contesto di riferimento sicuro.
Il punto di attrito principale si verifica perché gli argomenti del metodo devono corrispondere alla regola. Di solito, questa regola entra in gioco con i metodi di istanza su ref struct
, dove almeno un parametro è anche un ref struct
. Si tratta di un modello comune nel codice di basso livello in cui i tipi ref struct
in genere sfruttano i parametri Span<T>
nei relativi metodi. Ad esempio, si verificherà in qualsiasi tipo writer o stile di scrittura ref struct
che usa Span<T>
per trasmettere i buffer.
Questa regola esiste per impedire scenari come il seguente:
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);
}
}
Essenzialmente questa regola esiste perché il linguaggio deve presupporre che tutti gli input di un metodo escono verso il loro massimo consentito contesto sicuro. Quando sono presenti i parametri ref
o out
, inclusi i ricevitori, è possibile che gli input possano sfuggire come campi associati ai valori ref
(come avviene in RS.Set
sopra).
In pratica, esistono molti metodi di questo tipo che passano ref struct
come parametri senza mai volerli acquisire nell'output. Si tratta solo di un valore usato all'interno del metodo corrente. Per esempio:
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))
{
...
}
}
}
Per aggirare questo codice di basso livello, si ricorre a trucchi unsafe
per mentire al compilatore sulla durata del loro ref struct
. In questo modo si riduce significativamente la proposta di valore di ref struct
in quanto sono concepiti per evitare unsafe
continuando a scrivere codice ad alte prestazioni.
In questo caso scoped
è uno strumento efficace sui parametri ref struct
perché li rimuove dalla considerazione come restituiti dal metodo in base agli argomenti del metodo aggiornati devono corrispondere alla regola. Un parametro ref struct
utilizzato, ma mai restituito, può essere etichettato come scoped
per rendere i siti di chiamata più flessibili.
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))
{
...
}
}
}
Impedire l'assegnazione complessa di riferimenti derivante da una mutazione in sola lettura
Quando un ref
viene impiegato in un campo di readonly
in un costruttore o init
membro, il tipo è ref
e non ref readonly
. Si tratta di un comportamento di lunga durata che consente il codice simile al seguente:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Ciò comporta un potenziale problema se un tale ref
potesse essere archiviato in un campo ref
del medesimo tipo. Consentirebbe la mutazione diretta di un readonly struct
da un membro dell'istanza:
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++;
}
}
La proposta impedisce ciò perché viola le regole di contesto di riferimento sicuro. Considerare quanto segue:
- Il contesto sicuro di riferimento di
this
è membro-funzione e il contesto sicuro è il contesto chiamante . Entrambi sono standard perthis
in un membrostruct
. - Il del contesto di riferimento
di è membro della funzione . Ciò non rientra nelle regole di durata del campo . In particolare, regola 4.
A quel punto la riga r = ref i
non è valida per le regole di riassegnazione del riferimento .
Queste regole non sono state concepite per impedire questo comportamento, ma come effetto collaterale. È importante tenere presente questo aspetto per qualsiasi aggiornamento futuro delle regole per valutare l'impatto sugli scenari come questo.
Stupida assegnazione ciclica
Un aspetto in cui questo design ha avuto difficoltà è come un ref
possa essere restituito liberamente da un metodo. Consentire a tutti i ref
di essere restituiti liberamente come i valori normali è probabile che la maggior parte degli sviluppatori si aspetti in modo intuitivo. Tuttavia, consente scenari patologici che il compilatore deve considerare quando calcola la sicurezza dei riferimenti. Considerare quanto segue:
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;
}
}
Questo non è un modello di codice che si prevede che gli sviluppatori usino. Tuttavia, quando un ref
può essere restituito con la stessa durata di un valore, è legale in base alle regole. Il compilatore deve prendere in considerazione tutti i casi legali durante la valutazione di una chiamata al metodo e ciò comporta l'inutilizzabilità di tali API.
void M(ref S s)
{
...
}
void Usage()
{
// safe-context to caller-context
S local = default;
// Error: compiler is forced to assume the worst and concludes a self assignment
// is possible here and must issue an error.
M(ref local);
}
Per rendere utilizzabili queste API, il compilatore garantisce che la durata ref
per un parametro ref
sia inferiore alla durata di tutti i riferimenti nel valore del parametro associato. Si tratta della logica nel disporre di un contesto ref-safe per ref
a ref struct
che sia di restituzione esclusiva e out
che sia contesto chiamante . Ciò impedisce l'assegnazione ciclica a causa della differenza nei tempi di vita.
Si noti che
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;
}
}
Analogamente, [UnscopedRef] out
consente un'assegnazione ciclica perché il parametro ha sia il contesto sicuro che il contesto di sicurezza di riferimento di solo restituzione.
Promuovere [UnscopedRef] ref
a contesto chiamante è utile quando il tipo non è non un ref struct
(si noti che si vuole mantenere le regole semplici in modo che non distinguano i riferimenti a ref e gli struct non ref):
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;
}
}
In termini di annotazioni avanzate, la progettazione [UnscopedRef]
crea quanto segue:
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 non può essere approfondito tramite i campi ref
Si consideri l'esempio di codice seguente:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Quando si progettano le regole per i campi ref
nelle istanze readonly
in maniera isolata, le regole possono essere validamente progettate affinché quanto sopra sia legale o illegale. Essenzialmente readonly
può validamente essere approfondito in un campo ref
oppure può essere applicato solo all'ref
. L'applicazione solo al ref
impedisce la riassegnazione dei riferimenti, ma consente l'assegnazione normale che modifica il valore a cui si fa riferimento.
Questa progettazione non esiste in isolamento, ma sta elaborando delle regole per tipi che già dispongono di campi ref
. Il più rilevante, Span<T>
, dipende già fortemente dal fatto che readonly
non sia profondamente presente qui. Lo scenario principale è la possibilità di assegnare il campo ref
attraverso un'istanza di readonly
.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Ciò significa che dobbiamo scegliere l'interpretazione superficiale di readonly
.
Costruttori per la modellazione
Una sottile domanda di progettazione è: Come vengono modellati i corpi dei costruttori per la sicurezza dei riferimenti? Come viene essenzialmente analizzato il costruttore seguente?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Esistono circa due approcci:
- Modello come metodo di
in cui è un'istanza locale in cui contesto sicuro è contesto chiamante - Modello come metodo
static
in cuithis
è un parametroout
.
Un altro costruttore deve soddisfare i seguenti invarianti:
- Assicurarsi che i parametri
ref
possano essere acquisiti come campiref
. - Assicurati che i campi di
ref
perthis
non possano essere sfuggiti tramite i parametri diref
. Violerebbe l'assegnazione di riferimento complicata .
Lo scopo è quello di scegliere il modulo che soddisfa i nostri invarianti senza introdurre regole speciali per i costruttori. Dato che il modello migliore per i costruttori sta visualizzando this
come parametro out
. Il restituire solo natura del out
consente di soddisfare tutti gli invarianti sopra senza maiuscole e minuscole speciali:
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;
}
Gli argomenti del metodo devono corrispondere
Il principio per cui gli argomenti del metodo devono rispettare una certa regola è una fonte comune di confusione per gli sviluppatori. Si tratta di una regola che ha un certo numero di casi speciali che sono difficili da comprendere a meno che non si abbia familiarità con il ragionamento alla base della regola. Per comprendere meglio i motivi della regola, semplificheremo il contesto di riferimento e il contesto sicuro in modo da avere semplicemente il contesto .
I metodi possono restituire con una certa libertà lo stato passato a loro come parametri. È possibile restituire essenzialmente qualsiasi stato raggiungibile non limitato a uno scopo (inclusa la restituzione da ref
). Questa operazione può essere restituita direttamente tramite un'istruzione return
o indirettamente assegnando un valore ref
.
I ritorni diretti non rappresentano molti problemi per la sicurezza dei riferimenti. Il compilatore deve semplicemente esaminare tutti gli input restituibili a un metodo e quindi limita efficacemente il valore restituito al minimo contesto dell'input. Tale valore restituito passa quindi attraverso la normale elaborazione.
I rendimenti indiretti costituiscono un problema significativo perché tutti i ref
sono contemporaneamente sia input che output per il metodo. Questi output hanno già un noto contesto . Il compilatore non può dedurre quelli nuovi e deve considerarli al livello corrente. Ciò significa che il compilatore deve esaminare ogni singolo ref
che è assegnabile nel metodo chiamato, valutarlo è contestoe quindi verificare che nessun input restituito al metodo abbia un contesto di più piccolo rispetto a quello ref
. Se esiste un caso di questo tipo, la chiamata al metodo deve essere illegale perché potrebbe violare la sicurezza ref
.
Gli argomenti del metodo devono corrispondere è il processo in base al quale il compilatore asserisce questo controllo di sicurezza.
Un modo diverso per valutare questo che è spesso più facile da considerare per gli sviluppatori consiste nell'eseguire l'esercizio seguente:
- Esaminare la definizione del metodo per identificare tutte le posizioni in cui è possibile restituire indirettamente lo stato: a. Parametri di
ref
modificabili che puntano aref struct
b. Parametriref
modificabili con campiref
assegnabili ref c. Parametriref
assegnabili o campiref
che puntano aref struct
(considerare in modo ricorsivo) - Esaminare il sito di chiamata a. Identificare i contesti allineati con le posizioni identificate sopra b. Identificare i contesti di tutti gli input del metodo che possono essere restituiti e che non si allineano con i parametri di
scoped
.
Se un valore in 2.b è minore di 2.a, la chiamata al metodo deve essere illegale. Verranno ora esaminati alcuni esempi per illustrare le regole:
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);
}
}
Esaminando la chiamata a F0
è possibile passare attraverso (1) e (2). I parametri con potenziale ritorno indiretto sono a
e b
come entrambi possono essere assegnati direttamente. Gli argomenti che si allineano a tali parametri sono:
-
a
che mappa ax
che ha contesto di contesto del chiamante -
b
che mappa ay
nel contesto come membro della funzione
L'insieme di input restituibili al metodo è
-
x
con ambito di escape di contesto del chiamante -
ref x
con ambito di escape di contesto del chiamante -
y
con ambito di escape di membro della funzione
Il valore ref y
non può essere restituito perché esegue il mapping a un scoped ref
di conseguenza non viene considerato un input. Tuttavia, dato che è presente almeno un input con un ambito di escape inferiore (argomentoy
) rispetto a uno delle uscite (argomentox
), la chiamata al metodo non è valida.
Una variante diversa è la seguente:
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);
}
}
Anche in questo caso, i parametri con potenziali restituzioni indirette sono a
e b
come entrambi possono essere assegnati direttamente. Ma b
può essere escluso perché non punta a un ref struct
, quindi non può essere usato per archiviare lo stato ref
. Così abbiamo:
-
a
che mappa ax
che ha contesto di contesto del chiamante
Il set di input restituito al metodo è:
con di contesto di contesto del chiamante con di contesto di contesto del chiamante -
ref y
con contesto di membro-funzione di
Dato che è presente almeno un input con un ambito di escape più piccolo ( argomentoref y
) rispetto a uno degli output ( argomentox
) la chiamata al metodo non è valida.
Si tratta della logica che gli argomenti del metodo devono corrispondere alla regola che sta tentando di includere. Viene ulteriormente considerato scoped
come modo per rimuovere gli input dalla considerazione e readonly
come modo per rimuovere ref
come output (non può essere assegnato in un readonly ref
in modo che non possa essere un'origine di output). Questi casi speciali aggiungono complessità alle regole, ma è fatto per il vantaggio dello sviluppatore. Il compilatore cerca di rimuovere tutti gli input e gli output che sa non possono contribuire al risultato, per offrire agli sviluppatori la massima flessibilità quando chiamano un membro. Analogamente alla risoluzione del sovraccarico, vale la pena di rendere le regole più complesse quando creano una maggiore flessibilità per gli utenti.
Esempi di contesto sicuro inferito delle espressioni di dichiarazione
Riferito a Infer contesto sicuro delle espressioni di dichiarazione.
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).
}
}
Si noti che il contesto locale risultante dal modificatore scoped
è il più ristretto possibile che potrebbe essere utilizzato per la variabile. Se fosse ulteriormente ristretto, l'espressione farebbe riferimento a variabili dichiarate solo in un contesto più confinato rispetto all'espressione stessa.
C# feature specifications