Чи може «використання» більше ніж одного ресурсу спричинити витік ресурсу?


106

C # дозволяє мені зробити наступне (приклад з MSDN):

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

Що станеться, якщо font4 = new Fontкидає? З того, що я розумію, font3 просочиться ресурсами і не буде утилізований.

  • Це правда? (font4 не буде утилізований)
  • Чи using(... , ...)варто цього взагалі уникати на користь вкладеного вкладеного?

7
Це не просочиться пам'яттю; в гіршому випадку все одно вийде GC'd.
СЛАкс

3
Я не був би здивований, якщо using(... , ...)його скласти в вкладені за допомогою блоків незалежно, але я цього не знаю точно.
Dan J

1
Це я не мав на увазі. Навіть якщо ви взагалі не використовуєте using, GC все одно збиратиме його.
СЛАкс

1
@zneak: Якби він був складений до одного finallyблоку, він би не ввійшов до блоку, поки всі ресурси не були побудовані.
СЛАкс

2
@zneak: Оскільки при перетворенні a usingв a try- finallyвираження ініціалізації оцінюється за межами try. Тож це розумне питання.
Ben Voigt

Відповіді:


158

Немає.

Компілятор генерує окремий finallyблок для кожної змінної.

Специфікації (§8.13) говорить:

Коли придбання ресурсів має форму локальної змінної-декларації, можливо придбати кілька ресурсів заданого типу. usingзатвердження виду

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

точно еквівалентний послідовності вкладених з використанням операторів:

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

4
Це 8.13 у версії 5.0 для специфікації C #, btw.
Бен Войгт

11
@WeylandYutani: Що ви запитуєте?
СЛАкс

9
@WeylandYutani: Це веб-сайт із запитаннями та відповідями. Якщо у вас є питання, почніть нове запитання, будь ласка!
Ерік Ліпперт

5
@ user1306322 чому? Що робити, якщо я дійсно хочу знати?
Оксиморон

2
@ Oxymoron, тоді вам слід надати деякі докази зусиль, перш ніж надсилати запитання у формі досліджень та здогадок, інакше вам скажуть те саме, втратите увагу і в іншому випадку зазнайте більшої втрати. Просто порада, заснована на особистому досвіді.
user1306322

67

ОНОВЛЕННЯ : я використав це питання як основу для статті, яку можна знайти тут ; див. для додаткового обговорення цього питання. Дякую за гарне запитання!


Хоча відповідь Шабсе, безумовно, правильна і відповідає на запитання, яке було задано, у вашому питанні є важливий варіант, якого ви не задавали:

Що станеться, якщо font4 = new Font()викиди після того, як некерований ресурс був призначений конструктором, але до того, як ctor повернеться і заповнить font4посилання?

Дозвольте зробити це трохи більш зрозумілим. Припустимо, у нас є:

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

Зараз у нас є

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

Це те саме, що

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

ГАРАЗД. Припустимо, Whateverкидки. Потім finallyблок запускається і ресурс розміщується. Нема проблем.

Припустимо, Blah1()кидки. Тоді викид відбувається до виділення ресурсу. Об'єкт виділено, але ctor ніколи не повертається, тому fooніколи не заповнюється. Ми ніколи не входили tryтак, ми ніколи не вводимо finallyжодне. Посилання на об'єкт осиротіла. Врешті-решт, GC виявить це і поставить його до черги фіналізатора. handleдосі дорівнює нулю, тому фіналізатор нічого не робить. Зауважте, що фіналізатор повинен бути надійним перед об'єктом, який доопрацьовується, конструктор якого так і не завершився . Ви повинні написати фіналізатори, які є сильними. Це ще одна причина, чому ви повинні залишити писати фіналізатори експертам, а не намагатися зробити це самостійно.

Припустимо, Blah3()кидки. Кидок відбувається після виділення ресурсу. Але знову ж таки, fooніколи не заповнюється, ми ніколи не входимо вfinally в об'єкт, і об'єкт очищається потоком фіналізатора. Цього разу ручка не дорівнює нулю, і фіналізатор очищає її. Знову фіналізатор працює на об'єкті, конструктор якого ніколи не вдавався, але фіналізатор все одно працює. Очевидно, що це мусить, бо цього разу йому довелося попрацювати.

Тепер припустимо, Blah2()кидки. Кидок відбувається після виділення ресурсу, але до того, як handle буде заповнено! Знову фіналізатор запуститься, але зараз handleще нуль, і ми просочимо ручку!

Вам потрібно написати надзвичайно розумний код, щоб не допустити цього витоку. Тепер, що стосується вашого Fontресурсу, хто до біса піклується? Ми просочуємо ручку шрифту, велика справа. Але якщо ви абсолютно позитивно вимагаєте, щоб кожен некерований ресурс був очищений, незалежно від часу винятків тоді у вас дуже складна проблема.

CLR має вирішити цю проблему із замками. Оскільки C # 4, блоки, які використовують lockоператор, реалізовані так:

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

Enterбуло дуже ретельно написано так, що незалежно від того, які винятки викидаються , lockEnteredвстановлено значення "true", якщо і лише тоді, коли блокування було зроблено насправді. Якщо у вас є подібні вимоги, тоді вам потрібно написати:

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

і пишіть AllocateResourceрозумно Monitor.Enterтак, щоб незалежно від того, що відбувається всередині AllocateResource, handleзаповнюється, якщо і лише тоді, коли його потрібно буде розмістити.

Опис прийомів цього виходить за межі цієї відповіді. Якщо у вас є ця вимога, зверніться до експерта.


6
@gnat: прийнята відповідь. Що S має щось стояти. :-)
Ерік Ліпперт

12
@Joe: Звичайно, приклад надуманий . Я просто це надумав . Ризики не перебільшені, оскільки я не зазначив, який рівень ризику; швидше, я заявив, що така закономірність можлива . Те, що ви вважаєте, що встановлення поля безпосередньо вирішує проблему, вказує саме на мою думку: що, як і переважна більшість програмістів, які не мають досвіду подібної проблеми, ви не компетентні вирішувати цю проблему; Дійсно, більшість людей навіть не визнають, що є проблема, саме тому я написав цю відповідь в першу чергу .
Ерік Ліпперт

5
@Chris: Припустимо, між розподілом і поверненням, між поверненням і призначенням, виконано нульову роботу. Ми видаляємо всі ці Blahвиклики методу. Що заважає ThreadAbortException не відбуватися в будь-якій з цих точок?
Ерік Ліпперт

5
@Joe: Це не спірне суспільство; Я не прагну набрати очки, будучи більш переконливим . Якщо ви скептично налаштовані і не хочете приймати моє слово за це, що це складна проблема, яка вимагає консультації з експертами, щоб правильно вирішити, тоді ви можете не погодитися зі мною.
Ерік Ліпперт

7
@GilesRoberts: Як це вирішує проблему? Припустимо, виняток трапляється після виклику до, AllocateResourceале перед призначенням x. А ThreadAbortExceptionможе статися в цей момент. Кожен тут, здається, не вистачає моєї точки зору, а це створення ресурсу та присвоєння посилання на нього змінної - це не атомна операція . Для вирішення проблеми, яку я визначив, ви повинні зробити це атомною операцією.
Ерік Ліпперт

32

Як додаток до відповіді @SLaks, ось ІЛ для вашого коду:

.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

Зверніть увагу на вкладені блоки спробуйте / нарешті.


17

Цей код (заснований на оригінальній вибірці):

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

Він створює наступний CILVisual Studio 2013 , орієнтований на .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

Як бачите, try {}блок не починається до першого розподілу, який відбувається в IL_0012. На перший погляд, це, здається, виділяє перший елемент у незахищеному коді. Однак зауважте, що результат зберігається у розташуванні 0. Якщо другий розподіл потім не вдається, зовнішній finally {} блок виконується, і він отримує об'єкт з місця 0, тобто першого розподілу font3, і викликає його Dispose()метод.

Цікаво, що декомпіляція цієї збірки за допомогою dotPeek створює наступне відновлене джерело:

using System.Drawing;

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

Декомпільований код підтверджує, що все правильно і що по usingсуті розширено на вкладені usings. Код CIL трохи заплутано дивитись, і мені довелося дивитись на нього добрі кілька хвилин, перш ніж я правильно зрозумів, що відбувається, тому я не здивований, що деякі «казки про старих дружин» почали проростати це. Однак згенерований код - це правда, яка недоступна.


@Peter Mortensen ваша редакція видалила фрагменти коду IL (між IL_0012 та IL_0017), зробивши пояснення недійсним і заплутаним. Цей код повинен був бути дослівною копією отриманих результатів та редагуванням, що це визнає недійсним. Чи можете ви, будь ласка, переглянути свою редакцію та підтвердити це те, що ви планували?
Тім Лонг

7

Ось зразок коду для підтвердження відповіді @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);
    }
}

1
Це не доводить. Де знаходиться утилізація: t2? :)
Пьотр Перак

1
Питання полягає у розпорядженні першого ресурсу зі списку використання не другого. "Що станеться, якщо font4 = new Fontкидки? З того, наскільки я розумію, шрифт3 просочиться ресурсами і не буде утилізований."
wdosanjos
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.