Як запобігти поширенню IDisposable на всі ваші заняття?


138

Почніть з цих простих занять ...

Скажімо, у мене простий набір таких класів:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A Busмає a Driver, Driverмає два Shoes, кожен Shoeмає a Shoelace. Все дуже нерозумно.

Додайте об’єкт, який не використовується, в Shoelace

Пізніше я вирішую, що деяка операція над програмою Shoelaceможе бути багатопотоковою, тому я додаю EventWaitHandleпотоки для спілкування з ними. Отже, Shoelaceтепер виглядає так:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

Реалізуйте IDisposable на шнурці

Але тепер FxCop Microsoft поскаржиться: "Впровадьте IDisposable на" Shoelace ", оскільки він створює членів таких типів IDisposable:" EventWaitHandle "."

Гаразд, я впроваджую IDisposableдалі, Shoelaceі мій класний маленький клас стає цим жахливим безладом:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

Або (як наголошують коментатори), оскільки Shoelaceсам по собі не має керованих ресурсів, я міг би скористатися більш простою реалізацією розпорядження, не потребуючи Dispose(bool)і Destructor:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

Дивіться з жахом, як розповсюджується ID

Правильно, це виправлено. Але тепер FxCop поскаржиться на те, що Shoeстворює Shoelace, так Shoeмає бути IDisposableтеж.

І Driverтворити Shoeтак Driverтреба IDisposable. І Busтворити Driverтак Busтреба IDisposableі так далі.

Раптом моя невелика зміна на Shoelaceвикликає у мене багато роботи, і мій начальник цікавиться, чому мені потрібно оформити замовлення, Busщоб зробити зміни Shoelace.

Питання

Як ви запобігаєте цьому розповсюдженню IDisposable, але все ж забезпечуєте правильне розміщення ваших некерованих об'єктів?


9
Надзвичайно вдале запитання, я вважаю, що відповідь полягає в тому, щоб мінімізувати їхнє використання та намагатися утримати короткий термін використання IDSposables високого рівня, але це не завжди можливо (особливо, коли ці IDisposables обумовлені взаємодією з C ++ dlls або подібними). Подивіться на відповіді.
annakata

Добре Ден, я оновив питання, щоб показати обидва способи впровадження IDisposable на Shoelace.
GrahamS

Я зазвичай насторожено покладаюся на деталі реалізації інших класів, щоб захистити мене. Немає сенсу ризикувати, якщо я можу це легко запобігти. Можливо, я надто обережний або, можливо, я просто занадто довго провів програміст на С, але я скоріше
ставлюся

@Dan: Перевірка нуля все ще потрібна для того, щоб переконатися, що сам об’єкт не був встановлений на нуль, і в цьому випадку виклик waitHandle.Dispose () видасть NullReferenceException.
Скотт Дорман

1
Незважаючи ні на що, ви все-таки повинні використовувати метод Dispose (bool), як показано в розділі "Внести IDisposable на Shoelace", оскільки це (мінус фіналізатор) є повним зразком. Тільки тому, що клас реалізує IDisposable, не означає, що йому потрібен фіналізатор.
Скотт Дорман

Відповіді:


36

Ви дійсно не можете "запобігти" розповсюдженню ідентифікаторів. Деякі класи повинні бути розміщені, як AutoResetEvent, і найефективніший спосіб - це зробити Dispose()методом, щоб уникнути накладних витрат фіналізаторів. Але цей метод слід називати якось так, як саме у вашому прикладі класи, які інкапсулюють або містять IDisposable, мають розпоряджатися цими, тому вони також мають бути одноразовими тощо. Єдиний спосіб уникнути цього - це:

  • уникайте використання ідентифікаційних класів, де це можливо, блокуйте або чекайте подій в одних місцях, зберігайте дорогі ресурси в одному місці тощо
  • створюйте їх лише тоді, коли вам знадобляться, і розпоряджайтесь ними відразу після ( usingвізерунок)

В деяких випадках IDisposable можна ігнорувати, оскільки він підтримує необов'язковий випадок. Наприклад, WaitHandle реалізує IDisposable для підтримки названого Mutex. Якщо ім’я не використовується, метод Dispose нічого не робить. MemoryStream - ще один приклад, він не використовує системних ресурсів, і його реалізація Dispose також нічого не робить. Ретельне розмірковування про те, чи використовується некерований ресурс, чи не може бути повчальним. Так можна вивчити доступні джерела для бібліотек .net або використовувати декомпілятор.


4
Порада, яку ви можете проігнорувати, оскільки сьогоднішня реалізація нічого не робить, погана, оскільки майбутня версія насправді може зробити щось важливе в Dispose, і тепер у вас складно відстежити витік.
Енді

1
"тримайте дорогі ресурси", IDisposable не обов'язково дорогий, адже цей приклад, який використовує ручку рівного очікування, стосується "легкої ваги", як ви отримаєте.
markmnl

20

З точки зору правильності, ви не можете запобігти поширенню IDisposable через об'єктне відношення, якщо батьківський об'єкт створює і по суті є дочірнім об'єктом, який тепер повинен бути одноразовим. FxCop правильний у цій ситуації, і батько повинен бути ідентифікованим.

Що ви можете зробити, це уникати додавання ідентифікатора до класу листів у вашій ієрархії об’єктів. Це не завжди легке завдання, але це цікава вправа. З логічної точки зору, немає причин, що ShoeLace потрібно одноразово використовувати. Замість того, щоб сюди додати WaitHandle, чи можна також додати асоціацію між ShoeLace та WaitHandle у точці, яку він використовує. Найпростіший спосіб - через екземпляр словника.

Якщо ви можете перемістити WaitHandle в нещільну асоціацію через карту в точці, де фактично використовується WaitHandle, ви можете розірвати цей ланцюжок.


2
Дуже дивно робити асоціацію там. Цей AutoResetEvent є приватним для реалізації Shoelace, тому, на мою думку, викривати його публічно було б неправильним.
GrahamS

@GrahamS, я не кажу, що це відкрито. Я говорю, чи можна її перенести до того місця, коли пов’язують шнурки. Можливо, якщо зав'язування шнурок взуття є такою хитромудрою задачею, вони повинні бути класом шнурочного взуття.
JaredPar

1
Чи можете ви розширити свою відповідь деякими фрагментами коду JaredPar? Можливо, я трохи розтягую свій приклад, але я уявляю, що Shoelace створює та запускає нитку Tyer, яка терпляче чекає на waitHandle.WaitOne () Шнурок закликає waitHandle.Set (), коли хотіла, щоб нитка почала зав'язувати.
GrahamS

1
+1 про це. Я думаю, було б дивно, що просто Шнурок називатиметься одночасно, а не іншими. Подумайте, яким повинен бути сукупний корінь. У цьому випадку це має бути Bus, IMHO, хоча я не знайомий з доменом. Отже, у цьому випадку шина повинна містити чеканку і всі операції з шиною, і всі діти будуть синхронізовані.
Йоганнес Густафссон

16

Щоб запобігти IDisposableпоширенню, слід спробувати інкапсулювати використання одноразового предмета всередині одного методу. Спробуйте спроектувати Shoelaceінакше:

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

Обсяг ручки очікування обмежений Tieметодом, і класу не потрібно мати поле для одноразового використання, і тому не потрібно буде самостійно користуватися.

Оскільки ручка очікування є деталлю впровадження всередині Shoelace, вона не повинна жодним чином змінювати свій публічний інтерфейс, як-от додавання нового інтерфейсу в декларацію. Що буде тоді, коли вам більше не потрібно одноразове поле, ви видалите IDisposableдекларацію? Якщо ви думаєте про Shoelace абстракцію , ви швидко розумієте, що вона не повинна забруднюватися інфраструктурними залежностями, наприклад IDisposable. IDisposableповинні бути зарезервовані для класів, абстракція яких інкапсулює ресурс, який вимагає детермінованого очищення; тобто для класів, де одноразовість є частиною абстракції .


1
Я повністю погоджуюся, що деталізація реалізації Shoelace не повинна забруднювати загальнодоступний інтерфейс, але моя думка полягає в тому, що цього можна важко уникнути. Те, що ви пропонуєте тут, не завжди було б можливим: мета AutoResetEvent () - це зв'язок між потоками, тому його сфера дії зазвичай виходить за межі одного методу.
GrahamS

@GrahamS: ось чому я сказав спробувати спроектувати його таким чином. Ви також можете передати одноразові інші методи. Він руйнується лише тоді, коли зовнішні дзвінки до класу контролюють життєвий цикл одноразового використання. Відповідь я оновлю відповідно.
Джордао

2
Вибачте, я знаю, що ви можете передавати одноразові, але я все ще не бачу, що це працює. У моєму прикладі AutoResetEventформа використовується для зв'язку між різними потоками, які працюють в межах одного класу, тому вона повинна бути змінною члена. Ви не можете обмежити його сферу застосування методом. (наприклад, уявіть, що один потік просто сидить, чекаючи певної роботи, блокуючи waitHandle.WaitOne(). Головний потік викликає shoelace.Tie()метод, який просто робить a waitHandle.Set()і повертається негайно).
GrahamS

3

Це в основному те, що відбувається, коли ви змішуєте склад або агрегацію з класами одноразового використання. Як вже згадувалося, першим виходом було б відновлювати ручку очікування поза шнурок.

Сказавши це, ви можете значно скоротити шаблон одноразового використання, коли у вас немає некерованих ресурсів. (Я досі шукаю офіційну довідку для цього.)

Але ви можете опустити деструктор і GC.SuppressFinalize (це); а може, трохи очистити віртуальну порожнечу Dispose (bool disposition).


Дякую Хенку. Якби я взяв створення waitHandle з Shoelace, то комусь все-таки доведеться десь розпоряджатися цим, як би хтось знав, що Shoelace закінчив це (і що Shoelace не передавав його до будь-яких інших класів)?
GrahamS

Вам потрібно буде перенести не лише Створення, але і «відповідальність за» чеканку. Відповідь jaredPar цікавий початок ідеї.
Хенк Холтерман

Цей блог охоплює реалізацію IDisposable, зокрема стосується того, як спростити завдання, уникаючи змішування керованих та некерованих ресурсів в одному класі blog.stephencleary.com/2009/08/…
PaulS

3

Цікаво, якщо Driverвизначено вище:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

Тоді, коли Shoeзроблено IDisposable, FxCop (v1.36) не скаржиться, що так само Driverмає бути IDisposable.

Однак якщо це визначено так:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

тоді вона скаржиться.

Я підозрюю, що це лише обмеження FxCop, а не рішення, тому що в першій версії Shoeекземпляри все ще створюються Driverі все ще потрібно якось утилізувати.


2
Якщо Show реалізує IDisposable, створювати його в ініціалізаторі поля небезпечно. Цікаво, що FXCop не дозволив би подібні ініціалізації, оскільки в C # єдиний спосіб зробити їх безпечно - це змінні ThreadStatic (прямо-таки жахливі). У vb.net можна безпечно ініціалізувати об'єкти, що дозволяють ID, без змінних ThreadStatic. Код все ще некрасивий, але не зовсім такий огидний. Я б хотів, щоб MS запропонувала хороший спосіб безпечного використання таких ініціалізаторів поля.
supercat

1
@supercat: спасибі Чи можете ви пояснити, чому це небезпечно? Я здогадуюсь, що якби виняток був кинутий на одного з Shoeконструкторів, тоді shoesвін залишився б у важкому стані?
GrahamS

3
Якщо хтось не знає, що певний тип IDisposable може бути безпечно відмовлений, слід забезпечити, щоб кожен створений об'єкт отримав Dispose'd. Якщо в ініціалізаторі поля об’єкта створюється IDisposable, але викид викидається до того, як об’єкт буде повністю побудований, об’єкт і всі його поля будуть покинуті. Навіть якщо конструкція об'єкта завершена фабричним методом, який використовує блок "спробувати", щоб виявити, що будівництво не вдалося, фабриці складно отримати посилання на об'єкти, які потребують утилізації.
supercat

3
Єдиний спосіб, з якого я знаю, щоб вирішити це в C #, - це використовувати змінну ThreadStatic для збереження списку об'єктів, які потребують утилізації, якщо поточний конструктор кине. Тоді дозвольте кожному ініціалізатору поля зареєструвати кожен об’єкт у міру його створення, фабрично очистіть список, якщо він успішно завершується, і використовуйте пункт "нарешті", щоб розпоряджатись усіма елементами зі списку, якщо він не був очищений. Це був би працездатний підхід, але змінні ThreadStatic некрасиві. У vb.net можна було використати подібну методику, не потребуючи змінних ThreadStatic.
суперкарт

3

Я не думаю, що існує технічний спосіб запобігти поширенню IDisposable, якщо ви будете тримати дизайн так щільно. Тоді варто задуматися, чи правильно підійде дизайн.

У вашому прикладі, я думаю, має сенс мати взуття власником шнурка, і, можливо, водій повинен володіти своїм взуттям. Однак автобус не повинен володіти водієм. Як правило, водії автобусів не прямують до автобусів до автобуса :) У випадку з водіями та взуттям, водії рідко роблять власне взуття, тобто вони насправді не "володіють ними".

Альтернативним дизайном може бути:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

Новий дизайн, на жаль, є складнішим, оскільки він вимагає додаткових класів для пристосування та утилізації конкретних примірників взуття та водіїв, але це ускладнення властиве вирішуваній проблемі. Хороша річ у тому, що автобуси більше не можна одноразово використовувати лише з метою утилізації шнурків.


2
Це насправді не вирішує оригінальну проблему. Це може заспокоїти FxCop, але щось все-таки повинно розпоряджатися шнурком.
АндрійС

Я думаю, що все, що ви зробили, - це перенести проблему в інше місце. ShoePairWithDisposableLaces зараз є власником Shoelace (), тому його також потрібно зробити IDisposable - тож хто розпоряджається взуттям? Ви залишаєте це в якомусь контейнері IoC для обробки?
GrahamS

@AndrewS: Дійсно, щось все-таки повинно утилізувати водії, взуття та шнурки, але утилізацію не слід розпочинати автобусами. @GrahamS: Ви маєте рацію, я переношу проблему в інше місце, тому що я вважаю, що вона належить в іншому місці. Як правило, було б класичне взуття, яке також відповідало за утилізацію взуття. Я відредагував код, щоб зробити ShoePairWithDisposableLaces також одноразовим.
Джон

@Joh: Дякую Як ви кажете, проблема з переміщенням його в інше місце полягає в тому, що ви створюєте набагато більшу складність. Якщо ShoePairWithDisposableLaces створений якимсь іншим класом, то чи не повинен би цей клас повідомляти, коли Водій закінчив свою взуття, щоб він міг правильно розпоряджатися () ними?
GrahamS

1
@Graham: так, потрібен якийсь механізм сповіщення. Наприклад, може бути використана політика утилізації, заснована на еталонних підрахунках. Тут є додаткові складності, але, як ви, напевно, знаєте, "немає такого поняття, як безкоштовне взуття" :)
Джон

1

Як щодо використання Інверсії управління?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

Тепер ви зробили це проблемою, що викликає абонентів, і Shoelace не може залежати від того, чи буде доступна ручка очікування, коли вона потребує.
Енді,

@andy Що ви маєте на увазі під "Шнурком не може залежати від того, що ручка очікування буде доступна, коли вона потрібна"? Це передано конструктору.
Дарраг

Щось інше, можливо, зіпсується зі станом автоматичного детектування, перш ніж подати його в Shoelace, і це може початися з ARE у поганому стані; щось інше може зіткнутися зі станом ARE, поки Shoelace робить свою справу, змусивши Shoelace зробити неправильну справу. Це та сама причина, що ви блокуєте лише приватних членів. "Взагалі, .. уникайте блокування на екземпляри, що не відповідають коду" docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
Енді

Я погоджуюся з тим, що я блокую лише приватних членів. Але, схоже, ручки очікування бувають різні. Насправді, здається, корисніше передавати їх об’єкту, оскільки той самий екземпляр потрібно використовувати для спілкування через потоки. Тим не менш, я думаю, що ІОК є корисним рішенням проблеми ОП.
Дарраг

З огляду на те, що приклад ОП мав чекаря в якості приватного члена, надійним припущенням є те, що об'єкт очікує виключного доступу до нього. Якщо ручка стає видимою поза екземпляром, це порушує це. Зазвичай IoC може бути корисним для таких речей, але не тоді, коли мова йде про нарізування різьби.
Енді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.