Выполнение "конечного" вызова всякий раз, когда есть соответствующий "стартовый" вызов

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

Каждый раз, когда вы вызываете "StartJumping()" в своей функции, вы должны вызвать "EndJumping()" перед тем, как вернуться.

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

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

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
}

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

Ответ 1

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

Поэтому жизненно важно обеспечить это. Я создал этот класс:

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

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

Ответ 2

По существу проблема такова:

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

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

Мне хорошо известно, насколько это сложно. Когда мы добавили lambdas к С# в v3, у нас возникла большая проблема. Рассмотрим следующее:

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

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

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

Итак, я сделал то, что вы сделали. Начните подавлять отчет об ошибках, но не забывайте, что STOP подавляет отчет об ошибках.

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

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

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

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

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

Ответ 3

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

public static void Jump(Action<Jumper> jumpAction)
{
    StartJumping();
    Jumper j = new Jumper();
    jumpAction(j);
    EndJumping();
}

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

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

Ответ 6

Джефф,

то, что вы пытаетесь достичь, обычно называется Aspect Oriented Programming (AOP). Программирование с использованием AOP-парадигм в С# непросто - или надежно... пока. Есть некоторые возможности, встроенные непосредственно в среду CLR и .NET, что делает AOP возможным, это определенные узкие случаи. Например, когда вы получаете класс из ContextBoundObject, вы можете использовать ContextAttribute для ввода логики до/после вызова метода в экземпляре CBO. Здесь вы можете увидеть примеры того, как это делается.

Получение класса CBO является раздражающим и ограничительным - и есть еще одна альтернатива. Вы можете использовать такой инструмент, как PostSharp, для применения АОП к любому классу С#. 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 лучшей альтернативой, чем "перепрофилирование", выражение для использования неясно мне.. Я склонен соглашаться с @Eric Lippert, что ключевое слово using обманывает важную семантику вашего кода и накладывает побочные эффекты и семантику на символ } в конце используемого блока - что является неожиданным. Но разве это не отличается от применения AOP-атрибутов к вашему коду? Они также скрывают важную семантику за декларативным синтаксисом... но вот что такое точка АОП.

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

Ответ 7

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

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

Если у вас есть класс 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.

Ответ 11

С помощью шаблона using я могу просто использовать grep (?<!using.*)new\s+Jumper, чтобы найти все места, где может возникнуть проблема.

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

Ответ 12

  • Я не думаю, что вы хотите сделать эти методы статическими
  • Вам нужно проверить, есть ли дескриптор, вызываемый alredy.
  • Если я снова начну прыгать, что произойдет?

Вы можете использовать контрольный счетчик или флаг для отслеживания состояния "прыжка". Некоторые люди говорят, что IDisposable предназначен только для неуправляемых ресурсов, но я думаю, что все в порядке. В противном случае вы должны сделать start и end jumping private и использовать деструктор для работы с конструктором.

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

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