Una dichiarazione di ritorno deve essere all'interno o all'esterno di una serratura?


142

Mi sono appena reso conto che in qualche punto del mio codice ho l'istruzione return all'interno della serratura e qualche volta all'esterno. Qual'è il migliore?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

Quale dovrei usare?


Che ne dici di sparare a Reflector e fare qualche paragone con IL ;-).
Pop Catalin,

6
@Pop: fatto - nessuno dei due è migliore in termini di IL - si applica solo lo stile C #
Marc Gravell

1
Molto interessante, wow imparo qualcosa oggi!
Pokus,

Risposte:


192

In sostanza, il che rende il codice più semplice. Il singolo punto di uscita è un bell'ideale, ma non piegherei il codice fuori forma solo per raggiungerlo ... E se l'alternativa è dichiarare una variabile locale (all'esterno del blocco), inizializzarla (all'interno del blocco) e poi restituendolo (fuori dalla serratura), quindi direi che un semplice "ritorno" all'interno della serratura è molto più semplice.

Per mostrare la differenza in IL, lascia il codice:

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(nota che direi felicemente che ReturnInsideè un pezzo di C # più semplice / più pulito)

E guarda IL (modalità di rilascio ecc.):

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

Quindi a livello di IL sono [dare o prendere alcuni nomi] identici (ho imparato qualcosa ;-p). In quanto tale, l'unico confronto sensato è la legge (altamente soggettiva) dello stile di codifica locale ... Preferisco ReturnInsideper semplicità, ma non mi entusiasmerei neanche.


15
Ho usato il (gratuito ed eccellente) .NET Reflector di Red Gate (era: Lutz Roeder's .NET Reflector), ma anche ILDASM lo avrebbe fatto.
Marc Gravell

1
Uno degli aspetti più potenti di Reflector è che puoi effettivamente disassemblare IL nella tua lingua preferita (C #, VB, Delphi, MC ++, Chrome, ecc.)
Marc Gravell

3
Per il tuo semplice esempio l'IL rimane lo stesso, ma probabilmente perché restituisci solo un valore costante ?! Credo che per gli scenari della vita reale il risultato possa essere diverso e i thread paralleli potrebbero causare problemi l'uno con l'altro modificando il valore prima che venga restituito, l'istruzione return è al di fuori del blocco di blocco. Pericoloso!
Torbjørn,

@MarcGravell: Mi sono appena imbattuto nel tuo post mentre meditavo sullo stesso e anche dopo aver letto la tua risposta, non sono ancora sicuro di quanto segue: ci sono QUALSIASI circostanza in cui l'uso dell'approccio esterno potrebbe violare la logica thread-safe. Lo chiedo poiché preferisco un singolo punto di ritorno e non "SENTITO" bene per quanto riguarda la sicurezza del thread. Sebbene, se l'IL è lo stesso, la mia preoccupazione dovrebbe essere messa in discussione comunque.
Raheel Khan,

1
@RaheelKhan no, none; loro sono la stessa cosa. A livello di IL, non è possibile ret all'interno di una .tryregione.
Marc Gravell

42

Non fa alcuna differenza; sono entrambi tradotti nella stessa cosa dal compilatore.

Per chiarire, uno dei due viene effettivamente tradotto in qualcosa con la seguente semantica:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;

1
Bene, questo è vero per il tentativo / finalmente - tuttavia, il ritorno fuori dalla serratura richiede ancora locali extra che non possono essere ottimizzati via - e richiede più codice ...
Marc Gravell

3
Non puoi tornare da un blocco try; deve terminare con un codice ".leave". Quindi il CIL emesso dovrebbe essere lo stesso in entrambi i casi.
Greg Beech,

3
Hai ragione - ho appena guardato l'IL (vedi post aggiornato). Ho imparato qualcosa ;-p
Marc Gravell

2
Bene, sfortunatamente ho imparato da ore dolorose cercando di emettere codici operativi .ret in blocchi di prova e facendo in modo che il CLR si rifiuti di caricare i miei metodi dinamici :-(
Greg Beech,

Posso capire; Ho fatto una buona dose di Reflection.Emit, ma sono pigro; a meno che non sia molto sicuro di qualcosa, scrivo un codice rappresentativo in C # e poi guardo IL. Ma è sorprendente quanto velocemente inizi a pensare in termini di IL (cioè sequenziando lo stack).
Marc Gravell

28

Vorrei sicuramente mettere il ritorno all'interno della serratura. Altrimenti si rischia che un altro thread inserisca il blocco e modifichi la propria variabile prima dell'istruzione return, facendo in modo che il chiamante originale riceva un valore diverso da quello previsto.


4
Questo è corretto, un punto che sembra mancare agli altri soccorritori. I semplici campioni che hanno realizzato possono produrre lo stesso IL, ma non è così per la maggior parte degli scenari di vita reale.
Torbjørn,

4
Sono sorpreso che le altre risposte non ne parlino
Akshat Agarwal l'

5
In questo esempio stanno parlando dell'utilizzo di una variabile stack per memorizzare il valore restituito, ovvero solo l'istruzione return al di fuori del blocco e ovviamente la dichiarazione della variabile. Un altro thread dovrebbe avere un altro stack e quindi non potrebbe causare alcun danno, vero?
Guillermo Ruffino,

3
Non penso che questo sia un punto valido, in quanto un altro thread potrebbe aggiornare il valore tra la chiamata di ritorno e l'assegnazione effettiva del valore di ritorno alla variabile sul thread principale. Il valore restituito non può essere modificato o garantito in modo coerente con il valore attuale attuale in entrambi i modi. Destra?
Uroš Joksimović,

Questa risposta non è corretta Un altro thread non può cambiare una variabile locale. Le variabili locali sono memorizzate nello stack e ogni thread ha il proprio stack. A proposito, la dimensione predefinita dello stack di un thread è 1 MB .
Theodor Zoulias,

5

Dipende,

Ho intenzione di andare contro il grano qui. In genere ritornerei all'interno della serratura.

Di solito la variabile mydata è una variabile locale. Mi piace dichiarare le variabili locali mentre le inizializzo. Raramente ho i dati per inizializzare il mio valore di ritorno al di fuori del mio blocco.

Quindi il tuo confronto è in realtà imperfetto. Mentre idealmente la differenza tra le due opzioni sarebbe come avevi scritto, il che sembra dare un cenno al caso 1, in pratica è un po 'più brutto.

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

vs.

void example() { 
    lock (foo) {
        return ...;
    }
}

Trovo che il caso 2 sia notevolmente più facile da leggere e più difficile da rovinare, specialmente per frammenti corti.


4

Se ritieni che il blocco esterno sia migliore, ma fai attenzione se finisci per modificare il codice in:

return f(...)

Se f () deve essere chiamato con il blocco tenuto, allora ovviamente deve essere all'interno del blocco, poiché ha senso mantenere i ritorni all'interno del blocco per coerenza.


1

Per quello che vale, la documentazione su MSDN ha un esempio di ritorno dall'interno del blocco. Dalle altre risposte qui, sembra essere IL abbastanza simile ma, per me, sembra più sicuro tornare dall'interno del lock perché non corri il rischio che una variabile return sia sovrascritta da un altro thread.


0

Per facilitare la lettura del codice da parte degli altri sviluppatori, suggerirei la prima alternativa.


0

lock() return <expression> dichiarazioni sempre:

1) inserire il blocco

2) crea un archivio locale (thread-safe) per il valore del tipo specificato,

3) riempie il negozio con il valore restituito da <expression>,

4) blocco uscita

5) restituire il negozio.

Significa che il valore, restituito dall'istruzione lock, è sempre "cotto" prima della restituzione.

Non preoccuparti lock() return, non ascoltare nessuno qui))


-2

Nota: ritengo che questa risposta sia effettivamente corretta e spero che sia utile, ma sono sempre felice di migliorarla sulla base di feedback concreti.

Per riassumere e integrare le risposte esistenti:

  • La risposta accettata mostra che, indipendentemente dalla forma di sintassi scelta nel codice C # , nel codice IL - e quindi in fase di esecuzione - returnciò non accade fino a quando non viene rilasciato il blocco.

    • Anche se posizionare l' return interno del lockblocco quindi, in senso stretto, travisa il flusso di controllo [1] , è sintatticamente conveniente in quanto ovvia alla necessità di memorizzare il valore di ritorno in un aux. variabile locale (dichiarata all'esterno del blocco, in modo che possa essere utilizzata con un returnblocco esterno) - vedere la risposta di Edward KMETT .
  • Separatamente - e questo aspetto è secondario alla domanda, ma potrebbe ancora essere interessante ( la risposta di Ricardo Villamil cerca di affrontarla, ma penso erroneamente) - combinando lockun'istruzione con returnun'istruzione - vale a dire, ottenere valore returnin un blocco protetto da accesso concorrente - solo significato "protegge" il valore restituito al chiamante 's portata se non effettivamente bisogno di protezione una volta ottenuta , che si applica nei seguenti scenari:

    • Se il valore restituito è un elemento di una raccolta che necessita solo di protezione in termini di aggiunta e rimozione di elementi, non in termini di modifiche degli elementi stessi e / o ...

    • ... se il valore restituito è un'istanza di un tipo di valore o una stringa .

      • Si noti che in questo caso il chiamante riceve un'istantanea (copia) [2] del valore - che al momento dell'ispezione del chiamante potrebbe non essere più il valore corrente nella struttura dei dati di origine.
    • In ogni altro caso, il blocco deve essere eseguito dal chiamante , non (solo) all'interno del metodo.


[1] Theodor Zoulias osserva che questo è tecnicamente vero anche per l'immissione returnall'interno try, catch, using, if, while, for, ... istruzioni; tuttavia, lockè probabilmente lo scopo specifico dell'affermazione a invitare al controllo del vero flusso di controllo, come evidenziato da questa domanda che è stata posta e che ha ricevuto molta attenzione.

[2] L'accesso a un'istanza del tipo di valore crea invariabilmente una copia locale dello stack; anche se le stringhe sono istanze di tipo riferimento tecnicamente, si comportano in modo efficace istanze di tipo valore.


Per quanto riguarda lo stato attuale della tua risposta (revisione 13), stai ancora speculando sulle ragioni dell'esistenza del lock, e derivando significato dal posizionamento della dichiarazione di ritorno. Che è una discussione non correlata a questa domanda IMHO. Inoltre trovo abbastanza inquietante l'uso di "travisamenti" . Se ritorno da un locktravisa il flusso di controllo, poi lo stesso si può dire per ritorno da una try, catch, using, if, while, fore ogni altro costrutto del linguaggio. È come dire che il C # è pieno di false dichiarazioni sul flusso di controllo. Gesù ...
Theodor Zoulias,

"È come dire che il C # è pieno di false dichiarazioni sul flusso di controllo" - Beh, questo è tecnicamente vero, e il termine "travisamento" è solo un giudizio di valore se scegli di prenderlo in quel modo. Con try, if... personalmente non tendo nemmeno a pensarci, ma nel contesto lock, in particolare, la domanda è sorta per me - e se non fosse nata anche per gli altri, questa domanda non sarebbe mai stata posta e la risposta accettata non avrebbe fatto di tutto per indagare sul vero comportamento.
mklement0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.