Domanda Ref restituisce le restrizioni in C # 7.0


Sto cercando di capire il seguente estratto da un post ufficiale del blog sulle nuove funzionalità in C # 7.0 riguardanti i resi di ref.

  1. Puoi solo restituire i ref che sono "sicuri di tornare": quelli che lo erano   passato a te, e quelli che puntano in campi negli oggetti. 

  2. I locatori Ref vengono inizializzati in una determinata posizione di archiviazione e non possono essere modificati per puntare a un'altra.

Sfortunatamente, il post del blog non fornisce alcun esempio di codice. Apprezzeremmo molto se qualcuno potesse gettare più luce sulle restrizioni evidenziate in grassetto con esempi pratici e una spiegazione.

Grazie in anticipo.


17
2018-03-18 13:46


origine


risposte:


Hai delle risposte che chiariscono la restrizione, ma non il ragionamento alla base della restrizione.

Il ragionamento dietro la restrizione è questo non dobbiamo mai permettere ad un alias di una variabile morta. Se si dispone di un comune ordinario in un metodo ordinario, e si restituisce un riferimento a esso, quindi il locale è morto dal momento in cui viene utilizzato il riferimento.

Ora, si potrebbe sottolineare che un locale che viene restituito da ref potrebbe essere issato in un campo di una classe di chiusura. Sì, questo risolverebbe il problema. Ma il punto della caratteristica è quello di consentire agli sviluppatori di scrivere meccanismi a basso costo close-to-the-machine ad alte prestazioni e di sollevare automaticamente a una chiusura - e quindi di assumere il peso della pressione di raccolta e così via - lavorare contro questo obiettivo.

Le cose possono diventare un po 'complicate. Prendere in considerazione:

ref int X(ref int y) { return ref y; }
ref int Z( )
{
  int z = 123;
  return ref X(ref z);
}

Qui stiamo restituendo un riferimento al locale z in modo subdolo! Anche questo deve essere illegale. Ma ora considera questo:

ref double X(ref int y) { return ref whatever; }
ref double Z( )
{
  int z = 123;
  return ref X(ref z);
}

Ora possiamo conoscere che il ref restituito non è il rif z. Quindi possiamo dire che questo è legale se i tipi di refs passati sono tutti diversi dai tipi di refs restituiti?

Che dire di questo?

struct S { public int s; }
ref int X(ref S y) { return ref y.s; }
ref int Z( )
{
  S z = default(S);
  return ref X(ref z);
}

Ora ancora una volta abbiamo restituito un riferimento a una variabile morta.

Quando abbiamo progettato questa funzione per la prima volta (nel 2010 IIRC) ci sono state una serie di proposte complicate per affrontare queste situazioni, ma la mia proposta preferita era semplicemente "renderle tutte illegali". Non è possibile restituire un riferimento che è stato restituito da un metodo di restituzione, anche se non è possibile che sia morto.

Non so quale sia stata la regola del team C # 7.


16
2018-03-18 15:39



Per passare qualcosa per riferimento, deve essere classificato come variabile. Le specifiche C # (§5 Variabili) definiscono sette categorie di variabili: variabili statiche, variabili di istanza, elementi di array, parametri di valore, parametri di riferimento, parametri di output e variabili locali.

class ClassName {
    public static int StaticField;
    public int InstanceField;
}
void Method(ref int i) { }
void Test1(int valueParameter, ref int referenceParameter, out int outParameter) {
    ClassName instance = new ClassName();
    int[] array = new int[1];
    outParameter=0;
    int localVariable = 0;
    Method(ref ClassName.StaticField);  //Static variable
    Method(ref instance.InstanceField); //Instance variable
    Method(ref array[0]);               //Array element
    Method(ref valueParameter);         //Value parameter
    Method(ref referenceParameter);     //Reference parameter
    Method(ref outParameter);           //Output parameter
    Method(ref localVariable);          //Local variable
}

Il primo punto in realtà dice che è possibile restituire variabili classificate come parametri di riferimento, parametri di output, variabili statiche e variabili di istanza.

ref int Test2(int valueParameter, ref int referenceParameter, out int outParameter) {
    ClassName instance = new ClassName();
    int[] array = new int[1];
    outParameter=0;
    int localVariable = 0;
    return ref ClassName.StaticField;  //OK, "ones that point into fields in objects"
    return ref instance.InstanceField; //OK, "ones that point into fields in objects"
    return ref array[0];               //OK, array elements are also "safe to return" by reference
    return ref valueParameter;         //Error
    return ref referenceParameter;     //OK, "ones that were passed to you"
    return ref outParameter;           //OK, "ones that were passed to you"
    return ref localVariable;          //Error
}

Si noti che, per esempio, i campi dei tipi di valore, si dovrebbe considerare lo stato di "sicurezza per la restituzione" della variabile che racchiude. Non è sempre consentito, come nel caso dei campi di riferimento di esempio:

struct StructName {
    public int InstacneField;
}
ref int Test3() {
    StructName[] array = new StructName[1];
    StructName localVariable = new StructName();
    return ref array[0].InstacneField;      //OK, array[0] is "safe to return"
    return ref localVariable.InstacneField; //Error, localVariable is not "safe to return"
}

Risultato del metodo di restituzione ref considerato "safe to return", se questo metodo non accetta argomenti non "safe to return":

ref int ReturnFirst(ref int i, ref int ignore) => ref i;
ref int Test4() {
    int[] array = new int[1];
    int localVariable = 0;
    return ref ReturnFirst(ref array[0], ref array[0]);      //OK, array[0] is "safe to return"
    return ref ReturnFirst(ref array[0], ref localVariable); //Error, localVariable is not "safe to return"
}

Anche se lo sappiamo ReturnFirst(ref array[0], ref localVariable) restituirà un riferimento "sicuro da restituire" (ref array[0]), il compilatore non può dedurlo analizzando Test4 metodo in isolamento. Quindi, risultato di ReturnFirst metodo in quel caso considerato come non "sicuro da restituire".

Il secondo punto dice che la dichiarazione delle variabili locali di ref deve includere l'inizializzatore:

int localVariable = 0;
ref int refLocal1;                     //Error, no initializer
ref int refLocal2 = ref localVariable; //OK

Inoltre, la variabile locale ref non può essere riassegnata per puntare ad un'altra posizione di archiviazione:

int localVariable1 = 0;
int localVariable2 = 0;
ref int refLocal = ref localVariable1;
ref refLocal = ref localVariable2;     //Error
refLocal = ref localVariable2;         //Error

In realtà non esiste una sintassi valida per riassegnare la variabile locale di ref.


6
2018-03-18 15:25



Puoi trovare una grande discussione su questa funzione a GitHub - Proposta: Ref Returns and Locals.

1. Puoi solo restituire i ref che sono "sicuri di tornare": quelli che lo erano   passati a te e quelli che puntano in campi negli oggetti.

L'esempio seguente mostra il ritorno di un riferimento sicuro perché viene eseguito dal chiamante:

public static ref TValue Choose<TValue>(ref TValue val)
{
    return ref val;
}

Viceversa, una versione non sicura di questo esempio restituirebbe un riferimento a un locale (questo codice non verrebbe compilato):

public static ref TValue Choose<TValue>()
{
    TValue val = default(TValue);
    return ref val;
}

2. I locatori Ref vengono inizializzati in una determinata posizione di archiviazione e non possono essere modificati per puntare a un'altra.

La restrizione significa che è necessario inizializzare un riferimento locale sempre alla dichiarazione. Una dichiarazione come

ref double aReference;

non compilerebbe. Inoltre, non è possibile assegnare un nuovo riferimento a un riferimento già esistente come

aReference = ref anOtherValue;

4
2018-03-18 14:55



Le altre risposte in questa pagina sono complete e utili, ma volevo aggiungere un ulteriore punto, che è quello out parametri, che la tua funzione è necessaria per inizializzare completamente, conta come "sicuro di tornare" allo scopo di ref ritorno.

È interessante notare che combinando questo fatto con un altro nuovo C # 7 caratteristica, dichiarazione inline di variabili 'out', consente la simulazione di uno scopo generale dichiarazione inline di variabili locali capacità:

funzione di supporto:

public static class _myglobals
{
    /// <summary> Helper function for declaring local variables inline. </summary>
    public static ref T local<T>(out T t)
    {
        t = default(T);
        return ref t;
    }
};

Con questo helper, il chiamante specifica il inizializzazione della "variabile locale inline" di assegnando al ref-return dell'aiuto

Per dimostrare l'helper, ecco un esempio di una semplice funzione di confronto a due livelli che sarebbe tipica per un (ad es.) MyObj.IComparable<MyObj>.Compare implementazione. Anche se molto semplice, questo tipo di espressione non può aggirare il bisogno di una singola variabile locale - senza duplicare il lavoro, cioè. Ora normalmente, il bisogno di un locale bloccherebbe l'uso di un membro di espressione che è quello che ci piacerebbe fare qui, ma il problema è facilmente risolvibile con l'aiuto di cui sopra:

public int CompareTo(MyObj x) =>
                       (local(out int d) = offs - x.offs) == 0 ? size - x.size : d;

Procedura dettagliata: Variabile locale d è "dichiarato in linea" e inizializzato con il risultato del calcolo del confronto di primo livello, basato sul offs campi. Se questo risultato è inconcludente, torniamo a restituire un ordinamento di secondo livello (basato sul dimensione campi). Ma in alternativa, abbiamo ancora il risultato di primo livello disponibile per il reso, dal momento che è stato salvato in locale d.

Nota che quanto sopra può anche essere fatto senza la funzione di supporto, tramite C # 7  corrispondenza del modello:

public int CompareTo(MyObj other) => 
                       (offs - x.offs) is int d && d == 0 ? size - x.size : d;

includi nella parte superiore dei tuoi file sorgente:

using System;
using /* etc... */
using System.Xml;
using Microsoft.Win32;

using static _myglobals;    //  <-- puts function 'local(...)' into global name scope

namespace MyNamespace
{
   // ...

I seguenti esempi mostrano dichiarare una variabile locale in linea con la sua inizializzazione in C # 7. Se l'inizializzazione non è fornita, ottiene default(T), come assegnato dal local<T>(out T t) funzione di aiuto. Questo è possibile solo ora con il ref return caratteristica, dal ref return i metodi sono gli unici metodi che possono essere usati come ℓ-value.

Esempio 1:

var s = "abc" + (local(out int i) = 2) + "xyz";   //   <-- inline declaration of local 'i'
i++;
Console.WriteLine(s + i);   //   -->  abc2xyz3

esempio 2:

if ((local(out OpenFileDialog dlg) = new OpenFileDialog       // <--- inline local 'dlg'
    {
        InitialDirectory = Environment.CurrentDirectory,
        Title = "Pick a file",
    }).ShowDialog() == true)
{
    MessageBox.Show(dlg.FileName);
}

Il primo esempio assegna banalmente da un intero letterale. Nel secondo esempio, il locale inline dlg è assegnato da un costruttore (new espressione), e quindi l'intera espressione di assegnazione viene utilizzata per il suo valore risolto per chiamare un metodo di istanza (ShowDialog) sull'istanza appena creata. Per chiarezza precisa come esempio indipendente, termina mostrando che l'istanza di riferimento di dlg doveva infatti essere chiamato come variabile, al fine di recuperare una delle sue proprietà.


[modificare:] Per quanto riguarda ...

2. I locatori Ref vengono inizializzati in una determinata posizione di archiviazione e non possono essere modificati per puntare a un'altra.

... sarebbe certamente bello avere un ref variabile con un referente mutabile, poiché ciò contribuirebbe a evitare costosi controlli sui limiti di indicizzazione all'interno dei corpi degli anelli. Naturalmente, questo è anche esattamente il motivo per cui non è permesso. Probabilmente non puoi aggirare questo (es. ref a un'espressione di accesso dell'array con indicizzazione contenente ref non funzionerà; viene definitivamente risolto all'elemento nella posizione di riferimento quando inizializzato), ma se aiuta, nota che tu può prendere un ref a un puntatore, e questo include ref locale:

int i = 5, j = 6;

int* pi = &i;
ref int* rpi = ref pi;

Console.WriteLine(i + " " + *pi + " " + *rpi);      //   "5 5 5"

pi = &j;

Console.WriteLine(i + " " + *pi + " " + *rpi);      //   "5 6 6"

Il punto di questo codice di esempio dichiaratamente inutile è che, sebbene non abbiamo modificato ref locale variabile rpi stesso in qualsiasi modo (dal momento che non puoi), esso fa ora hanno un referente (definitivo) diverso.


Più seriamente, cosa ref locale  fa ora, per quanto riguarda il rafforzamento dell'IL nei corpi dell'anello di indicizzazione dell'array, è una tecnica che chiamo timbratura di tipo valore. Ciò consente un efficiente IL in corpi di loop che devono accedere a più campi di ciascun elemento in una matrice di tipi di valore. In genere, questo è stato un compromesso tra l'inizializzazione esterna (newobj / initobj) seguito da un singolo accesso di indicizzazione rispetto a sul posto non inizializzazione ma con la spesa dell'indicizzazione di runtime multiplo ridondante.

Con timbratura di tipo valore tuttavia, ora possiamo completamente evitare per-elemento initobj / newobj Istruzioni IL e anche solo un singolo calcolo di indicizzazione in fase di esecuzione. Mostrerò prima l'esempio e poi descriverò la tecnica in generale di seguito.

/// <summary>
/// Returns a new array of (int,T) where each element of 'src' is paired with its index.
/// </summary>
public static (int Index, T Item)[] TagWithIndex<T>(this T[] src)
{
    if (src.Length == 0)
        return new (int, T)[0];

    var dst = new (int Index, T Item)[src.Length];     // i.e, ValueTuple<int,T>[]
    ref var p = ref dst[0];      //  <--  co-opt element 0 of target for 'T' staging

    ref int i = ref p.Index;  //  <-- index field in target will also control loop
    i = src.Length;    

    while (true)
    {
        p.Item = src[--i];
        if (i == 0)
            return dst;
        dst[i] = p;
    }
}

L'esempio mostra un uso conciso ma estremo della tecnica di stampaggio valore-tipo; puoi discernere il suo tocco (dato via in un commento) da solo se sei interessato. Nel seguito, parlerò invece della tecnica di stampaggio del valore in termini più generali.

Per prima cosa, preparati ref locali con riferimenti direttamente ai campi rilevanti in a istanza di staging del tipo di valore. Questo può essere o nello stack, o, come mostrato nell'esempio, cooptato dall'elemento last-to-be-processed dell'array di destinazione stesso. Potrebbe essere utile avere un ref anche per l'intera istanza di staging, specialmente se si utilizza la tecnica co-opting.

Ogni iterazione del corpo del loop può quindi preparare l'istanza di staging in modo molto efficiente e, come fase finale, quando è pronta, "stamparla" all'ingrosso nell'array con una sola operazione di indicizzazione. Naturalmente, se l'elemento finale dell'array è stato cooptato come istanza di staging, è possibile lasciare il ciclo leggermente prima.


0
2017-09-07 22:14