O “uso” com mais de um recurso pode causar vazamento de recursos?

106

C # me permite fazer o seguinte (exemplo do MSDN):

using (Font font3 = new Font("Arial", 10.0f),
            font4 = new Font("Arial", 10.0f))
{
    // Use font3 and font4.
}

O que acontece se font4 = new Fontjogar? Pelo que entendi, o font3 vazará recursos e não será descartado.

  • Isso é verdade? (font4 não será eliminado)
  • Isso significa que using(... , ...)deve ser evitado completamente em favor do uso aninhado?
Benjamin Gruenbaum
fonte
7
Não vai vazar memória; na pior das hipóteses, ainda será submetido a GC.
SLaks
3
Eu não ficaria surpreso se using(... , ...)fosse compilado em blocos aninhados de qualquer maneira, mas não tenho certeza disso.
Dan J
1
Isso não foi o que eu quis dizer. Mesmo se você não usar usingnada, o GC ainda irá coletá-lo.
SLaks
1
@zneak: Se tivesse compilado em um único finallybloco, não teria entrado no bloco até que todos os recursos fossem construídos.
SLaks
2
@zneak: Porque na conversão de a usingpara a try- finally, a expressão de inicialização é avaliada fora do try. Portanto, é uma questão razoável.
Ben Voigt

Respostas:

158

Não.

O compilador irá gerar um finallybloco separado para cada variável.

A especificação (§8.13) diz:

Quando uma aquisição de recurso assume a forma de uma declaração de variável local, é possível adquirir vários recursos de um determinado tipo. Uma usingdeclaração do formulário

using (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement 

é precisamente equivalente a uma sequência de instruções aninhadas usando:

using (ResourceType r1 = e1)
   using (ResourceType r2 = e2)
      ...
         using (ResourceType rN = eN)
            statement
SLaks
fonte
4
Isso é 8.13 na especificação C # versão 5.0, btw.
Ben Voigt
11
@WeylandYutani: O que você está perguntando?
SLaks
9
@WeylandYutani: Este é um site de perguntas e respostas. Se você tiver uma pergunta, comece uma nova pergunta, por favor!
Eric Lippert
5
@ user1306322 por quê? E se eu realmente quiser saber?
Oxímoro de
2
@Oxymoron então você deve fornecer alguma evidência de esforço antes de postar a questão em forma de pesquisa e suposições, ou então você ouvirá o mesmo, perderá a atenção e ficará ainda mais perdido. Apenas um conselho baseado na experiência pessoal.
user1306322
67

ATUALIZAÇÃO : Usei essa pergunta como base para um artigo que pode ser encontrado aqui ; veja-o para uma discussão adicional deste assunto. Obrigado pela boa pergunta!


Embora a resposta de Schabse esteja correta e responda à pergunta que foi feita, há uma variante importante da sua pergunta que você não perguntou:

O que acontece se font4 = new Font()throws depois que o recurso não gerenciado foi alocado pelo construtor, mas antes que o ctor retorne e preencha font4com a referência?

Deixe-me deixar isso um pouco mais claro. Suponha que temos:

public sealed class Foo : IDisposable
{
    private int handle = 0;
    private bool disposed = false;
    public Foo()
    {
        Blah1();
        int x = AllocateResource();
        Blah2();
        this.handle = x;
        Blah3();
    }
    ~Foo()
    {
        Dispose(false);
    }
    public void Dispose() 
    { 
        Dispose(true); 
        GC.SuppressFinalize(this);
    }
    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (this.handle != 0) 
                DeallocateResource(this.handle);
            this.handle = 0;
            this.disposed = true;
        }
    }
}

Agora temos

using(Foo foo = new Foo())
    Whatever(foo);

Este é o mesmo que

{
    Foo foo = new Foo();
    try
    {
        Whatever(foo);
    }
    finally
    {
        IDisposable d = foo as IDisposable;
        if (d != null) 
            d.Dispose();
    }
}

ESTÁ BEM. Suponha que Whateverjogue. Em seguida, o finallybloco é executado e o recurso é desalocado. Sem problemas.

Suponha que Blah1()jogue. Então, o lançamento acontece antes que o recurso seja alocado. O objeto foi alocado, mas o ctor nunca retorna, então foonunca é preenchido. Nunca inserimos o, tryportanto, nunca inserimos o finallytambém. A referência do objeto ficou órfã. Eventualmente, o GC descobrirá isso e o colocará na fila do finalizador. handleainda é zero, então o finalizador não faz nada. Observe que o finalizador deve ser robusto diante de um objeto que está sendo finalizado e cujo construtor nunca foi concluído . Você está exigido para escrever finalizadores que são tão forte. Essa é outra razão pela qual você deve deixar os finalizadores de redação para especialistas e não tentar fazer você mesmo.

Suponha que Blah3()jogue. O lançamento acontece depois que o recurso é alocado. Mas, novamente, foonunca é preenchido, nunca inserimos o finallye o objeto é limpo pelo encadeamento do finalizador. Desta vez, o identificador é diferente de zero e o finalizador o limpa. Novamente, o finalizador está sendo executado em um objeto cujo construtor nunca foi bem-sucedido, mas o finalizador é executado de qualquer maneira. Obviamente que sim, porque desta vez, tinha trabalho a fazer.

Agora suponha que Blah2()jogue. O lançamento acontece depois que o recurso é alocado, mas antes de handle ser preenchido! Novamente, o finalizador será executado, mas agora handleainda é zero e vazamos o identificador!

Você precisa escrever um código extremamente inteligente para evitar que esse vazamento aconteça. Agora, no caso do seu Fontrecurso, quem se importa? Vazamos um identificador de fonte, grande coisa. Mas se você exige de forma absolutamente positiva que todos os recursos não gerenciados sejam limpos, não importa o momento das exceções , você tem um problema muito difícil em mãos.

O CLR tem que resolver esse problema com travas. Desde C # 4, os bloqueios que usam a lockinstrução foram implementados assim:

bool lockEntered = false;
object lockObject = whatever;
try
{
    Monitor.Enter(lockObject, ref lockEntered);
    lock body here
}
finally
{
    if (lockEntered) Monitor.Exit(lockObject);
}

Enterfoi escrito com muito cuidado para que, independentemente das exceções lançadas , lockEnteredseja definido como verdadeiro se e somente se o bloqueio foi realmente executado. Se você tiver requisitos semelhantes, o que você precisa é realmente escrever:

    public Foo()
    {
        Blah1();
        AllocateResource(ref handle);
        Blah2();
        Blah3();
    }

e escreva AllocateResourcehabilmente Monitor.Enterassim, não importa o que aconteça dentro AllocateResource, o handleseja preenchido se e somente se precisar ser desalocado.

Descrever as técnicas para fazer isso está além do escopo desta resposta. Consulte um especialista se você tiver esse requisito.

Eric Lippert
fonte
6
@gnat: A resposta aceita. Esse S tem que representar algo. :-)
Eric Lippert
12
@Joe: Claro que o exemplo é inventado . Eu apenas planejei . Os riscos não são exagerados porque não declarei qual é o nível de risco; em vez disso, declarei que esse padrão é possível . O fato de você acreditar que definir o campo resolve diretamente o problema indica exatamente o meu ponto: que, como a grande maioria dos programadores que não tem experiência com esse tipo de problema, você não é competente para resolvê-lo; na verdade, a maioria das pessoas nem sequer reconhecer que não é um problema, que é por isso que escrevi essa resposta em primeiro lugar .
Eric Lippert de
5
@Chris: Suponha que não haja trabalho realizado entre a alocação e o retorno e entre o retorno e a atribuição. Excluímos todas essas Blahchamadas de método. O que impede que ThreadAbortException aconteça em qualquer um desses pontos?
Eric Lippert
5
@Joe: Esta não é uma sociedade de debates; Não procuro marcar pontos sendo mais convincente . Se você está cético e não quer acreditar na minha palavra de que este é um problema complicado que requer consulta com especialistas para ser resolvido corretamente, então você pode discordar de mim.
Eric Lippert de
7
@GilesRoberts: Como isso resolve o problema? Suponha que a exceção aconteça após a chamada para, AllocateResourcemas antes da atribuição para x. A ThreadAbortExceptionpode acontecer nesse ponto. Todo mundo aqui parece estar perdendo o meu ponto, que é a criação de um recurso e a atribuição de uma referência a ele a uma variável não é uma operação atômica . Para resolver o problema que identifiquei, você deve torná-lo uma operação atômica.
Eric Lippert
32

Como um complemento à resposta de @SLaks, aqui está o IL para seu código:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 74 (0x4a)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] class [System.Drawing]System.Drawing.Font font3,
        [1] class [System.Drawing]System.Drawing.Font font4,
        [2] bool CS$4$0000
    )

    IL_0000: nop
    IL_0001: ldstr "Arial"
    IL_0006: ldc.r4 10
    IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
    IL_0010: stloc.0
    .try
    {
        IL_0011: ldstr "Arial"
        IL_0016: ldc.r4 10
        IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
        IL_0020: stloc.1
        .try
        {
            IL_0021: nop
            IL_0022: nop
            IL_0023: leave.s IL_0035
        } // end .try
        finally
        {
            IL_0025: ldloc.1
            IL_0026: ldnull
            IL_0027: ceq
            IL_0029: stloc.2
            IL_002a: ldloc.2
            IL_002b: brtrue.s IL_0034

            IL_002d: ldloc.1
            IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
            IL_0033: nop

            IL_0034: endfinally
        } // end handler

        IL_0035: nop
        IL_0036: leave.s IL_0048
    } // end .try
    finally
    {
        IL_0038: ldloc.0
        IL_0039: ldnull
        IL_003a: ceq
        IL_003c: stloc.2
        IL_003d: ldloc.2
        IL_003e: brtrue.s IL_0047

        IL_0040: ldloc.0
        IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_0046: nop

        IL_0047: endfinally
    } // end handler

    IL_0048: nop
    IL_0049: ret
} // end of method Program::Main

Observe os blocos aninhados try / finally.

David Heffernan
fonte
17

Este código (baseado na amostra original):

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (Font font3 = new Font("Arial", 10.0f),
                    font4 = new Font("Arial", 10.0f))
        {
            // Use font3 and font4.
        }
    }
}

Ele produz o seguinte CIL (no Visual Studio 2013 , visando .NET 4.5.1):

.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
    // Code size       82 (0x52)
    .maxstack  2
    .locals init ([0] class [System.Drawing]System.Drawing.Font font3,
                  [1] class [System.Drawing]System.Drawing.Font font4,
                  [2] bool CS$4$0000)
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  nop
    IL_0008:  ldstr      "Arial"
    IL_000d:  ldc.r4     10.
    IL_0012:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                  float32)
    IL_0017:  stloc.0
    .try
    {
        IL_0018:  ldstr      "Arial"
        IL_001d:  ldc.r4     10.
        IL_0022:  newobj     instance void [System.Drawing]System.Drawing.Font::.ctor(string,
                                                                                      float32)
        IL_0027:  stloc.1
        .try
        {
            IL_0028:  nop
            IL_0029:  nop
            IL_002a:  leave.s    IL_003c
        }  // end .try
        finally
        {
            IL_002c:  ldloc.1
            IL_002d:  ldnull
            IL_002e:  ceq
            IL_0030:  stloc.2
            IL_0031:  ldloc.2
            IL_0032:  brtrue.s   IL_003b
            IL_0034:  ldloc.1
            IL_0035:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
            IL_003a:  nop
            IL_003b:  endfinally
        }  // end handler
        IL_003c:  nop
        IL_003d:  leave.s    IL_004f
    }  // end .try
    finally
    {
        IL_003f:  ldloc.0
        IL_0040:  ldnull
        IL_0041:  ceq
        IL_0043:  stloc.2
        IL_0044:  ldloc.2
        IL_0045:  brtrue.s   IL_004e
        IL_0047:  ldloc.0
        IL_0048:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
        IL_004d:  nop
        IL_004e:  endfinally
    }  // end handler
    IL_004f:  nop
    IL_0050:  nop
    IL_0051:  ret
} // end of method Class1::.ctor

Como você pode ver, o try {}bloco não começa antes da primeira alocação, que ocorre em IL_0012. À primeira vista, isso parece alocar o primeiro item no código desprotegido. No entanto, observe que o resultado é armazenado na localização 0. Se a segunda alocação falhar, o bloco externo é finally {} executado e busca o objeto na localização 0, ou seja, a primeira alocação de font3, e chama seu Dispose()método.

Curiosamente, a descompilação desse conjunto com dotPeek produz a seguinte fonte reconstituída:

using System.Drawing;

public class Class1
{
    public Class1()
    {
        using (new Font("Arial", 10f))
        {
            using (new Font("Arial", 10f))
                ;
        }
    }
}

O código descompilado confirma que tudo está correto e que o usingé essencialmente expandido em usings aninhados . O código CIL é um pouco confuso de se olhar, e eu tive que ficar olhando para ele por alguns minutos antes de entender corretamente o que estava acontecendo, então não estou surpreso que alguns 'contos de esposas antigas' começaram a surgir sobre isto. No entanto, o código gerado é a verdade inatacável.

Tim Long
fonte
@Peter Mortensen, sua edição removeu pedaços do código IL (entre IL_0012 e IL_0017), tornando a explicação inválida e confusa. Esse código foi concebido para ser uma cópia literal dos resultados que obtive e a edição invalida isso. Você pode revisar sua edição e confirmar se era isso que você pretendia?
Tim Long
7

Aqui está um código de amostra para provar a resposta do @SLaks:

void Main()
{
    try
    {
        using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
        {
        }
    }
    catch(Exception ex)
    {
        Console.WriteLine("catch");
    }
    finally
    {
        Console.WriteLine("done");
    }

    /* outputs

        Construct: t1
        Construct: t2
        Dispose: t1
        catch
        done

    */
}

public class TestUsing : IDisposable
{
    public string Name {get; set;}

    public TestUsing(string name)
    {
        Name = name;

        Console.WriteLine("Construct: " + Name);

        if (Name == "t2") throw new Exception();
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose: " + Name);
    }
}
wdosanjos
fonte
1
Isso não prova isso. Onde está Dispose: t2? :)
Piotr Perak de
1
A questão é sobre o descarte do primeiro recurso na lista de uso, não o segundo. "O que acontece se font4 = new Fontlançar? Pelo que entendi, o font3 vazará recursos e não será descartado."
wdosanjos de