Осуществление “конца” звонит каждый раз, когда существует соответствующий вызов “запуска”

Скажем, я хочу осуществить правило:

Каждый раз Вы называете "StartJumping ()" в Вашей функции, необходимо назвать "EndJumping ()" перед возвратом.

Когда разработчик пишет их код, они могут просто забыть называть EndSomething - таким образом, я хочу помочь помнить.

Я могу думать о только одном способе сделать это: и это злоупотребляет ключевым словом "использования":

class Jumper : IDisposable {
    public Jumper() {   Jumper.StartJumping(); }
    public void Dispose() {  Jumper.EndJumping(); }

    public static void StartJumping() {...}
    public static void EndJumping() {...}
}

public bool SomeFunction() {
    // do some stuff

    // start jumping...
    using(new Jumper()) {
        // do more stuff
        // while jumping

    }  // end jumping
}

Существует ли лучший способ сделать это?

8
задан Robert Harvey 20 March 2015 в 15:39
поделиться

12 ответов

Я не соглашусь с Эриком: когда это делать или нет, зависит от обстоятельств. В какой-то момент я переработал свою большую базу кода, чтобы включить семантику получения / выпуска для всех обращений к пользовательскому классу изображений. Изначально изображения размещались в неподвижных блоках памяти, но теперь у нас появилась возможность помещать изображения в блоки, которые можно было перемещать, если они не были получены. В моем коде это серьезная ошибка, когда блок памяти проскочил после разблокировки.

Следовательно, очень важно обеспечить соблюдение этого закона.Я создал этот класс:

public class ResourceReleaser<T> : IDisposable
{
    private Action<T> _action;
    private bool _disposed;
    private T _val;

    public ResourceReleaser(T val, Action<T> action)
    {
        if (action == null)
            throw new ArgumentNullException("action");
        _action = action;
        _val = val;
    }

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

    ~ResourceReleaser()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _disposed = true;
            _action(_val);
        }
    }
}

, который позволяет мне создать этот подкласс:

public class PixelMemoryLocker : ResourceReleaser<PixelMemory>
{
    public PixelMemoryLocker(PixelMemory mem)
        : base(mem,
        (pm =>
            {
                if (pm != null)
                    pm.Unlock();
            }
        ))
    {
        if (mem != null)
            mem.Lock();
    }

    public PixelMemoryLocker(AtalaImage image)
        : this(image == null ? null : image.PixelMemory)
    {
    }
}

Который, в свою очередь, позволяет мне написать этот код:

using (var locker = new PixelMemoryLocker(image)) {
    // .. pixel memory is now locked and ready to work with
}

Он выполняет нужную мне работу, и быстрый поиск подсказывает мне, что он мне нужен в 186 местах который, я могу гарантировать, никогда не откажется от разблокировки. И я должен быть в состоянии дать эту гарантию - иначе я могу заморозить огромный кусок памяти в куче моего клиента. Я не могу этого сделать.

Однако в другом случае, когда я занимаюсь шифрованием документов PDF, все строки и потоки шифруются в словарях PDF, за исключением случаев, когда это не так. Действительно. Есть небольшое количество крайних случаев, когда неправильно зашифровать или расшифровать словари, поэтому при потоковой передаче объекта я делаю следующее:

if (context.IsEncrypting)
{
    crypt = context.Encryption;
    if (!ShouldBeEncrypted(crypt))
    {
        context.SuspendEncryption();
        suspendedEncryption = true;
    }
}
// ... more code ...
if (suspendedEncryption)
{
    context.ResumeEncryption();
}

так почему я выбрал этот подход вместо подхода RAII? Ну, любое исключение, которое происходит в коде ... more ... означает, что вы мертвы в воде. Восстановления нет. Никакого выздоровления быть не может. Вы должны начать с самого начала, и объект контекста необходимо реконструировать, так что его состояние все равно будет изменено. И для сравнения, мне нужно было выполнить этот код только 4 раза - вероятность ошибки намного меньше, чем в коде блокировки памяти, и если я забуду один в будущем, сгенерированный документ будет немедленно сломан (сбой быстро).

Так что выбирайте RAII, когда вы абсолютно точно ДОЛЖНЫ иметь вызов в скобках и не можете потерпеть неудачу. Не беспокойтесь о RAII, если это тривиально сделать иначе.

9
ответ дан 5 December 2019 в 04:33
поделиться

Мы сделали почти в точности то, что вы предлагаете, как способ добавления ведения журнала трассировки методов в наши приложения. Beats, что нужно сделать 2 журнала регистрации, один для входа и один для выхода.

2
ответ дан 5 December 2019 в 04:33
поделиться

Я немного прокомментировал здесь некоторые ответы о том, что IDisposable есть и нет, но я повторю, что IDisposable должен включать детерминированную очистку, но не гарантирует детерминированную очистку. то есть его вызов не гарантируется, а лишь отчасти гарантируется в паре с с использованием блока .

// despite being IDisposable, Dispose() isn't guaranteed.
Jumper j = new Jumper();

Я не буду комментировать ваше использование с помощью , потому что Эрик Липперт справился с этим гораздо лучше.

Если у вас есть класс IDisposable, не требующий финализатора, я видел шаблон для определения, когда люди забывают вызвать Dispose () , заключается в добавлении финализатора, который условно компилируется в сборках DEBUG, чтобы вы можете регистрировать что-то всякий раз, когда вызывается ваш финализатор.

Реалистичный пример - это класс, который каким-то особым образом инкапсулирует запись в файл. Поскольку MyWriter содержит ссылку на FileStream , который также является IDisposable , мы также должны реализовать IDisposable из соображений вежливости.

public sealed class MyWriter : IDisposable
{
    private System.IO.FileStream _fileStream;
    private bool _isDisposed;

    public MyWriter(string path)
    {
        _fileStream = System.IO.File.Create(path);
    }

#if DEBUG
    ~MyWriter() // Finalizer for DEBUG builds only
    {
        Dispose(false);
    }
#endif

    public void Close()
    {
        ((IDisposable)this).Dispose();
    }

    private void Dispose(bool disposing)
    {
        if (disposing && !_isDisposed)
        {
            // called from IDisposable.Dispose()
            if (_fileStream != null)
                _fileStream.Dispose();

            _isDisposed = true;
        }
        else
        {
            // called from finalizer in a DEBUG build.
            // Log so a developer can fix.
            Console.WriteLine("MyWriter failed to be disposed");
        }
    }

    void IDisposable.Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

}

Ой. Это довольно сложно, но именно этого люди ожидают, когда видят IDisposable.

Класс пока даже ничего не делает, кроме открытия файла, но это то, что вы получаете с IDisposable , а ведение журнала чрезвычайно упрощено.

    public void WriteFoo(string comment)
    {
        if (_isDisposed)
            throw new ObjectDisposedException("MyWriter");

        // logic omitted
    }

Финализаторы дороги, а MyWriter выше не требует финализатора, поэтому нет смысла добавлять его вне сборок DEBUG.

1
ответ дан 5 December 2019 в 04:33
поделиться
  1. Я не думаю, что вы хотите, чтобы эти методы были статичными.
  2. Вам нужно проверить при удалении, был ли уже вызван переход к концу.
  3. Что произойдет, если я снова вызову начало прыжка?

Вы можете использовать счетчик ссылок или флаг, чтобы отслеживать состояние «прыжка». Некоторые люди скажут, что IDisposable предназначен только для неуправляемых ресурсов, но я думаю, что это нормально. В противном случае вы должны сделать начало и конец прыжка частными и использовать деструктор для работы с конструктором.

class Jumper
{
    public Jumper() {   Jumper.StartJumping(); }
    public ~Jumper() {  Jumper.EndJumping(); }

    private void StartJumping() {...}
    public void EndJumping() {...}
}
0
ответ дан 5 December 2019 в 04:33
поделиться

Будет ли полезен абстрактный базовый класс? Метод в базовом классе может вызвать StartJumping (), реализацию абстрактного метода, который будут реализовывать дочерние классы, а затем вызвать EndJumping ().

1
ответ дан 5 December 2019 в 04:33
поделиться

Альтернативный подход, который будет работать при некоторых обстоятельствах (т.е. когда все действия могут происходить в конце), заключался бы в создании серии классов с плавным интерфейсом и некоторым финальным методом Execute ().

var sequence = StartJumping().Then(some_other_method).Then(some_third_method);
// forgot to do EndJumping()
sequence.Execute();

Execute () может возвращаться вниз по цепочке и обеспечивать соблюдение любых правил (или вы можете построить последовательность закрытия по мере построения последовательности открытия).

Одно из преимуществ этого метода перед другими заключается в том, что вы не ограничены правилами области видимости. например если вы хотите построить последовательность на основе пользовательского ввода или других асинхронных событий, вы можете это сделать.

6
ответ дан 5 December 2019 в 04:33
поделиться

Мне нравится этот стиль, и я часто использую его, когда хочу гарантировать некоторое поведение разрыва: часто его читать намного проще, чем попытаться окончательно. Я не думаю, что вам следует беспокоиться об объявлении и названии ссылки j, но я считаю, что вам следует избегать вызова метода EndJumping дважды, вы должны проверить, не был ли он уже удален. И что касается вашего примечания к неуправляемому коду: это финализатор, который обычно реализуется для этого (хотя обычно вызываются Dispose и SuppressFinalize, чтобы освободить ресурсы раньше).

1
ответ дан 5 December 2019 в 04:33
поделиться

Джефф,

то, что вы пытаетесь достичь, обычно называют аспектно-ориентированным программированием (AOP ). Программирование с использованием парадигм АОП на C # непросто - и надежно ... пока что. Некоторые возможности, встроенные непосредственно в среду CLR и .NET, делают возможным использование АОП в определенных узких случаях. Например, когда вы наследуете класс от ContextBoundObject , вы можете использовать ContextAttribute для внедрения логики до / после вызовов методов в экземпляре CBO. Вы можете увидеть примеры того, как это делается здесь.

Создание класса CBO раздражает и ограничивает - и есть еще одна альтернатива. Вы можете использовать такой инструмент, как PostSharp, для применения АОП к любому классу C #. PostSharp гораздо более гибок, чем CBO, потому что он по существу переписывает ваш код IL на этапе посткомпиляции . Хотя это может показаться немного пугающим, оно очень мощное, потому что позволяет вплетать код практически любым способом, который вы можете себе представить.Вот пример PostSharp, созданный на основе вашего сценария использования:

using PostSharp.Aspects;

[Serializable]
public sealed class JumperAttribute : OnMethodBoundaryAspect
{
  public override void OnEntry(MethodExecutionArgs args) 
  { 
    Jumper.StartJumping();
  }     

  public override void OnExit(MethodExecutionArgs args) 
  { 
    Jumper.EndJumping(); 
  }
}

class SomeClass
{
  [Jumper()]
  public bool SomeFunction()  // StartJumping *magically* called...          
  {
    // do some code...         

  } // EndJumping *magically* called...
}

PostSharp достигает магии , переписывая скомпилированный код IL, чтобы включить инструкции для выполнения кода, который вы определили в JumperAttribute class ' OnEntry и OnExit методы.

Мне неясно, является ли в вашем случае PostSharp / AOP лучшей альтернативой, чем "перепрофилирование" оператора using. Я склонен согласиться с @Eric Lippert, что ключевое слово using запутывает важную семантику вашего кода и накладывает побочные эффекты и семантическое ментирование на символ } в конце блока using, что неожиданно . Но чем это отличается от применения атрибутов АОП к вашему коду? Они также скрывают важную семантику за декларативным синтаксисом ... но это своего рода суть АОП.

Один момент, в котором я полностью согласен с Эриком, заключается в том, что изменение вашего кода, чтобы избежать такого глобального состояния (когда это возможно), вероятно, является лучшим вариантом. Это не только позволяет избежать проблемы обеспечения правильного использования, но также может помочь избежать проблем с многопоточностью в будущем, к которым глобальное состояние очень восприимчиво.

5
ответ дан 5 December 2019 в 04:33
поделиться

Если вам нужно управлять операцией с заданной областью действия, я бы добавил метод, который принимает Action , чтобы содержать необходимые операции в экземпляре перемычки:

public static void Jump(Action<Jumper> jumpAction)
{
    StartJumping();
    Jumper j = new Jumper();
    jumpAction(j);
    EndJumping();
}
8
ответ дан 5 December 2019 в 04:33
поделиться

На самом деле я не считаю это злоупотреблением использованием ; Я использую эту идиому в разных контекстах, и у меня никогда не было проблем ... особенно с учетом того, что с использованием - всего лишь синтаксический сахар. Один из способов я использую его для установки глобального флага в одной из сторонних библиотек, которые я использую, чтобы изменение отменялось при завершении операций:

class WithLocale : IDisposable {
    Locale old;
    public WithLocale(Locale x) { old = ThirdParty.Locale; ThirdParty.Locale = x }
    public void Dispose() { ThirdParty.Locale = old }
}

Обратите внимание, что вам не нужно назначать переменную в с помощью пункт. Этого достаточно:

using(new WithLocale("jp")) {
    ...
}

Я немного скучаю по идиоме RAII C ++ здесь, где деструктор вызывается всегда. с использованием , я думаю, ближе всего к C #.

4
ответ дан 5 December 2019 в 04:33
поделиться

С шаблон использования Я могу просто использовать команду grep (? , чтобы найти все места, где могут быть проблемы.

С StartJumping мне нужно вручную просматривать каждый вызов, чтобы выяснить, есть ли вероятность, что исключение, return, break, continue, goto и т. Д. Может привести к тому, что EndJumping не будет вызван.

1
ответ дан 5 December 2019 в 04:33
поделиться

По сути проблема заключается в следующем:

  • у меня есть глобальное состояние...
  • и я хочу мутировать это глобальное состояние...
  • но я хочу убедиться, что я мутирую его обратно.

Вы обнаружили, что это больно, когда вы это делаете. Мой совет: вместо того, чтобы пытаться найти способ сделать так, чтобы было менее больно, постарайтесь найти способ не делать болезненную вещь в первую очередь.

Я хорошо понимаю, как это трудно. Когда мы добавили лямбды в C# в версии 3, мы столкнулись с большой проблемой. Подумайте о следующем:

void M(Func<int, int> f) { }
void M(Func<string, int> f) { }
...
M(x=>x.Length);

Как мы можем успешно связать это? Ну, мы пробуем оба варианта (x is int, или x is string) и смотрим, какой из них, если таковой имеется, выдает ошибку. Те, которые не дают ошибок, становятся кандидатами на разрешение перегрузки.

Механизмом сообщения об ошибках в компиляторе является global state. В C# 1 и 2 никогда не возникало ситуации, когда нужно было бы сказать: "Свяжите все тело метода для определения наличия ошибок но не сообщайте об ошибках". В конце концов, в этой программе вы не хотите получить ошибку "int не имеет свойства Length", вы хотите, чтобы она обнаружила это, сделала заметку и не сообщала об этом.

Так что я сделал именно то, что сделали вы. Начните подавлять сообщения об ошибках, но не забудьте ЗАПРЕТИТЬ подавлять сообщения об ошибках.

Это ужасно. Что мы действительно должны сделать, так это перепроектировать компилятор так, чтобы ошибки были выходом семантического анализатора, а не глобальным состоянием компилятора. Однако это трудно протащить через сотни тысяч строк существующего кода, который зависит от этого глобального состояния.

В любом случае, есть еще кое-что, о чем стоит подумать. Ваше решение "using" имеет эффект остановки прыжка, когда возникает исключение. Правильно ли это? Возможно, нет. Ведь было брошено неожиданное, необработанное исключение. Вся система может быть массивно нестабильной. Ни один из ваших внутренних инвариантов состояния не может быть действительно инвариантным в этом сценарии.

Посмотрите на это с другой стороны: Я мутировал глобальное состояние. Затем я получил неожиданное, необработанное исключение. Я знаю, я думаю, я снова мутирую глобальное состояние! Это поможет! Похоже, это очень, очень плохая идея.

Конечно, это зависит от того, что такое мутация глобального состояния. Если это "снова начать сообщать об ошибках пользователю", как в компиляторе, тогда правильным для не обработанного исключения будет снова начать сообщать об ошибках пользователю: в конце концов, нам нужно будет сообщить об ошибке, которую компилятор только что выдал как не обработанное исключение!

С другой стороны, если мутация глобального состояния заключается в том, чтобы "разблокировать ресурс и позволить наблюдать и использовать его ненадежному коду", то автоматическая разблокировка потенциально является ОЧЕНЬ ПЛОХОЙ ИДЕЕЙ. Это неожиданное, необработанное исключение может быть свидетельством атаки на ваш код от злоумышленника, который очень надеется, что вы собираетесь разблокировать доступ к глобальному состоянию теперь, когда оно находится в уязвимой, непоследовательной форме.

14
ответ дан 5 December 2019 в 04:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: