Выбрасывание исключения из соответствующего типа

В моем коде у меня часто бывают ситуации вроде этого:

public void MyMethod(string data)
{
    AnotherClass objectOfAnotherClass = GetObject(data);
    if (objectOfAnotherClass == null)
        throw new WhatExceptionType1("objectOfAnotherClass is null.");

    if (objectOfAnotherClass.SomeProperty < 0)
        throw new WhatExceptionType2("SomeProperty must not be negative.");
}

Предположим, что GetObject использует некоторые внешние библиотеки, которые не находятся под моим контролем, и что эта библиотека возвращает null, если объект для data не существует и считает отрицательным SomeProperty как допустимое состояние и, следовательно, doesn 'исключение. Дальше представьте, что MyMethod не может работать без objectOfAnotherClass и не имеет смысла с отрицательным SomeProperty.

Каковы правильные исключения для WhatExceptionType1/2 для этой ситуации?

В основном у меня было четыре варианта:

  • 1) InvalidOperationException, потому что MyMethod не имеет смысла в условиях, описанных выше. С другой стороны, рекомендации (и Intellisense в VS тоже) говорят о том, что необходимо исключить InvalidOperationException, если объект, к которому принадлежит метод, находится в недопустимом состоянии. Теперь сам объект не находится в недопустимом состоянии. Вместо этого входной параметр data и некоторые другие операции, основанные на этом параметре, приводят к ситуации, когда MyMethod больше не может работать.

  • 2) ArgumentException, потому что есть значения для data, метод может работать с другими значениями, которые метод не может. Но я не могу проверить это, проверив только data, я должен вызвать другие операции, прежде чем решиться.

  • 3) Exception, потому что я не знаю, какой другой тип исключения использовать, и потому что все другие предопределенные исключения кажутся слишком специализированными и не подходят для моей ситуации.

  • 4) MyCustomException (мой собственный тип исключения, полученный из Exception). Кажется, это всегда вариант, но я беспокоюсь, что мне приходится определять множество специальных классов исключений для многих разных условий ошибки, когда я начинаю следовать этому шаблону.

Есть ли другие и лучшие варианты? Каковы аргументы в пользу или против этих вариантов?

Благодарим вас за отзыв!

Ответ 1

Будьте внимательны при использовании встроенных типов исключений... они имеют очень специфические значения для платформы .NET, и, если вы не используете его для точно того же значения, лучше свернуть (+1 к Джону Сондерсу за Serializeable).

InvalidOperationException имеет значение:

Исключение, которое вызывается, когда вызов метода недействителен для текущего состояния объекта.

Например, если вы вызываете SqlConnection.Open(), вы получаете InvalidOperationException, если вы не указали источник данных. InvalidOperationException не подходит для вашего сценария.

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

Exception - всего лишь плохая идея, в общем... это дает вызывающему пользователю абсолютно никакой идеи, почему метод не смог создать объект. (В этом отношении, имея только catch (Exception ex) {..}, тоже плохая идея)

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

ObjectCreateException:   // The call to GetObject() returned null<br />
InvalidObjectException:  // The object returned by GetObject() is invalid 
                         // (because the property < 0)

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

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

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

Команда разработчиков .NET решила, что ApplicationException не имеет реальной ценности и не одобряет ее, но мой пурист мне всегда нравился, поэтому он сохраняется в моем коде. Здесь, однако, это действительно не добавляет никакой ценности и только увеличивает глубину наследственной иерархии. Поэтому не стесняйтесь наследовать непосредственно от Exception.

/// <summary>
/// A general, base error for GS3 applications </summary>
[Serializable]
public class Gs3Exception : ApplicationException {


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class </summary>
    public Gs3Exception() {}


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class </summary>
    /// <param name="message">A brief, descriptive message about the error </param>
    public Gs3Exception(string message) : base(message) {}


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class 
    /// when deserializing </summary>
    /// <param name="info">The object that holds the serialized object data </param>
    /// <param name="context">The contextual information about the source or
    ///  destination.</param>
    public Gs3Exception(SerializationInfo info, StreamingContext context) : base(info, context) { }


    /// <summary>
    /// Initializes a new instance of the <see cref="Gs3Exception"/> class
    /// with a message and inner exception </summary>
    /// <param name="Message">A brief, descriptive message about the error </param>
    /// <param name="exc">The exception that triggered the failure </param>
    public Gs3Exception(string Message, Exception exc) : base(Message, exc) { }


}


/// <summary>
/// An object queried in an request was not found </summary>
[Serializable]
public class ObjectNotFoundException : Gs3Application {

    private string objectName = string.Empty;

    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    public ObjectNotFoundException() {}


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    /// <param name="message">A brief, descriptive message about the error</param>
    public ObjectNotFoundException(string message) : base(message) {}


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class </summary>
    /// <param name="ObjectName">Name of the object not found </param>
    /// <param name="message">A brief, descriptive message about the error </param>
    public ObjectNotFoundException(string ObjectName, string message) : this(message) {
        this.ObjectName = ObjectName;
    }


    /// <summary>
    /// Initializes a new instance of the <see cref="ObjectNotFoundException"/> class.
    /// This method is used during deserialization to retrieve properties from 
    /// the serialized data. </summary>
    /// <param name="info">The object that holds the serialized object data.</param>
    /// <param name="context">The contextual information about the source or
    /// destination.</param>
    public ObjectNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) {
        if (null != info) {
            this.objectName = info.GetString("objectName");
        }
    }


    /// <summary>
    /// When serializing, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> 
    /// with information about the exception. </summary>
    /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds 
    /// the serialized object data about the exception being thrown.</param>
    /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
    /// <exception cref="T:System.ArgumentNullException">
    /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic) </exception>
    /// <PermissionSet>
    ///     <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/>
    ///     <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/>
    /// </PermissionSet>
    [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags=SecurityPermissionFlag.SerializationFormatter)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context) {

        base.GetObjectData(info, context);

        //  'info' guaranteed to be non-null (base.GetObjectData() will throw an ArugmentNullException if it is)
        info.AddValue("objectName", this.objectName);

    }


    /// <summary>
    /// Gets or sets the name of the object not found </summary>
    /// <value>The name of the object </value>
    public string ObjectName {
        get { return objectName; }
        set { objectName = value; }
    }

}

PS: Прежде чем кто-нибудь назовет меня на этом, причина базы Gs3Exception, которая добавляет больше значения, чем ApplicationException, является блоком приложений для обработки исключений Enterprise Library... с использованием базового исключения на уровне приложения, мы можем создавать общие правила ведения журналов для исключений, создаваемых непосредственно нашим кодом.

Ответ 2

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

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

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

Ответ 3

Мое голосование будет ArgumentException по крайней мере в первом случае, если не оба; ArgumentExceptions следует вызывать, цитируя, "когда один из аргументов, предоставленных методу, недействителен". Если MyMethod не может использовать аргумент data для создания допустимого экземпляра AnotherClass, как и ожидалось в MyMethod, этот аргумент недействителен для использования в MyMethod.

Поймите, что если вы не планируете перехватывать исключения из разных типов и обрабатывать их по-разному, то точно, какое исключение выбрано, действительно не имеет значения. Некоторые исключения (например, ArgumentNullException) создают настраиваемое сообщение на основе очень небольшой информации, поэтому их легко настроить и локализовать, другие (например, SqlExceptions) имеют более конкретные данные о том, что пошло не так, но все это лишнее, если все, что вы хотите, выкинуть исключение, говорящее "Oops!".

Ответ 4

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

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

Вы нарушаете этот контракт здесь. Вы пытаетесь превратить его из исключительного в исключительное, но не имея понятия, что не так. Самое лучшее, что нужно сделать, - не менять этот контракт и не доводить его до вызывающего вашего метода. В конце концов, тот, который знает, что делает GetObject(). Измените имя метода, используйте слово "Try" и верните bool.

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

Ответ 5

Как об использовании ObjectNotFoundException. Это правильно описывает ситуацию.

Ответ 6

Сначала классифицируйте ошибку. Эрик Липперт в своем блоге имеет лучшую классификацию, которую я видел: фатальный, пронзительный, досадный и экзогенный. Ваше исключение не будет смертельным; это будет одно из: головокружение, досада или экзогенность.

Ошибка имеет смысл, если вы можете сказать, что для правильного ввода name вы знаете, что GetObject вернет объект, который имеет смысл для вашего метода. Другими словами, единственной причиной для этих исключений является ошибка в коде, вызывающем MyMethod. В этом случае на самом деле не имеет значения, какой тип исключения вы используете, потому что в любом случае вы никогда не увидите его в производстве - ArgumentException (если проблема была с name) или InvalidOperationException (если проблема была в состоянии объекта, определяющего MyMethod), будет прекрасным выбором в этой ситуации, но конкретный тип исключения не должен быть документирован (иначе он станет частью API).

Ошибка является досадной, если GetObject является предсказуемым относительно name (т.е. для данного name вы всегда будете получать действительный объект или никогда не получите действительный объект), но name на пользовательском вводе (или конфигурации). В этом случае вы не хотите вообще бросать исключение; вы хотите написать метод TryMyMethod, чтобы вызывающий код не должен был перехватывать исключение, чтобы иметь дело с не исключительной ситуацией.

Ошибка является экзогенной, если поведение GetObject непредсказуемо относительно name: какое-то внешнее воздействие может привести к тому, что name станет действительным или недействительным. В этом случае вам нужно выбрать конкретный тип исключения и документировать его как часть API. ArgumentException не будет хорошим выбором для экзогенного исключения; не было бы InvalidOperationException. Вы должны выбрать что-то более похожее на FileNotFoundException, более подробно описать основную проблему (независимо от того, что GetObject делает).

Ответ 7

Я знаю, что этот ответ далек, слишком поздно, чтобы быть полезным для OP, но я склонен думать, что трудность здесь скорее является запахом кода. Если мы рассмотрим принцип единой ответственности на уровне метода, а не просто класс, то, похоже, этот метод нарушает его. Он делает 2 вещи: анализирует data, а затем выполняет дальнейшую обработку. Поэтому правильная вещь - устранить одну ответственность, а именно разбор. Вы можете сделать это, изменив свой аргумент на экземпляр AnotherClass. Тогда станет ясно, что вы должны делать:

public void MyMethod(AnotherClass data)
{
    if (data == null)
        throw new ArgumentNullException("data is null.");

    if (data.SomeProperty < 0)
        throw new ArgumentException("data.SomeProperty must not be negative.");

    ...
}

Затем становится ответственностью вызывающего абонента, чтобы вызвать метод разбора и справиться с этой ситуацией. Они могут выбрать исключение или предварительно проверить его перед вызовом метода. Вызывающий должен сделать немного больше работы, но с учетом большей гибкости. Например, это также дает возможность разрешить альтернативные конструкции AnotherClass; вызывающие могут создавать его в коде вместо его разбора или вызывать некоторый альтернативный метод синтаксического анализа.

Ответ 8

Это действительно зависит от того, как вы планируете обрабатывать исключения в своих приложениях. Пользовательские исключения хороши в ситуациях try/catch, но попытки/уловы также дороги. Если вы не планируете ловить и не обрабатываете свое настраиваемое исключение, тогда: throw new Exception ( "Index out the range: SomeProperty не должен быть отрицательным" ); так же полезно, как и пользовательское исключение.

public class InvalidStateException : ApplicationException
{
   ...
}

В вашем коде:

// test for null
if(objectOfAnotherClass == null) throw new NullReferenceException("Object cannot be null");

// test for valid state
if(objectOfAnotherClass.SomeProperty < 0) throw new InvalidStateException("Object is in an invalid state");

Ответ 9

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

Дело в том, что каждый метод имеет формальный контракт в том смысле, что если вы удовлетворяете всем предварительным условиям, promises удовлетворяет некоторым постусловиям. Как правило, все предпосылки можно разделить на три типа:

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

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

  • SecurityException
  • InvalidOperationException
  • ArgumentException

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

И, наконец, ошибки Exceptions, которые генерируются CLR, которые могут возникать практически в любом месте, в любое время и практически невозможно предсказать. Они никогда не будут в явном виде частью ваших методов и как таковые никогда не должны быть брошены (не специально обработаны) с помощью кода пользователя. В этом смысле они сопоставляются почти взаимно однозначно с фатальными исключениями Липперта.

Итак, что вы должны сделать, на мой взгляд, в случае, представленном Slauma?

Ну, как вы можете себе представить, это полностью зависит от контрактов MyMethod(data), GetObject(data) и objectOfAnotherClass.SomeProperty. Что говорит API GetObject(data) о том, в каких ситуациях он вернет null (или выбросит собственные исключения)? Каково точное значение и спецификация SomeProperty?

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

С другой стороны, если GetObject(data) указано, что он возвращает только null, когда у вызывающего пользователя недостаточно прав для извлечения объекта, вы можете выбросить SecurityException (или производную), чтобы указать проблему вызывающему абоненту MyMethod(data).

Наконец, если GetObject(data) promises он "никогда" не вернет null, и он все равно сделает это, вы можете разрешить сбой кода с помощью NullReferenceException (потому что праведно полагая, что он никогда не будет null) или обрабатывать ситуацию конкретно, когда вы имеете дело с чувствительным кодом, бросая свой собственный тип исключения (поскольку постусловие от вызывающего объекта MyMethod не сработало).

Второй случай немного сложнее, но к нему можно подойти почти так же. Предположим, что objectOfAnotherClass представил строку или объект, которые были выбраны из базы данных с помощью идентификатора data, а MyMethod(data) четко указано, что data должен указывать идентификатор объекта, где objectOfAnotherClass.SomeProperty >= 0, а затем в этом случае бросать ArgumentException (или производное) будет частью вашего дизайна.

Затем снова, когда MyMethod(data) работает в контексте, где вызывающий может предположить, что действительный идентификатор никогда не вернет объект таким образом, что objectOfAnotherClass.SomeProperty < 0 (или лучше: даже не знает, что такой объект существует), то такое возникновение действительно неожиданно на сайте вызывающего абонента, а MyMethod(data) должен либо не проверять регистр явно (опять же, поскольку предполагается, что это не произойдет), либо, если вы собираетесь использовать более надежный код, бросьте конкретное исключение, указывающее на проблему.

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