Uma declaração de retorno deve estar dentro ou fora de uma fechadura?

142

Acabei de perceber que, em algum lugar do meu código, tenho a declaração de retorno dentro da fechadura e algumas vezes fora. Qual é o melhor?

1)

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

2)

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

}

Qual devo usar?

Patrick Desjardins
fonte
Que tal disparar Refletor e fazer alguma comparação de IL ;-).
Pop Catalin
6
@Pop: feito - nem é melhor em termos IL - somente C # estilo aplica
Marc Gravell
1
Muito interessante, uau, eu aprendo algo hoje!
Pokus

Respostas:

192

Essencialmente, o que torna o código mais simples. Um único ponto de saída é um ótimo ideal, mas eu não alteraria o código apenas para alcançá-lo ... E se a alternativa estiver declarando uma variável local (fora do bloqueio), inicializando-o (dentro do bloqueio) e depois devolvê-lo (fora da fechadura), então eu diria que um simples "retorno" dentro da fechadura é muito mais simples.

Para mostrar a diferença na IL, vamos codificar:

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

(note que felizmente argumentaria que ReturnInsideé um bit mais simples / limpo de C #)

E olhe para o IL (modo de lançamento etc):

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

Então, no nível de IL, eles são [dar ou receber alguns nomes] idênticos (eu aprendi alguma coisa ;-p). Como tal, a única comparação sensata é a lei (altamente subjetiva) do estilo de codificação local ... Prefiro ReturnInsidea simplicidade, mas também não me empolguei.

Marc Gravell
fonte
15
Usei o (gratuito e excelente) .NET Reflector do Red Gate (era: o .NET Reflector de Lutz Roeder), mas o ILDASM também o faria.
Marc Gravell
1
Um dos aspectos mais poderosos do Reflector é que você pode realmente desmontar a IL para o seu idioma preferido (C #, VB, Delphi, MC ++, Chrome, etc.)
Marc Gravell
3
Para o seu exemplo simples, o IL permanece o mesmo, mas provavelmente é porque você retorna apenas um valor constante ?! Acredito que, para cenários da vida real, o resultado pode ser diferente, e threads paralelos podem causar problemas um ao outro, modificando o valor antes de retorná-lo, a instrução de retorno está fora do bloco de bloqueio. Perigoso!
Torbjørn
@ MarcGravell: Acabei de me deparar com sua postagem enquanto pondera a mesma e mesmo depois de ler sua resposta, ainda não tenho certeza sobre o seguinte: Existem ALGUMAS circunstâncias em que o uso da abordagem externa pode quebrar a lógica de segurança de threads. Eu pergunto isso, já que prefiro um único ponto de retorno e não me sinto muito bem com a segurança de threads. Embora, se o IL é o mesmo, minha preocupação deve ser discutida de qualquer maneira.
Raheel Khan
1
@RaheelKhan não, nenhum; eles são os mesmos. No nível IL, você não pode ret dentro de uma .tryregião.
Marc Gravell
42

Não faz diferença; ambos são traduzidos para a mesma coisa pelo compilador.

Para esclarecer, qualquer um é efetivamente traduzido para algo com a seguinte semântica:

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

return myData;
Greg Beech
fonte
1
Bem, isso é verdade para o try / finally - no entanto, o retorno fora do bloqueio ainda requer locals extras que não podem ser otimizados de distância - e leva mais de código ...
Marc Gravell
3
Você não pode retornar de um bloco try; deve terminar com um código operacional ".leave". Portanto, o CIL emitido deve ser o mesmo em ambos os casos.
Greg Beech
3
Você está certo - acabei de olhar para o IL (ver post atualizado). Eu aprendi algo ;-p
Marc Gravell
2
Cool, infelizmente, eu aprendi com horas dolorosas tentando emitem .ret op-códigos em blocos try e tendo o lixo CLR para carregar meus métodos dinâmicos :-(
Greg Beech
Eu posso relacionar; Já fiz bastante Reflection.Emit, mas sou preguiçoso; a menos que eu tenha muita certeza de alguma coisa, escrevo código representativo em C # e depois olho para o IL. Mas é surpreendente a rapidez com que você começa a pensar em termos de IL (por exemplo, sequenciar a pilha).
Marc Gravell
28

Eu definitivamente colocaria o retorno dentro da fechadura. Caso contrário, você corre o risco de outro encadeamento entrar no bloqueio e modificar sua variável antes da instrução de retorno, fazendo com que o chamador original receba um valor diferente do esperado.

Ricardo Villamil
fonte
4
Isso está correto, um ponto em que os outros respondentes parecem estar ausentes. As amostras simples que eles criaram podem produzir a mesma IL, mas isso não ocorre na maioria dos cenários da vida real.
Torbjørn
4
Estou surpreso que as outras respostas não falem sobre isso
Akshat Agarwal
5
Neste exemplo, eles estão falando sobre o uso de uma variável de pilha para armazenar o valor de retorno, ou seja, apenas a declaração de retorno fora do bloqueio e, claro, a declaração da variável. Outro segmento deve ter outra pilha e, portanto, não poderia causar nenhum dano, estou certo?
Guillermo Ruffino 29/02
3
Não acho que esse seja um ponto válido, pois outro encadeamento pode atualizar o valor entre a chamada de retorno e a atribuição real do valor de retorno à variável no encadeamento principal. O valor que está sendo retornado não pode ser alterado ou ter consistência garantida com o valor real atual de qualquer maneira. Certo?
Uroš Joksimović
Esta resposta está incorreta. Outro thread não pode alterar uma variável local. Variáveis ​​locais são armazenadas na pilha e cada encadeamento possui sua própria pilha. Entre o tamanho padrão da pilha de um thread é de 1 MB .
Theodor Zoulias
5

Depende,

Eu vou contra a corrente aqui. Eu geralmente voltaria para dentro da fechadura.

Normalmente, a variável mydata é uma variável local. Gosto de declarar variáveis ​​locais enquanto as inicializo. Eu raramente tenho os dados para inicializar meu valor de retorno fora do meu bloqueio.

Portanto, sua comparação é realmente falha. Embora, idealmente, a diferença entre as duas opções seja a que você escreveu, o que parece indicar o caso 1, na prática é um pouco mais feio.

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

vs.

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

Acho que o caso 2 é consideravelmente mais fácil de ler e mais difícil de estragar, especialmente para trechos curtos.

Edward KMETT
fonte
4

Se o bloqueio externo parecer melhor, mas tome cuidado se você alterar o código para:

return f(...)

Se f () precisar ser chamado com a trava mantida, obviamente ela deverá estar dentro da trava, pois assim, manter retornos dentro da trava para obter consistência faz sentido.

Rob Walker
fonte
1

Quanto vale a pena, a documentação no MSDN tem um exemplo de retorno de dentro do bloqueio. A partir das outras respostas aqui, parece ser uma IL muito semelhante, mas, para mim, parece mais seguro retornar de dentro do bloqueio, porque você não corre o risco de uma variável de retorno ser substituída por outro encadeamento.

greyseal96
fonte
0

Para facilitar a leitura do código por outros desenvolvedores, sugiro a primeira alternativa.

Adam Asham
fonte
0

lock() return <expression> declarações sempre:

1) digite bloqueio

2) torna o armazenamento local (seguro para threads) para o valor do tipo especificado,

3) preenche a loja com o valor retornado por <expression>,

4) trava de saída

5) devolva a loja.

Isso significa que o valor, retornado da instrução lock, sempre "cozinhado" antes do retorno.

Não se preocupe lock() return, não ouça ninguém aqui))

mshakurov
fonte
-2

Nota: Acredito que esta resposta seja factualmente correta e espero que seja útil também, mas estou sempre feliz em melhorá-la com base em comentários concretos.

Para resumir e complementar as respostas existentes:

  • A resposta aceita mostra que, independentemente da forma de sintaxe que você escolher no código C # , no código IL - e, portanto, no tempo de execução -, returnisso não acontecerá até depois que o bloqueio for liberado.

    • Mesmo que a colocação do return interior do lockbloco represente incorretamente o fluxo de controle [1] , é sintaticamente conveniente , uma vez que evita a necessidade de armazenar o valor de retorno em um aux. variável local (declarada fora do bloco, para que possa ser usada com uma returnfora do bloco) - veja a resposta de Edward KMETT .
  • Separadamente - e esse aspecto é incidental à pergunta, mas ainda pode ser interessante ( a resposta de Ricardo Villamil tenta abordá-la, mas incorretamente, eu acho) - combinando uma lockafirmação com uma returnafirmação - ou seja, obtendo valor returnem um bloco protegido de acesso simultâneo - apenas "protege" significativamente o valor retornado no escopo do chamador se ele não precisar de proteção uma vez obtido , o que se aplica nos seguintes cenários:

    • Se o valor retornado for um elemento de uma coleção que precisa apenas de proteção em termos de adição e remoção de elementos, não em termos de modificações dos próprios elementos e / ou ...

    • ... se o valor que está sendo retornado for uma instância de um tipo ou sequência de valores .

      • Observe que, nesse caso, o chamador recebe um instantâneo (cópia) [2] do valor - que, no momento em que o chamador inspeciona, pode não ser mais o valor atual na estrutura de dados de origem.
    • Em qualquer outro caso, o bloqueio deve ser realizado pelo chamador , não (apenas) dentro do método.


[1] Theodor Zoulias aponta que isso é tecnicamente também é verdade para a colocação returndentro try, catch, using, if, while, for, ... declarações; no entanto, é o objetivo específico da lockdeclaração que provavelmente convidará ao exame do verdadeiro fluxo de controle, conforme evidenciado por essa pergunta ter sido feita e ter recebido muita atenção.

[2] O acesso a uma instância de tipo de valor invariavelmente cria uma cópia local da thread e na pilha; mesmo que as strings sejam tecnicamente instâncias do tipo referência, elas se comportam efetivamente com instâncias do mesmo valor.

mklement0
fonte
Com relação ao estado atual da sua resposta (revisão 13), você ainda está especulando sobre os motivos da existência do lock, e derivando significado do posicionamento da declaração de retorno. Qual é uma discussão não relacionada a esta pergunta IMHO. Também acho o uso de "deturpa" bastante perturbador. Se retornar de uma lockdeturpa o fluxo de controle, em seguida, o mesmo poderia ser dito para retornar de uma try, catch, using, if, while, for, e qualquer outra construção da linguagem. É como dizer que o C # está cheio de deturpações de fluxo de controle. Jesus ...
Theodor Zoulias
"É como dizer que o C # está repleto de deturpações de fluxo de controle" - Bem, isso é tecnicamente verdadeiro, e o termo "deturpação" é apenas um julgamento de valor se você optar por fazê-lo dessa maneira. Com try, if... Eu, pessoalmente, não tendem a sequer pensar nisso, mas no contexto de lock, especificamente, surgiu a questão para mim - e se ele não surgiu também para os outros, esta questão nunca teria sido solicitado , e a resposta aceita não teria se esforçado muito para investigar o verdadeiro comportamento.
mklement0