Гарантии безопасности исключений

    Ошибки в обработке ошибок являются наиболее распространенным источником ошибок

    Бредня, пришедшая в голову при написании этой статьи

    Основные баталии по поводу того, что лучше использовать при программировании на C# – исключения или коды возврата для обработки, ушли в далекое прошлое (*), но до сих пор не утихают баталии другого рода: да, хорошо, мы остановились на обработке исключений, но как же нам их обрабатывать «правильно»?

    Существует множество точек зрения о том, что же такое «правильно», большая часть из которых сводится к тому, что нужно перехватывать только те исключения, которые ты можешь обработать, а все остальные пробрасывать вызывающему коду. Ну, а в случае, если на верхний уровень пробралось дерзким образом непонятное исключение, то стрелять все приложение целиком, поскольку уже не понятно, находится ли оно, родимое, в согласованном состоянии или нет.

    Существует множество «за» и «против» такого способа перехвата и обработки исключений, но сегодня я хочу рассмотреть несколько другую тему. А именно тему обеспечения согласованного состояния приложения в свете возникновения исключения – три уровня безопасности исключений.


    Три типа гарантий



    В конце 90-х годов Дейв Абрахамс (Dave Abrahams) предложил три уровня безопасности исключений: базовая гарантия, строгая гарантия и гарантия отсутствия исключений. Эта идея была тепло встречена сообществом С++ разработчиков, а после ее популяризации (и некоторой модификации) Гербом Саттером, гарантии безопасности исключений стали широко применяться в boost-е, в стандартной библиотеке С++, а также при разработке прикладных приложений.

    Изначально эти гарантии были предложены Дейвом Абрахамсом для реализации библиотеки STLPort на языке С++, но сама идея безопасности исключений не привязана к конкретному языку программирования и может использоваться в других языках, использующих исключения в качестве основного механизма обработки ошибок, таких как Java или C#. Кроме того, в настоящее время существует две версии определений гарантии безопасности исключений: (1) исходная версия, предложенная Дейвом Абрахамсом и (2) модифицированная версия, популяризированная Саттером и Страуструпом, и более подходящая не только для библиотек, но и для прикладных приложений.

    Базовая гарантия


    Исходное определение: “в случае возникновения исключений не должно быть никаких утечек ресурсов”.

    Современное определение: “при возникновении любого исключения в некотором методе, состояние программы должно оставаться согласованным”. Это означает, не только отсутствие утечек ресурсов, но и сохранение инвариантов класса, что является более общим критерием, по сравнению с базовым определением.

    Разница между этими двумя формулировками обусловлена тем, что изначально эта гарантия были предложена для реализации библиотеки на языке С++ и не имела никакого отношения к прикладным приложениями. Но если говорить о более общем случае (т.е. о приложении, а не только о библиотеке), то можно сказать, что утечки ресурсов является лишь одним из источников багов, но далеко не единственным. Сохранение инварианта в любой устойчивый момент времени (**) является залогом того, что никакой внешний код не сможет «увидеть» рассогласованного состояния приложения, что, согласитесь, не менее важно, чем отсутствие утечек ресурсов. Мало какого пользователя банковского приложения будут интересовать утечки памяти, если при переводе денег с одного счета на другой, деньги могут «уйти» с одного счета, но «не дойти» до другого.

    Строгая гарантия


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

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

    Гарантия отсутствия исключений


    Гарантия отсутствия исключений сводится к следующему: “ни при каких обстоятельствах функция не будет генерировать исключения”.

    Эта гарантия наиболее простая с точки зрения определения, однако, она не так проста, как кажется. Во-первых, ее практически невозможно обеспечить в общем случае, особенно в среде .Net, когда исключение может произойти практически в любой точке приложения. На практике, лишь единицы операций следуют этой гарантии, и именно на основании таких операций строятся гарантии предыдущих уровней. В языке C#, то одной из немногих операций, обеспечивающих эту гарантию, является присваивание ссылок, а в языке C++ — функция swap, реализующая обмен значений. Именно на основании этих функций зачастую и реализуется строгая гарантия исключений, когда вся «грязная работа» выполняется во временном объекте, который затем присваивается результирующему значению.

    Во-вторых, в некоторых случаях невозможно обеспечить нормальную работу других функций, если некоторые операции не будут следовать гарантии отсутствия исключений. Так, например, в языке С++, для обеспечения даже базовой гарантии исключений (а точнее утечки ресурсов) в контейнерах необходимо, чтобы деструктор пользовательского типа не генерировал исключения.

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

    Теперь давайте рассмотрим несколько примеров.

    Примеры нарушения базовой гарантии



    Главным способом предотвращения утечек памяти и ресурсов в языке C++ является идиома RAII (Resource Acquisition Is Initialization), которая заключается в том, что объект захватывает ресурс в конструкторе и освобождает его в деструкторе. А поскольку вызов деструктора осуществляется автоматически при выходе объекта из области видимости по любой причине, в том числе и при возникновении исключения, то неудивительно, что эта же идиома используется и для обеспечения безопасности исключений.

    В язык C# эта идиома перекочевала в виде интерфейса IDisposable и конструкции using, однако, в отличие от С++ она применима для управления временем жизни ресурса в некоторой области видимости, и не подходит для управления множеством ресурсов, захватываемых в конструкторе.

    Давайте рассмотрим такой пример:

    // Некоторый класс, содержащий управляемые ресурсы
    class DisposableA : IDisposable
    {
      public void Dispose() {}
    }

    // Еще один класс с управляемыми ресурсами
    class DisposableB : IDisposable
    {
      public DisposableB()
      {
        disposableA = new DisposableA();
        throw new Exception("OOPS!");
      }

      public void Dispose() {}

      private DisposableA disposableA;
    }

    // Где-то в приложении
    using (var disposable = new DisposableB())
    {
      // Упс! Метод Dispose не будет вызван ни для
      // DisposableB, ни для DisposableA
    }

    * This source code was highlighted with Source Code Highlighter.


    Итак, у нас есть два disposable-класса: DisposableA и DisposableB, каждый из которых захватывает некоторый управляемый ресурс в конструкторе и освобождает его в методе Dispose. Давайте пока не будем рассматривать финализатор, поскольку он никак не поможет нам гарантировать детерминированный порядок освобождения ресурсов, что в некотором случае является жизненно важным.

    В данном случае, при генерации исключения конструктором класса DisposableB мы никогда не вызовем метод Dispose, поскольку объект disposable никогда не существовал. В этом плане поведение у большинства mainstream языков программирования более или менее одинаковое, но есть и некоторые отличия. Сходство заключается в том, что если конструктор «упадет», то вызывающий код так и не сможет получить ссылку на еще не сконструированный объект и явно освободить его ресурсы. Однако, в отличие от языка С++, в котором вызов деструктора полностью сконструированных полей осуществляется автоматически, в «управляемом» языке C# этого не происходит (***).Если конструктор класса DisposableB сгенерирует исключение и не освободит уже захваченные ранее ресурсы самостоятельно, мы получим «утечку ресурсов» (или, как минимум, недетерминированное их освобождение).

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

    class Base : IDisposable
    {
      public Base()
      {
        // Захватываем некоторый ресурс
      }
      public void Dispose() {}    
    }

    class Derived : Base, IDisposable
    {
      public Derived(object data)
      {
        if (data == null)
          throw new ArgumentNullException("data");
        // OOPS!!
      }
    }
    // И снова где-то в приложении
    using (var derived = new Derived(null))
    {}

    * This source code was highlighted with Source Code Highlighter.


    Генерация исключения в конструкторе класса Derived нарушает базовую гарантию исключений и приводит к утечке ресурсов, поскольку метод Dispose класса Base не вызывается (****). Опять таки, поскольку компилятор знает об интерфейсе IDisposable только через призму конструкции using, то во всех случаях, когда disposable объект является полем другого класса, за вызов метода Dispose отвечает только программист.

    Помимо базового класса, подобную же шутку может сыграть и инициализаторы полей, когда конструктор одного из полей может генерировать исключение:

    class ComposedDisposable : IDisposable
    {
      public void Dispose() {}

      private readonly DisposableA disposableA = new DisposableA();
      // А что, если конструктор DisposableB упадет? OOPS!!
      private readonly DisposableB disposableB = new DisposableB();
    }

    * This source code was highlighted with Source Code Highlighter.


    В данном случае, если конструктор класса DisposableB при инициализации поля disposableB сгенерирует исключение, то перехватить его и освободить уже захваченные ресурсы будет невозможно. В С++ существует такая вещь, как перехват исключений, возникших в списке инициализации (см. Exception and Member Initialization), однако такая возможность в языке C# отсутствует, поэтому выход из этой ситуации один: старайтесь ее не допускать.

    Что касается всех предыдущих случаев, то обеспечение базовой гарантии исключений полностью ложится на плечи разработчика, поскольку никакого «сахара» для этих целей язык C# не предоставляет. Все, что нам остается, это либо создавать подобъекты в нужном порядке и disposable поле создавать в самом конце конструктора, либо оборачивать их создание в блок try/catch и очищать все ресурсы, в случае возникновения исключения.

    Пример строгой гарантии исключений. Object initializer и collection initializer



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

    Инициализатор объектов и коллекций (object initializer и collection initializer) обеспечивают атомарность создания и инициализации объекта или заполнения коллекции списком элементов. Давайте рассмотрим следующий пример.

    class Person
    {
      public string FirstName { get; set; }
      public string LastName { get; set; }
      public int Age { get; set; }
    }

    var person = new Person
    {
      FirstName = "Bill",
      LastName = "Gates",
      Age = 55,
    };

    * This source code was highlighted with Source Code Highlighter.


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

    var person = new Person();
    person.FirstName = "Bill";
    person.LastName = "Gates";
    person.Age = 55;

    * This source code was highlighted with Source Code Highlighter.


    Однако на самом деле, при вызове инициализатора объекта создается временная переменная, затем изменяются свойства именно этой переменной, и только потом она присваивается новому объекту:

    var tmpPerson = new Person();
    tmpPerson.FirstName = "Bill";
    tmpPerson.LastName = "Gates";
    tmpPerson.Age = 55;
    var person = tmpPerson;

    * This source code was highlighted with Source Code Highlighter.


    Это обеспечивает атомарность процесса создания объекта и невозможность использования частично инициализированного объекта в случае генерации исключения одним из setter-ов. Аналогичный принцип лежит и в инициализаторе коллекций, когда объекты добавляются во временную коллекцию и лишь после ее заполнения временная переменная присваивается новому объекту.

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

    Заключение



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

    Если говорить о практическом применении данных гарантий, то следует помнить несколько моментов. Во-первых, код, не выполняющий базовую гарантию исключений некорректен; на его основе просто невозможно создать приложение, чье состояние не будет разламываться при его использовании или изменении (*****). Во-вторых, не стоит параноить и добиваться максимальной гарантии. Добиться гарантии отсутствия исключений на 100% вообще практически невозможно из-за наличия асинхронных исключений, но даже реализация строгой гарантии во многих случаях может быть неоправданно дорогой.

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

    ----------------------

    (*) На самом деле, «горячих» дебатов, в общем-то, и не было по одной простой причине: вы не можете программировать на платформе .Net без обработки исключений. Подобные дебаты актуальны, например, в языке C++, особенно, когда речь заходит о низкоуровневом программировании.

    (**) В общем случае, никто не требует сохранение инварианта всегда; обычно требуется сохранение инварианта «до» и «после» вызова открытого метода, но совсем необязательно его сохранение после вызова закрытого метода, выполняющего лишь «часть» работы.

    (***) Может показаться весьма забавным тот факт, что более «навороченный» язык, такой как C# может не делать чего-то, что делает старина С++, но это действительно так. Давайте в качестве примера, перепишем рассмотренный ранее код с C# на C++:

    class Resource1
    {
    public:
      Resource1()
      {
        // Захватываем некоторый ресурс, будь-то выделяем память
        // в куче или создаем дескриптор ОС
      }
      ~Resource1()
      {
        // Освобождаем захваченный ресурс
      }
    };

    class Resource2
    {
    public:
      Resource2()
      {
        // В этой точке кода объект resource1_ уже проинициализирован
        throw std::exception("Yahoo!");
      }
    private:
      Resource1 resource1_;
    };


    // где-то в приложении создаем экземпляр класса Resource2
    Resource2 resource2;


    * This source code was highlighted with Source Code Highlighter.


    Как уже было сказано ранее в языке С++ (в отличие от языка C#), при генерации исключения в конструкторе класса деструкторы уже сконструированных полей (т.е. подобъектов) будут вызваны автоматически. Это значит, что в данном случае вызов деструктора объекта Resource1 произойдет автоматически и никаких утечек ресурсов не будет.

    Такие отличия в поведения языков C# и C++ легко объяснимо. В языке С++ ресурсом является все, включая динамически выделенную память, поэтому и средства управления ресурсами находятся на более высоком уровне. Прикладной же программист, работающий с языком C#, значительно чаще использует ресурсы в блоке using, нежели захватывает ресурсы в конструкторе. И если же он сталкивается с такой задачей, то решить ее придется ему самостоятельно, без помощи компилятора.

    Кстати, Герб Саттер уже рассказывал об этом когда-то в своей заметке: “Constructor Exceptions in C++, C#, and Java”.

    (****) Может я уже и достал с этими примечаниями, но это достаточно важное и, кажется, предпоследнее. Подобный пример достаточно часто любят задавать на собеседованиях, так что теперь, мои читатели знают на него правильный ответ!

    (*****) Все сказанное в этой статье относится только к синхронным исключениям, поскольку гарантировать согласованное возникновении «асинхронных» исключений, таких как OutOfMemoryException или ThreadAbortException практически невозможно. За пруфом сюда: «О вреде метода Thread.Abort».

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 813 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 41

      +3
      ИМХО перепутаны два понятия, согласованность состояния и освобождение ресурсов.

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

      2) Dispose(), в отличие от деструктора Си++, обычный метод который можно вызвать обычным образом. Ничто не мешает вручную написать try/finally вручную вызывать Dispose() самостоятельно. Более того, try/catch можно написать в конструкторе и вызвать Dispose() из catch. причём одно не исключает друогого, так как Dispose() по спецификации может быть вызван многократно.

      3) Ну и самое главное замечание, можно сказать что в статье есть фактическая ошибка. В C# есть то, чего нет в Си++, Constrained Execution Regions. Это как раз возможность множество операций объединить в непрерываемый исключениями блок операций. Так что далеко не только присваивание ссылки может быть использовано.
        0
        На самом деле я хотел лишь сказать, что утечки ресурсов есть лишь частный случай рассогласованного состояния приложения. Но вот перепутанность понятий я не заметил. Ни по одному из приведенных тобой пунктов у меня нет никаких возражений, поскольку все это так и есть. И об этом, вроде как и идет речь. Разве нет?

        По поводу 1): Да, так и есть и я об этом же и пишу, вот пруф из статьи:
        В язык C# эта идиома (речь идет о RAII) перекочевала в виде интерфейса IDisposable и конструкции using, однако, в отличие от С++ она применима для управления временем жизни ресурса в некоторой области видимости, и не подходит для управления множеством ресурсов, захватываемых в конструкторе.

        Но ведь если мы в конструкторе не освободим уже захваченные ресурсы мы получим утечку ресурсов, что можно рассматривать, как рассогласованное состояние приложения: инвариант объекта не получен, но количество доступных системных ресурсов уменьшено (например, файл остался открыт, что приведет к ошибке в другом месте приложения).

        По поводу 2) Да, так и есть, вот еще один пруф:
        Все, что нам остается, это либо создавать подобъекты в нужном порядке и disposable поле создавать в самом конце конструктора, либо оборачивать их создание в блок try/catch и очищать все ресурсы, в случае возникновения исключения.


        По поводу 3) Да, в .Net-е действительно есть Constrained Execution Regions, которые способны обеспечить все рассматриваемые в статье гарантии, не только для синхронных исключений, но и для асинхронных. Просто вопрос в том, что в прикладном приложении усилия обеспечения таких гарантий просто не окупятся, поскольку большинство классов .Net Framework таких гарантий для асинхронных исключений не предоставляют.

        Пруф по этому поводу находится в одной из ссылок, в приведенной заметке «О вреде метода Thread.Abort», а именно в статье Джо Даффи Atomicity and asynchronous exception failures, в которой он пишет следующее:
        We don’t expect most of the Framework to be willing or able to recover fully from asynchronous exception.

        З.Ы. За фидбек спасибо!
          +1
          Ну вот смотри. У тебя написано “при возникновении любого исключения в некотором методе, состояние программы должно оставаться согласованным” и в качестве примера для нарушения базовой гарантии использование IDisposable и отсутствие вызова Dispose().

          Я бы привёл другой пример, где исключение вызывает не отсутствие вызова Dispose(), а его неправильную работу, из-за несогласованности полей класса. Собственно, это как раз то для чего нужен CER. Что-то вроде

          class DisposableX: IDisposable
          {

          DisposableX()
          {
          _handleA = OpenHandle(...);
          // Тут исключение по любой причине.
          _handleB = OpenHandle(_handleA, ...);
          }

          void Dispose()
          {
          CloseHandle(_handleB); // ага, сейчас, он же IntPtr.Zero.
          CloseHandle(_handleA);
          }

          }
            0
            Да, согласен, возможно пример и не самый удачный, но осознанный. Во-первых, хотел показать, что даже в таком высокоуровневом языке как C# можно довольно легко допустить утечки ресурсов. Кроме того, я упоминал пример имено рассогласования состояния приложения, когда при переводе денег с одного счета на другой мы получаем рассогласованное состояние из-за исключения.

            Возможно стоило привести подобный пример:

            public void TransferMoney(Acount anotherAccount, Decimal amount)
            {
            this.Withdraw(amount);
            // Если вторая часть операции упадет, то получим рассогласованное состояние
            anotherAccount.Deposit(amount);
            }


            Но он показался уж слишком банальным.

            Кстати, твой пример не совсем подходит, поскольку твой объект работает с неуправляемыми ресурсами, а в этом случае нам понадобится финализатор, который будет вызван даже если конструктор объекта «упадет» (это еще одно важное отличие между С++ и C#). Так что в твоем случае, достаточно корректно очистить ресурсы в финализаторе (вызвав что-то вроде Dispose(false)), и сделать нечто подобное (хотя, мы опять таки получим недетерминированный характер очистки):

            ~DisposableX() {
            }

            private void Dispose(bool disposing)
            {
            // В общем, на этот флаг нам, в общем-то плевать, поскольку мы работаем только
            // с неуправляемыми ресурсами...
            if (_handleA != IntPtr.Zero)
            CloseHandle(_handleA);
            if (_handleB != IntPtr.Zero)
            CloseHandle(_handleB);
            }


            В общем, с неуправляемыми ресурсами, все же несколько другая песня. Да и юзаются они значительно реже, нежели управляемые.
          0
          к 3му пункту — причём, если правильно помню, там ещё можно ещё до входа в try/catch блок проверить, хватит ли ресурсов для работы метода (это не точная проверка, конечно, но определённые гарантии даёт).
            0
            Это только для выделения здоровенных кусков памяти относится. Речь идет о классе MemoryFailPoint.
          –2
          __Микрософт не рекомендует использовать var нигде кроме как в случае анонимных типов.
            +1
            Пруф в студию, пожалуйста. А то я как ни гляну в блог Эрика, так он все о противоположном пишет чего-то. Видно не понимает ничего в этом:) Вот, кстати, отличная статья об этом: Uses and misuses of implicit typing и вот она же на русском.

            А если серьезно, какое это имеет отношение к рассматриваемой теме?
              +1
              Скорее они рекомендуют использовать его там, где явно указывается тип возвращаемого значения:

              var sr = new StreamReader();

              но не

              var sr = GetSomeCustomReader();
                0
                Пруфлинк:
                > The var keyword can also be useful when the specific type of the variable is tedious to type on the keyboard, or is obvious, or does not add to the readability of the code. One example where var is helpful in this manner is with nested generic types such as those used with group operations. In the following query, the type of the query variable is IEnumerable<IGrouping<string, Student>>. As long as you and others who must maintain your code understand this, there is no problem with using implicit typing for convenience and brevity.
                  +2
                  Я, все же, не могу с этим полностью согласиться по нескольким причинам:
                  Во-первых, следуя этому пруфу в моем коде нет неверного использования var-а, поскольку все типы «obvious».
                  Во-вторых, если следовать такой логике, то программировать на F# вообще невозможно, поскольку там типы явно не указываются.
                  В-третьих, MSDN — далеко не лучший советчик. Просто посмотрите на примеры кода для большинства классов и попробуйте их использовать без модификаций в своих продуктах, объясняя это тем, что в MSDN-е так написано. У меня следующие авторитеты: здравый смысл, собственный опыт, Липпер, Скитт и им подобные. Анонимусы в MSDN-е идут в конце списка.
                  В-четвертых, как показывает практика, зачастую дополнительная информация о типе легко доступна (достаточно подвести мышару к var-у), и не несет дополнительной полезной информации. Я предпочитаю такое правило (кстати, именно оно записано в нашем корпоративном Style guideline): используйте var всегда, и используйте явную типизацию только тогда, когда без этого читать код становится проблематично. Практика показывает, что в большинстве случаев использование var-а нисколько не конфьюзит читателя.
                  В-пятых, это тотальный офтоп, который не имеет ни малейшего отношения к рассматриваемой теме:)
              0
              Не лучше ли было бы явно указать тип в этой строчке?
              var person = tmpPerson;

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

              Извиняюсь, что чуть не по теме.
                +2
                Как, собственно, пишет Липперт, совершенно не важно, какого типа переменная, важно, что она хранит.

                «Если давать имена переменным с типом в имени»
                Это не «тип в имени», это конкретный объект. person — это не «объект типа Person», это «персона».
                0
                Всё гениальное просто…
                  +1
                  Да, здесь много тонкостей, спасибо за хороший обзор.

                  Насколько я знаю, рекомендуется не захватывать ресурсы в конструкторе. Конструктор должен только создавать (конструировать) объект. (Кажется, это ещё в «Совершенном коде» рекомендовалось. Не помню точно.)
                  Пример такого подхода — SqlConnection: при создании объекта реальное соединение с базой данных не будет установлено, для этого нужно сначала вызвать Open().

                  В случае, если нужно в одной строчке создавать объект и захватывать ресурс, лучше использовать статические методы или методы другого класса. Пример — File.Open().

                  Такой подход позволит упростить работу с ресурсами и снизить риск утечек.
                    +3
                    На мой взгляд довольно хорошее и простое решение
                    0
                    Спасибо. Единственное, возник вопрос по поводу асинхронных исключений:
                    Все сказанное в этой статье относится только к синхронным исключениям, поскольку гарантировать согласованное возникновении «асинхронных» исключений, таких как OutOfMemoryException или ThreadAbortException практически невозможно.

                    Имеется ввиду то, что гарантировать отсутствие таких исключений нельзя, поскольку возникнуть они могут практически в любой момент?
                      +1
                      А, разобрался уже. Дело в том, что такие исключения могут происходить между инструкицями присваивания, например, за счет чего даже стандартные конструкции типа
                      using (Obj obj = new Obj()
                      {
                      }
                      могут быть небезопасными.
                        +1
                        Да, совершенно верно. Мы не можем обеспечить сами даже базовую гарантию безопасности асинхронных исклюений, поскольку основные строительные блоки, на основе которых строится наше приложение, такую гарантию не обеспечивают. Речь идет о классах .Net Framework, которые могут оказаться в рассогласованном состоянии из-за возникновения асинхронного исключения в процессе их работы.
                      0
                      кто-нибудь может сказать чем же закончились ушедшие в прошлое баталии «исключения vs коды ошибок»?
                      Я юзаю исключения или кастомные исключения, но на работе часто требуют, чтобы я не обрабатывал ошибки с помощью исключений (разумеется, я это делаю в разумных пределах), только какой-то фатал эрор или нал-референс в аргументе. Хотя у нас остается некорректные значения аргументов, ненайденные объекты в базе, которые не позволяют продолжать нормальный процесс работы, ошибки валидации и т.д.
                      на MSDN пишут, что исключения применять нужно очень избирательно и именно эта фраза вводит в заблуждение: где границы той избирательности на практике.
                      Очень бы хотелось именно примеров и экспертных мнений
                        0
                        «кто-нибудь может сказать чем же закончились ушедшие в прошлое баталии «исключения vs коды ошибок»?»
                        В .net все очень просто — только исключения. Просто потому, что попробуйте найти стандартную функцию в core library, которая возвращает код ошибки, а не бросает исключение.
                          0
                          Как мне назвали проблему: в случае исключения может падать IIS а его подъем — тяжка задача для системы (возможно я сформулировал несколько криво, уже не помню точно, но смысь примерно такой была), а вот с техлидом не поспоришь, хотя я лично замерял скорость обработки исключений vs кодов ошибок (return 1,2,3)
                          Да, отличие от 5 до 100 раз в зависимости от кол-во исключений/секунду, но ни кто не будет генерировать 10000-20000 исключений в секунду в нормальном режиме и во-вторых эти 10 000 исключений обрабатываются за 0,002 секунды.
                            0
                            «в случае исключения может падать IIS»
                            Обманули. Ничего никуда не падает. Не говоря уже о том, что исключения можно (и нужно) обрабатывать.
                              0
                              Не обманули, скорее — очень раздули проблему. Например, исключение на потоке из пула убивает IIS, точнее, процесс, обслуживающий Application Pool:
                              ThreadPool.QueueUserWorkItem(delegate { throw new Exception(); });

                              И тут же в логах: Faulting application name: w3wp.exe, version: 7.5.7601.17514, time stamp: 0x4ce7afa2…

                              Но это не настолько серьезный аргумент, чтобы переходить с исключений на коды ошибок.
                                0
                                Оооо, потоки из пула. Те самые, которые на iis вообще использовать не рекомендуется, если мне память не изменяет.
                                  0
                                  Где и кем не рекомендуется? В MSDN? Покажите ссылку, пожалуйста. Вы же не гуглите каждый метод CLR на предмет безопасности под IIS?
                                  Пример валит IIS, в статье по QueueUserWorkItem предупреждения нет — этого достаточно, чтобы кто-то наступил на грабли.

                                  Стандартный TransactionScope использует потоки из пула для реализации таймаутов. Любой зарегистрированный в нем ресурс может кинуть исключение в момент rollback-а, и точно так же утянет за собой w3wp. И, да, я видел такое на своем проекте, на живой системе. TransactionScope тоже не рекомендуется использовать?
                                    0
                                    «Где и кем не рекомендуется?»
                                    Здравым смыслом. Мы аккурат на той неделе это гуглили (да, именно этот метод на пример безопасности под IIS), потому что где-то в подкорке есть опасения относительно многопоточности (любой) в IIS. И были правы, потому что использование тредпула может привести к голоданию обработчика запросов iis, что не благо совсем.

                                    То, что вы привели — красивый, показательный и очень полезный пример, который надо держать в голове, да. Спасибо.

                                    Но тем не менее самостоятельно использовать потоки в web я все равно не буду.

                                    (Ну и IIS-то все равно не падает)
                                      0
                                      Мне здравый смысл подсказывал, что использовать TransactionScope — безопасно. У вас есть 100% уверенность, что в приложении косвенно не используется пул? Где граница использования потоков? TPL можно использовать? PLINQ? Как насчет стандартных Async Pages?
                                      При исключении в таске в TPL — приложение падает при вызове финализатора, что еще интереснее в отладке.

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

                                      >> (Ну и IIS-то все равно не падает)
                                      Я же проверил перед тем, как написать комментарий. И DevServer, и IIS7 под .NET 4 Integrated Mode падают при вписывании строчки в Page_Load на стандартном шаблоне сайта.
                                        0
                                        «Где граница использования потоков?»
                                        Там, где начинаются потоки, а не IOCP. Последнее время я пребываю в устойчивой уверенности, что никакую обработку на IIS параллелить не надо, чтобы не трогать его бедный процесс обработки входящих. Если надо что-то реально параллелить — в отдельный сервис, с которым асинхронно общаться. А обычно там ничего настолько cpu-bound не бывает, наибольшая часть операций — IO-bound, а эти надо вешать на IOCP.

                                        «Я же проверил перед тем, как написать комментарий. И DevServer, и IIS7 под .NET 4 Integrated Mode падают при вписывании строчки в Page_Load на стандартном шаблоне сайта.»
                                        Как вы же написали выше, падает не IIS, а процесс, обслуживающий этот apppool. Некоторая разница.
                                  0
                                  На самом деле, до меня внезапно дошло, что, собственно, мы тут на пустом месте это обсуждаем, потому что как раз в порожденных потоках исключения использовать рекомендуется очень аккуратно (то есть, ловить).

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

                                  (впрочем, я выше писал — «исключения можно (и нужно) обрабатывать»)
                                  0
                                  обрабатывать нужно. Вот это «считается» и нагружает объекты
                                    +1
                                    Я, если честно, не понял, про какое «считается» вы говорите, и что нагружает объекты.

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

                                    При этом на самом верхнем уровне все исключения ловятся, аккуратно обрабатываются и кладутся куда положено.
                                  0
                                  Хмм, странная информация… в случае исключения в веб-приложении никакой IIS естественно не падает :)
                                    0
                                    речь идёт о пуле приложений в IIS. PashaPash верно определил
                              –6
                              Я всё чаще испытываю чувство, что история развития языков и методологий программирования сделала очень странный виток. Этот пост — тому яркая иллюстрация.Обработка исключений. Господа, это уже не смешно, то что вы тут обсуждаете — это азбука Delphi. Все это было придумано, обсуждено, пережевано, осмысленно и внедрено более 10 лет назад.
                                +3
                                Не ради стебу, а справедливости для. Исходная статья Абрахамса датирована 1996-м годом (т.е. 15 лет назад), проектированию по контракту более 20 лет, функциональщине более 50-ти. Но это не мешает нам переоткрывать все эти вещи для себя.

                                Кстати, напомню, что здесь речь идет не просто об «обработке исключений», а о гарантии безопасности исключений. Так что, если не затруднит, скиньте пруфы на «азбуку Delphi», в которой обсуждаются именно гарантии безопасности исключений?

                                З.Ы. Напомню, что в среде С++ программистов — это старый и известный баян, но вот в среде C# программистов эта тема далеко не так распространена.
                                  –1
                                  Гарантии безопасности в Delphi не обсуждаются, т.к. VCL исповедует одну единственную строгую гарантию, хотя явно об этом не говорится. За внешние ресурсы и библиотеки конечно никто поручится не сможет.
                                  В зависимости от квалификации програмиста программа может плавать от полного отсутствия гарантий вообще (говнокод) до строгой гарантии.

                                  Что касается обработки исключений, то достаточно знать всего 3 правила:
                                  1. Любая инструкция может вызвать исключение
                                  1.1. Следовательно, выделение ресурсов и работа с ними должна находится в секции try, а освобождение — в секции finally
                                  2. При возникновении исключения в конструкторе будет автоматически вызван деструктор.
                                  3. Вызов .Free у нулевого объекта не вызовет исключение.

                                  Всё! Следование этим правилам на всех уровня приложения даёт как минимум базовую гарантию безопасности исключений.
                                    +3
                                    Троллинг детектед:)

                                    Базовая гарантия исключений не достигается банальным растыкиванием блоков try/catch/finally.

                                    Банальный пример:

                                    // Метод класса Account
                                    public void Transfer(Account account, Decimal value)
                                    {
                                    try {
                                    Withdraw(value);
                                    account.Deposit(value);
                                    }
                                    catch(Exception e)
                                    {
                                    // А!!! Че делать! А давайте просто запишем ошибку в лог или пробросим ее дальше
                                    log.WriteError(e);
                                    throw;
                                    }
                                    }
                                    Думаете такого не бывает? Я встречаю такой код постоянно. Так вот, исключения здесь обрабатываются, но базовая гарантия исключений не соблюдается.

                                    З.Ы. Повторюсь еще раз. В статье речь не только и не столько об управлении ресурсами, сколько о гарантиях обеспечения согласованного состояния приложения (что есть более общий случай).
                                      –1
                                      Даже не планировал вас сейчас троллить, честное слово :)

                                      Кстати а где в приведенном коде утечка ресурсов? Я не слишком силен в С#.

                                      PS: да, ваша правда, статья несколько шире
                                        +1
                                        > З.Ы. Повторюсь еще раз. В статье речь не только и не столько об управлении ресурсами, сколько о гарантиях обеспечения согласованного состояния приложения (что есть более общий случай).

                                        Я привел пример, когда мы не получаем утечку ресурсов, но получаем рассогласованное состояние приложения, что и есть нарушение базовой гарантии безопасности исключений. (и что более важно в общем случае)
                                          0
                                          Так в вашем примере неправильно обработка исключений производится. в секции catch надо было устранить рассогласование. Или не помещать туда withdraw. Кстати что это за метод?

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое