Вопрос: Как выглядит код, когда вы не используете исключения для управления потоком?


Я принял совет, который я видел в других ответах на вопрос о том, когда бросать исключения, но теперь у меня API новый шум. Вместо вызова методов, заключенных в блоки try / catch (досадные исключения), у меня есть параметры аргументов с набором ошибок, которые могли произойти во время обработки. Я понимаю, почему обернуть все в try / catch - это плохой способ контролировать поток приложения, но я редко вижу код где-нибудь, что отражает эту идею.

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

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

public void Save(UserAccount account, UserSubmittedFile file, out IList<ErrorMessage> errors)
{
    PictureData pictureData = _loader.GetPictureData(file, out errors);

    if(errors.Any())
    {
        return;
    }

    pictureData.For(account);

    _repo.Save(pictureData);
}

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

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

Мне бы хотелось, чтобы некоторые рекомендации по этому поводу!

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

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

public UserAccount Create(UserAccount account, out IList<ErrorMessage> errors)
{
    errors = _modelValidator.Validate(account);

    if (errors.Any())
    {
        return null;
    }

    if (_userRepo.UsernameExists(account.Username))
    {
        errors.Add(new ErrorMessage("Username has already been registered."));
        return null;
    }

    account = _userRepo.CreateUserAccount(account);

    return account;
}

Должен ли я использовать какое-то исключение проверки? Или мне следует возвращать сообщения об ошибках?


8


источник


Ответы:


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

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

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

UsernameStatus result = CheckUsernameStatus(username);
if(result == UsernameStatus.Available)
{
    CreateUserAccount(username);
}
else
{
    //update UI with appropriate message
}

enum UsernameStatus
{
    Available=1,
    Taken=2,
    IllegalCharacters=3
}

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


7



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

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


9



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


3



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

Для примера, который вы опубликовали, у меня было бы что-то вроде:

public UserAccount Create(UserAccount account)
{
    if (_userRepo.UsernameExists(account.Username))
        throw new UserNameAlreadyExistsException("username is already in use.");
    else
        return _userRepo.CreateUserAccount(account);
}

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

try
{
    UserAccount newAccount = accountThingy.Create(account);
}
catch (UserNameAlreadyExistsException unaex)
{
    MessageBox.Show(unaex.Message);
    return; // or do whatever here to cancel proceeding
}
catch (SomeOtherCustomException socex)
{
    MessageBox.Show(socex.Message);
    return; // or do whatever here to cancel proceeding
}
// If this is as high up as an exception in the app should bubble up to, 
// I'll catch Exception here too

Это похоже на стиль многих методов System.IO ( http://msdn.microsoft.com/en-us/library/d62kzs03.aspx ) для примера.

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


3



Я бы допустил исключения, но, основываясь на вашей теме, вы искали альтернативу. Почему бы не включить информацию о состоянии или ошибке в свой объект PictureData. Затем вы можете просто вернуть объект с ошибками в нем, а остальные останутся пустыми. Просто предложение, но вы в значительной степени делаете то, какие исключения были сделаны для решения :)


0



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

На языке, подобном C #, который предлагает структурированную обработку исключений, идея состоит в том, чтобы разрешить выявление, распространение и исключение «исключительных» случаев в вашем коде в итоге  обрабатываются. Обработка обычно остается на самом высоком уровне вашего приложения (например, клиент Windows с диалоговыми окнами пользовательского интерфейса и ошибок, веб-сайт со страницами ошибок, средство ведения журнала в цикле сообщений фоновой службы и т. Д.). В отличие от Java, которая использует проверенная обработка исключений, C # не требует, чтобы вы специально обрабатывали каждое исключение, которое может проходить через ваши методы. Напротив, попытка сделать это, несомненно, приведет к некоторым серьезным узким местам производительности, поскольку перехват, обработка и, возможно, повторное броски исключений - это дорогостоящий бизнес.

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

В большинстве случаев хорошо написанное приложение C # не будет иметь столько блоков try / catch в основной бизнес-логике и будет иметь гораздо больше попыток / наконец, или еще лучше, используя блоки. Для большинства кодов озабоченность в ответ на исключение заключается в том, чтобы восстановить красиво, выпуская ресурсы, блокировки и т. Д. И позволяя продолжить исключение. В вашем коде более высокого уровня, обычно во внешнем контуре обработки сообщений приложения или в стандартном обработчике событий для таких систем, как ASP.NET, вы в конечном итоге выполните свою структурированная  обработки с помощью try / catch, возможно с несколькими предложениями catch для устранения конкретных ошибок, требующих уникальной обработки.

Если вы правильно обрабатываете исключения и строительный код, который использует исключения соответствующим образом, вам не придется беспокоиться о множестве блоков try / catch / finally, кодов возврата или кривых меток с множеством параметров ref и out. Вы должны увидеть код более как это:

public void ClientAppMessageLoop()
{
    bool running = true;
    while (running)
    {
        object inputData = GetInputFromUser();
        try
        {
            ServiceLevelMethod(inputData);
        }
        catch (Exception ex)
        {
            // Error occurred, notify user and let them recover
        }
    }
}

// ...

public void ServiceLevelMethod(object someinput)
{
    using (SomeComponentThatsDisposable blah = new SomeComponentThatsDisposable())
    {
        blah.PerformSomeActionThatMayFail(someinput);
    } // Dispose() method on SomeComponentThatsDisposable is called here, critical resource freed regardless of exception
}

// ...

public class SomeComponentThatsDisposable: IDosposable
{
    public void PErformSomeActionThatMayFail(object someinput)
    {
        // Get some critical resource here...

        // OOPS: We forgot to check if someinput is null below, NullReferenceException!
        int hash = someinput.GetHashCode();
        Debug.WriteLine(hash);
    }

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

        // Clean up critical resource if its not null here!
    }
}

Следуя приведенной выше парадигме, у вас нет много беспорядочного кода try / catch по всему миру, но вы все еще «защищены» от исключений, которые в противном случае прерывают ваш обычный поток программ и накапливаются до вашего кода обработки исключений на более высоком уровне.

РЕДАКТИРОВАТЬ:

Хорошая статья, которая охватывает предполагаемое использование исключений и почему исключения не проверены на C #, - это следующее интервью с Андерсом Хейльсбергом, главным архитектором языка C #:

http://www.artima.com/intv/handcuffsP.html

EDIT 2:

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

public PictureDataService: IPictureDataService
{
  public PictureDataService(RepositoryFactory repositoryFactory, LoaderFactory loaderFactory)
  {
     _repositoryFactory = repositoryFactory;
     _loaderFactory = loaderFactory;
  }

  private readonly RepositoryFactory _repositoryFactory;
  private readonly LoaderFactory _loaderFactory;
  private PictureDataRepository _repo;
  private PictureDataLoader _loader;

  public void Save(UserAccount account, UserSubmittedFile file)
  {
    #region Validation
    if (account == null) throw new ArgumentNullException("account");
    if (file == null) throw new ArgumentNullException("file");
    #endregion

    using (PictureDataRepository repo = getRepository())
    using (PictureDataLoader loader = getLoader())
    {
      PictureData pictureData = loader.GetPictureData(file);
      pictureData.For(account);
      repo.Save(pictureData);
    } // Any exceptions cause repo and loader .Dispose() methods 
      // to be called, cleaning up their resources...the exception
      // bubbles up to the client
  }

  private PictureDataRepository getRepository()
  {
    if (_repo == null)
    {
      _repo = _repositoryFactory.GetPictureDataRepository();
    }

    return _repo;
  }

  private PictureDataLoader getLoader()
  {
    if (_loader == null)
    {
        _loader = _loaderFactory.GetPictureDataLoader();
    }

    return _loader;
  }
}

public class PictureDataRepository: IDisposable
{
  public PictureDataRepository(ConnectionFactory connectionFactory)
  {
  }

  private readonly ConnectionFactory _connectionFactory;
  private Connection _connection;

  // ... repository implementation ...

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

    _connection.Close();
    _connection = null; // 'detatch' from this object so GC can clean it up faster
  }
}

public class PictureDataLoader: IDisposable
{
  // ... Similar implementation as PictureDataRepository ...
}

0