'використання' твердження проти 'спробувати нарешті'


81

У мене є купа властивостей, для яких я буду використовувати блокування читання / запису. Я можу реалізувати їх або за try finallyдопомогою usingпункту, або речення.

В try finallyя б придбав замок до try, і відпустив в finally. У цьому usingреченні я б створив клас, який отримує замок у своєму конструкторі та звільняє у своєму методі Dispose.

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

Спосіб 1 ( try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

Спосіб 2:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}

1
Як вже зазначалося у багатьох відповідях, метод 2 дуже хороший, але щоб уникнути сміття в кучі кожного разу, коли ви використовуєте блокування, вам слід змінити ReadLock та WriteLock на структури. Незважаючи на те, що оператор using використовує інтерфейс структури ID, що одноразовий, C # є досить розумним, щоб уникнути боксу!
Майкл Гелінг,

Відповіді:


95

З MSDN, використовуючи Statement (C # Reference)

Оператор using забезпечує виклик Dispose, навіть якщо виникає виняток під час виклику методів на об'єкті. Ви можете досягти того ж результату, помістивши об'єкт всередину блоку try, а потім викликавши Dispose у блоці нарешті; насправді саме так перекладається компілятором оператор using. Раніше приклад коду розширюється до наступного коду під час компіляції (зверніть увагу на додаткові фігурні дужки, щоб створити обмежений обсяг для об’єкта):

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

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

Тож переходьте до варіанту 2.

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


що найкраще випустити ресурс, який неможливо створити за допомогою оператора або повторно використовувати або передавати як параметр? try / catch !!!
bjan

2
@bjan ну, чому ти взагалі розглядаєш usingу такому випадку? це не те, що usingдля.
чакрит

1
саме тому я також згадую, try/catchоскільки це виглядає єдиним способом обробки через try/catch/finallyблок. сподівався, що usingі з цим впорався
bjan

Ну, так, try/finallyя думаю, це ваш єдиний варіант. ІМО, однак, я думаю, що в таких випадках (де завжди слід викликати Dispose () завжди повинен бути якийсь об'єкт / шматок коду, відповідальний за підтримку життя об'єкта.) Якщо один клас обробляє лише екземпляр, а хтось інший повинен пам'ятати щоб розпорядитися цим, я думаю, там трохи пахне. Не знаю, як можна це додати на мовному рівні.
чакрит

Я думаю, що єдина причина уникнути "використання" - це повністю уникнути одноразового використання, оскільки це, здається, ще одна інстанція об'єкта?
Demetris Leptos

12

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

У першому випадку хтось, хто редагує код, повинен бути обережним, щоб нічого не вставити між викликом блокування Acquire (Read | Write) і спробою.

(Використання блокування читання / запису для окремих властивостей, подібних до цього, як правило, є надмірним. Однак їх найкраще застосовувати на набагато вищому рівні. Тут часто буває достатньо простого блокування, оскільки можливість суперечки, мабуть, дуже мала, враховуючи час утримання блокування для отримання блокування читання / запису - це більш дорога операція, ніж простий замок).


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

1
Ні, оператор using - це, по суті, синтаксичний цукор для шаблону try / нарешті.
Блер Конрад,

Застосовувана модель гарантує, що об’єкт завжди буде утилізований.
Nathen Silver,

Якщо ви використовуєте рефлектор, ви побачите, що блок using перекладається компілятором у конструкцію try ... нарешті, тому на рівні IL вони еквівалентні. "Використання" - це просто синтатичний цукор.
Роб Уолкер,

^^ пам'ятайте, є потенційні сценарії, коли нарешті не викликається. Наприклад, відключення електроенергії. ;)
Дивовижний

8

Розглянемо можливість того, що обидва рішення погані, оскільки вони маскують винятки .

A tryбез a, catchочевидно, має бути поганою ідеєю; див. MSDN щодо того, чому usingтвердження є також небезпечним.

Зверніть увагу, що зараз Microsoft рекомендує ReaderWriterLockSlim замість ReaderWriterLock.

Нарешті, зауважте, що в прикладах Microsoft використовуються два блоки спроби уловлення, щоб уникнути цих проблем, наприклад

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

Просте, послідовне, чисте рішення - хороша мета, але припускаючи, що ви не можете просто використовувати lock(this){return mydateetc;}, ви можете переглянути підхід; з додатковою інформацією, я впевнений, що Stack Overflow може допомогти ;-)


2
Спроба нарешті не обов'язково маскує виняток. У моєму прикладі замок отримано, тоді якщо будь-який виняток буде введено в область дії, замок буде звільнений, але виняток все одно буде спливаючим.
Джеремі

1
@Jeremy: якщо ваш нарешті блок викине виняток, це замаскує виняток, доданий у ваш блок try, - ось що, як говорилося в статті msdn, було (тією ж) проблемою використання синтаксису
Стівен А. Лоу,

3
Він не замаскує виняток, він замінить виняток точно так само, як і ваш.
Джон Ханна,

5

Я особисто якомога частіше використовую оператор C # "використання", але я роблю разом із ним кілька конкретних речей, щоб уникнути згаданих потенційних проблем. Проілюструвати:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

Зверніть увагу, що я відокремлюю оператор "using" на один метод, а використання об’єкта (-ів) - на інший метод за допомогою блоку "try" / "catch". Я можу вкласти кілька таких "виразів" для пов'язаних об'єктів (я іноді заглиблююсь у свій робочий код на три-чотири).

У своїх Dispose()методах для цих користувацьких IDisposableкласів я ловлю винятки (але НЕ помилки) і реєструю їх (використовуючи Log4net). Я ніколи не стикався з ситуацією, коли будь-який із цих винятків міг вплинути на мою обробку. Потенційні помилки, як зазвичай, дозволяють розповсюджувати стек викликів і, як правило, завершують обробку відповідним повідомленням (повідомлення про помилку та стек).

Якби я якось зіткнувся з ситуацією, коли протягом цього могло статися значне виняток Dispose(), я б переробив цю ситуацію. Чесно кажучи, я сумніваюся, що це коли-небудь станеться.

Тим часом, масштаби та переваги очищення від "використання" роблять це однією з найулюбленіших функцій C #. До речі, я працюю на Java, C # та Python як на мої основні мови, багато-де-куди кидають сюди-туди, і "використання" є однією з найулюбленіших мовних особливостей навколо, оскільки це практична щоденна робоча коня .


Порада щодо читабельності коду: Вирівняйте та стисніть оператори використання, не маючи фігурних дужок, за винятком внутрішнього "використання".
Bent Tranberg

4

Мені подобається 3-й варіант

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

З двох ваших варіантів другий варіант є найчистішим і легшим для розуміння того, що відбувається.


оператор lock не працює так само, як і ReaderWriterLock.
chakrit

@chakrit: Ні, але якщо ви не знаєте, що насправді існує суперечка щодо блокування, вони, ймовірно, будуть ефективнішими.
Майкл Берр,

Правда, але завжди слід спочатку йти на замок смайлів. У запитанні суббіля нічого не сказано про проблеми з продуктивністю чи вимоги, які не дозволяють блокувати. Тому немає жодної причини намагатись бути хитрими. Просто зафіксуйте його і розгойдайте.

4

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

У будь-якому випадку, придбання блокування при доступі до об’єкта замість властивості значно зменшить кількість коду блокування, який вам доведеться написати.


Так, я точно бачу вашу думку. Більшість моїх властивостей - це насправді були, інтри, які мені не потрібно було б блокувати, оскільки вони повинні бути атомними. Є деякі дати та строки, які я хотів би заблокувати. оскільки меншості потрібні замки, мені найкраще поставити його на власність
Джеремі

4

DRY каже: друге рішення. Перше рішення дублює логіку використання блокування, тоді як друге - ні.


1

Блоки Try / Catch, як правило, призначені для обробки винятків, тоді як використання блоків використовується для того, щоб об'єкт був утилізований.

Для блокування читання / запису спроба / уловлювання може бути найбільш корисною, але ви також можете використовувати обидва, наприклад:

using (obj)
{
  try { }
  catch { }
}

так що ви можете неявно зателефонувати своєму інтерфейсу IDisposable, а також зробити стислу обробку винятків.


0

Я думаю, що метод 2 був би кращим.

  • Простіший та читабельніший код у ваших властивостях.
  • Менш схильний до помилок, оскільки код блокування не потрібно переписувати кілька разів.

0

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

У мене є модель, дуже схожа на вашу, за одним винятком. Я визначив базовий інтерфейс ILock і в ньому запропонував один метод під назвою Acquire (). Метод Acquire () повернув об'єкт IDisposable, і, як результат, означає, що поки об'єкт, з яким я маю справу, має тип ILock, його можна використовувати для здійснення області блокування. Чому це важливо?

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

interface ILock {
    IDisposable Acquire();
}

class MonitorLock : ILock {
    IDisposable Acquire() { ... acquire the lock for real ... }
}

Мені подобається ваша модель, але я б хотів приховати механіку блокування від абонента. FWIW, я виміряв накладні витрати на використання техніки порівняно з спробою остаточно, і накладні витрати на розподіл одноразового об'єкта матимуть між 2-3% накладних витрат.


0

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

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                rwlMyLock_m,
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                rwlMyLock_m,
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private static DateTime ReadLockedMethod(
        ReaderWriterLock rwl,
        ReadLockMethod method
    )
    {
        rwl.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }

    private static void WriteLockedMethod(
        ReaderWriterLock rwl,
        WriteLockMethod method
    )
    {
        rwl.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}

0

SoftwareJedi, у мене немає облікового запису, тому я не можу редагувати свої відповіді.

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

class StackOTest
{
    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            DateTime retval = default(DateTime);
            ReadLockedMethod(
                delegate () { retval = dtMyDateTime_m; }
            );
            return retval;
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private void ReadLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}

0

Далі створюються методи розширення для класу ReaderWriterLockSlim, які дозволяють зробити наступне:

var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

Ось код:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}

0

Насправді у вашому першому прикладі, щоб зробити рішення порівнянними, ви б також застосували їх IDisposableтам. Тоді ви зателефонуєте Dispose()з finallyблоку, замість того, щоб відпускати замок безпосередньо.

Тоді ви будете реалізовувати "яблука до яблук" (і MSIL) - так само (MSIL буде однаковим для обох рішень). Це, мабуть, хороша ідея використовувати usingчерез доданий масштаб і тому, що Framework забезпечить правильне використання IDisposable(останнє буде менш вигідним, якщо ви реалізуєте IDisposableсебе).


-1

Дурний я. Існує спосіб зробити це ще простішим, зробивши заблоковані методи частиною кожного екземпляра (замість статичних, як у моєму попередньому дописі). Зараз я дійсно віддаю перевагу цьому, оскільки немає необхідності передавати `rwlMyLock_m 'якомусь іншому класу чи методу.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private DateTime ReadLockedMethod(ReadLockMethod method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(WriteLockMethod method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.