instrukcja „using” vs „try last”

81

Mam kilka właściwości, na których mam zamiar używać blokad odczytu / zapisu. Mogę je zaimplementować za pomocą klauzuli try finallylub usingklauzuli.

W przypadku try finallyzdobycia blokady przed tryi zwolnienia w finally. W usingklauzuli utworzyłbym klasę, która uzyskuje blokadę w swoim konstruktorze i zwalnia w swojej metodzie Dispose.

Używam blokad odczytu / zapisu w wielu miejscach, więc szukałem sposobów, które mogą być bardziej zwięzłe niż try finally. Interesuje mnie kilka pomysłów, dlaczego jeden sposób może nie być zalecany lub dlaczego jeden może być lepszy od drugiego.

Metoda 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();
        }
    }
}

Metoda 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();
    }
}
Jeremy
źródło
1
Jak już stwierdzono w wielu odpowiedziach, Metoda 2 jest bardzo dobra, ale aby uniknąć śmieci na stercie za każdym razem, gdy używasz blokady, powinieneś zmienić ReadLock i WriteLock na struktury. Mimo że instrukcja using używa interfejsu IDisposable struktury, C # jest wystarczająco sprytny, aby uniknąć boksu!
Michael Gehling

Odpowiedzi:

95

Z MSDN przy użyciu instrukcji (odwołanie w C #)

Instrukcja using zapewnia wywołanie metody Dispose, nawet jeśli wystąpi wyjątek podczas wywoływania metod w obiekcie. Możesz osiągnąć ten sam wynik, umieszczając obiekt w bloku try, a następnie wywołując metodę Dispose w bloku final; w rzeczywistości jest to sposób tłumaczenia instrukcji using przez kompilator. Przykładowy kod wcześniej rozwija się do następującego kodu w czasie kompilacji (zwróć uwagę na dodatkowe nawiasy klamrowe, aby utworzyć ograniczony zakres dla obiektu):

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

Zasadniczo jest to ten sam kod, ale z ładnym automatycznym sprawdzaniem wartości null i dodatkowym zakresem dla twojej zmiennej . Dokumentacja stwierdza również, że „zapewnia poprawne użycie obiektu IDisposable”, więc równie dobrze możesz uzyskać jeszcze lepszą obsługę frameworka dla wszelkich niejasnych przypadków w przyszłości.

Więc wybierz opcję 2.

Umieszczenie zmiennej w zakresie, który kończy się natychmiast po tym, jak nie jest już potrzebna, jest również plusem.

chakrit
źródło
co jest najlepsze, aby zwolnić zasób, którego nie można utworzyć za pomocą instrukcji lub ponownie użyć lub przekazać jako parametr out? try / catch !!!
bjan
2
@bjan cóż, dlaczego w ogóle rozważasz usingten przypadek? nie do tego usingsłuży.
chakrit
1
dlatego też wspominam, try/catchponieważ wygląda to na jedyny sposób obsługi przez try/catch/finallyblok. using
Miałem
Cóż, tak try/finally, to chyba jedyna opcja w tym przypadku. IMO, jednak myślę, że zawsze powinien istnieć jakiś obiekt / fragment kodu odpowiedzialny za utrzymanie czasu życia obiektu w takich przypadkach (gdzie Dispose () powinno być zawsze wywoływane.) Jeśli jedna klasa obsługuje tylko instancję i ktoś inny musi pamiętać żeby się go pozbyć, wydaje mi się, że trochę tam pachnie. Nie wiem, jak można to dodać na poziomie języka.
chakrit
Myślę, że jedynym powodem unikania „używania” jest całkowite unikanie rzeczy jednorazowego użytku, ponieważ jest to kolejna instancja obiektu, jak sądzę?
Demetris Leptos
12

Zdecydowanie wolę drugą metodę. Jest bardziej zwięzły w miejscu użycia i mniej podatny na błędy.

W pierwszym przypadku osoba edytująca kod musi uważać, aby nie wstawić niczego między wywołaniem Acquire (Read | Write) Lock a próbą.

(Stosowanie blokady odczytu / zapisu na poszczególnych właściwościach, takich jak ta, jest jednak zwykle przesadą. Najlepiej jest je stosować na znacznie wyższym poziomie. Zwykła blokada jest tutaj często wystarczająca, ponieważ możliwość rywalizacji jest prawdopodobnie bardzo mała, biorąc pod uwagę czas utrzymywania blokady dla, a uzyskanie blokady odczytu / zapisu jest operacją droższą niż zwykła blokada).

Rob Walker
źródło
A co powiesz na bezpieczeństwo w przypadku awarii? Wiem, że próba w końcu zawsze spowoduje odpalenie ostatniego bloku, czy jest jakiś sposób, że dyspozytor nie zostanie wywołany?
Jeremy
1
Nie, instrukcja using jest zasadniczo cukrem syntaktycznym dla wzorca try / final.
Blair Conrad
Model using zapewnia, że ​​obiekt zawsze zostanie usunięty.
Nathen Silver
Jeśli używasz reflektora, zobaczysz, że blok using jest tłumaczony przez kompilator na konstrukcję try ... final, więc na poziomie IL są one równoważne. „Używanie” to po prostu cukier syntetyczny.
Rob Walker
^^ pamiętaj, że istnieją potencjalne scenariusze, w których ostatecznie nie jest wywoływane. Na przykład przerwa w dostawie prądu. ;)
Quibblesome
8

Rozważ możliwość, że oba rozwiązania są złe, ponieważ maskują wyjątki .

A trybez a catchpowinno być oczywiście złym pomysłem; zobacz MSDN, aby dowiedzieć się, dlaczego to usingstwierdzenie jest również niebezpieczne.

Należy również zauważyć, że firma Microsoft zaleca teraz ReaderWriterLockSlim zamiast ReaderWriterLock.

Na koniec zwróć uwagę, że przykłady firmy Microsoft używają dwóch bloków try-catch, aby uniknąć tych problemów, np

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

Proste, spójne i czyste rozwiązanie to dobry cel, ale zakładając, że nie możesz po prostu użyć lock(this){return mydateetc;}, możesz ponownie rozważyć podejście; z więcej informacji Jestem pewien, że Stack Overflow może pomóc ;-)

Steven A. Lowe
źródło
2
Ostateczna próba niekoniecznie maskuje wyjątek. W moim przykładzie blokada jest uzyskiwana, a następnie jeśli jakikolwiek wyjątek zostanie zgłoszony w zakresie, blokada zostanie zwolniona, ale wyjątek nadal będzie się rozwijać.
Jeremy
1
@Jeremy: jeśli twój ostatni blok zgłosi wyjątek, zamaskuje wyjątek zgłoszony w twoim bloku try - o tym mówił artykuł msdn, który był (tym samym) problemem ze składnią
Steven A. Lowe
3
Nie zamaskuje wyjątku, zastąpi go dokładnie w taki sam sposób, jak twój.
Jon Hanna
5

Osobiście używam instrukcji C # „using” tak często, jak to możliwe, ale jest kilka konkretnych rzeczy, które robię razem z nią, aby uniknąć wymienionych potencjalnych problemów. Ilustrować:

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

Zauważ, że oddzielam instrukcję „using” na jedną metodę, a użycie obiektu (obiektów) na inną metodę za pomocą bloku „try” / „catch”. Mogę zagnieżdżać kilka takich instrukcji „using” dla powiązanych obiektów (czasami w kodzie produkcyjnym zagnieżdżam się trzy lub cztery).

W moich Dispose()metodach dla tych niestandardowych IDisposableklas łapię wyjątki (ale NIE błędy) i rejestruję je (za pomocą Log4net). Nigdy nie spotkałem się z sytuacją, w której którykolwiek z tych wyjątków mógłby wpłynąć na moje przetwarzanie. Potencjalne błędy, jak zwykle, mogą rozprzestrzeniać się w górę stosu wywołań i zazwyczaj kończą przetwarzanie z zarejestrowanym odpowiednim komunikatem (błąd i ślad stosu).

Gdybym w jakiś sposób napotkał sytuację, w której może wystąpić znaczący wyjątek podczas Dispose(), przeprojektowałbym tę sytuację. Szczerze mówiąc, wątpię, żeby to kiedykolwiek się stało.

W międzyczasie zakres i zalety „używania” czyszczenia sprawiają, że jest to jedna z moich ulubionych funkcji C #. Nawiasem mówiąc, pracuję w Javie, C # i Pythonie jako moich podstawowych językach, z wieloma innymi wrzucanymi tu i ówdzie, a „używanie” jest jedną z moich ulubionych funkcji językowych, ponieważ jest to praktyczny, codzienny koń .

Rob Williams
źródło
Wskazówka, aby zapewnić czytelność kodu: wyrównaj i skompresuj instrukcje use, nie używając nawiasów klamrowych, z wyjątkiem wewnętrznego „using”.
Bent Tranberg
4

Podoba mi się trzecia opcja

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

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

Z dwóch opcji, druga opcja jest najczystsza i łatwiejsza do zrozumienia, co się dzieje.


źródło
instrukcja lock nie działa w taki sam sposób, jak ReaderWriterLock.
chakrit
@chakrit: Nie, ale jeśli nie wiesz, że faktycznie istnieje rywalizacja o blokady, prawdopodobnie będą bardziej wydajne.
Michael Burr
To prawda, ale zawsze powinieneś najpierw wybrać smiple lock. Pytanie subby nie mówi nic o problemach z wydajnością lub wymaganiach, które nie pozwalają na blokadę. Dlatego nie ma powodu, aby próbować być sprytnym. Po prostu zablokuj i rozkołysz
4

„Wiązka właściwości” i blokowanie na poziomie metody pobierającej i ustawiającej właściwości wygląda nieprawidłowo. Twoje blokowanie jest zbyt drobnoziarniste. W większości typowych zastosowań obiektów chciałbyś się upewnić, że nabyłeś blokadę, aby uzyskać dostęp do więcej niż jednej właściwości w tym samym czasie. Twój konkretny przypadek może być inny, ale trochę w to wątpię.

W każdym razie, uzyskanie blokady podczas uzyskiwania dostępu do obiektu zamiast do właściwości znacznie zmniejszy ilość kodu blokującego, który trzeba będzie napisać.

Hans Passant
źródło
Tak, zdecydowanie rozumiem twój punkt widzenia. Większość moich właściwości to w rzeczywistości boole, ints, których nie musiałbym blokować, ponieważ powinny być atomowe. Jest kilka ciągów znaków z datą i godziną, na których chciałbym zablokować. ponieważ mniejszość potrzebowałaby zamków, najlepiej jest umieścić je na posesji
Jeremy
4

DRY mówi: drugie rozwiązanie. Pierwsze rozwiązanie powiela logikę używania zamka, podczas gdy drugie nie.

Teta
źródło
1

Bloki Try / Catch służą zazwyczaj do obsługi wyjątków, natomiast bloki służą do zapewnienia, że ​​obiekt zostanie usunięty.

W przypadku blokady odczytu / zapisu try / catch może być najbardziej przydatne, ale możesz również użyć obu, na przykład:

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

dzięki czemu można niejawnie wywołać swój interfejs IDisposable, a także zwięźle obsługiwać wyjątki.

Łukasz
źródło
0

Myślę, że metoda 2 byłaby lepsza.

  • Prostszy i bardziej czytelny kod w Twoich nieruchomościach.
  • Mniej podatne na błędy, ponieważ kod blokujący nie musi być przepisywany kilka razy.
Nathen Silver
źródło
0

Chociaż zgadzam się z wieloma powyższymi komentarzami, w tym szczegółowością blokady i wątpliwą obsługą wyjątków, pytanie dotyczy podejścia. Pozwólcie, że podam jeden duży powód, dla którego wolę używać zamiast try {} wreszcie modelu… abstrakcji.

Mam model bardzo podobny do twojego z jednym wyjątkiem. Zdefiniowałem interfejs bazowy ILock i umieściłem w nim jedną metodę o nazwie Acquire (). Metoda Acquire () zwróciła obiekt IDisposable iw rezultacie oznacza, że ​​tak długo, jak obiekt, z którym mam do czynienia, jest typu ILock, można go używać do blokowania zakresu. Dlaczego to jest ważne?

Mamy do czynienia z wieloma różnymi mechanizmami blokowania i zachowaniami. Twój obiekt blokady może mieć określony limit czasu. Implementacją blokady może być blokada monitora, blokada czytnika, blokada programu zapisującego lub blokada obrotu. Jednak z punktu widzenia dzwoniącego to wszystko nie ma znaczenia, to, na czym mu zależy, to to, że umowa na blokowanie zasobu jest honorowana, a blokada robi to w sposób zgodny z jego implementacją.

interface ILock {
    IDisposable Acquire();
}

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

Podoba mi się twój model, ale rozważę ukrycie mechaniki zamka przed dzwoniącym. FWIW, zmierzyłem narzut związany z użyciem techniki w porównaniu z próbą ostateczną, a narzut związany z przydzieleniem jednorazowego obiektu będzie miał między 2-3% narzut wydajności.

Ajaxx
źródło
0

Dziwię się, że nikt nie zasugerował umieszczenia próby w końcu w anonimowych funkcjach. Podobnie jak technika tworzenia instancji i usuwania klas za pomocą instrukcji using, utrzymuje to blokadę w jednym miejscu. Sam wolę to tylko dlatego, że wolę przeczytać słowo „w końcu” niż słowo „pozbyć się”, kiedy myślę o zwolnieniu blokady.

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

źródło
0

SoftwareJedi, nie mam konta, więc nie mogę edytować swoich odpowiedzi.

W każdym razie poprzednia wersja nie była zbyt dobra do użytku ogólnego, ponieważ blokada odczytu zawsze wymagała zwrócenia wartości. To naprawia, że:

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

źródło
0

Poniższe tworzy metody rozszerzające dla klasy ReaderWriterLockSlim, które umożliwiają wykonanie następujących czynności:

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

Oto kod:

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();
    }
}
Clint Chapman
źródło
0

Właściwie w swoim pierwszym przykładzie, aby zapewnić porównywalność rozwiązań, również byś je IDisposabletam wdrożył . Wtedy zadzwoniłbyś Dispose()z finallybloku zamiast bezpośrednio zwalniać blokadę.

Wtedy byłaby to implementacja typu „jabłka na jabłka” (i MSIL) -wsparcie (MSIL będzie taki sam dla obu rozwiązań). Nadal prawdopodobnie dobrym pomysłem jest użycie usingze względu na dodany zakres i ponieważ Framework zapewni właściwe użycie IDisposable(to drugie jest mniej korzystne, jeśli wdrażasz IDisposablesamodzielnie).

galaksja
źródło
-1

Głupi ja. Można to jeszcze uprościć, czyniąc zablokowane metody częścią każdej instancji (zamiast statycznej, jak w moim poprzednim poście). Teraz naprawdę wolę to, ponieważ nie ma potrzeby przekazywania `rwlMyLock_m 'do innej klasy lub metody.

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

źródło