Обеспечение вызова «конец» всякий раз, когда есть соответствующий вызов «старт» - PullRequest
7 голосов
/ 28 апреля 2010

Допустим, я хочу применить правило:

Каждый раз, когда вы вызываете «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
}

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

Ответы [ 12 ]

14 голосов
/ 28 апреля 2010

По сути, проблема в следующем:

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

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

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

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

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

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

Так что я сделал именно то, что вы сделали. Начните подавлять отчеты об ошибках, но не забудьте остановить подавление отчетов об ошибках.

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

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

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

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

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

9 голосов
/ 28 апреля 2010

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

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

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, если это тривиально.

8 голосов
/ 28 апреля 2010

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

public static void Jump(Action<Jumper> jumpAction)
{
    StartJumping();
    Jumper j = new Jumper();
    jumpAction(j);
    EndJumping();
}
6 голосов
/ 28 апреля 2010

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

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

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

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

4 голосов
/ 28 апреля 2010

Джеф,

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

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

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

4 голосов
/ 28 апреля 2010

На самом деле я не вижу в этом злоупотребления 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 в C ++, где деструктор всегда вызывается. using это самое близкое, что вы можете получить в C #, я думаю.

2 голосов
/ 28 апреля 2010

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

1 голос
/ 29 апреля 2010

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

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

1 голос
/ 28 апреля 2010

Я прокомментировал некоторые ответы здесь немного о том, что такое 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.

1 голос
/ 28 апреля 2010

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

...