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

Mar 08, 2011 19:00

Представим следующий код:

SomeMethod(xxx someArgument)
{
...
switch(someArgument.SomeEnumProperty)
{
case xxx:
...
case yyy:
...
default:
throw new ???????????????? // какой тип?
}
...
}

Т.е. в метод передается некоторый объект, свойство которого (типа Enum) имеет недопустимое для данного метода значение. Такая ситуация может быть, когда метод сознательно допускает только определенные значения перечисления (и не может быть выполнен/не должен вызываться при других значениях); либо когда делается "перестраховка на будущее", т.е. если в будущем добавится новый элемент перечисления, не поддерживаемый данным методом, то метод сразу об этом сообщит с помощью исключения, после чего разработчик сможет либо исправить вызывающий код, либо этот метод, так чтобы он стал поддерживать новое значение перечисления.

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

На ум сразу приходят следующие варианты:
- ArgumentException
- ArgumentOutOfRangeException
- InvalidEnumArgumentException
- NotSupportedException
- InvalidOperationException
Ну а дальше кто во что горазд. Даже в уважаемых open source проектах встречались разные ApplicationException :) и т.д.

Рассмотрим каждый из этих типов исключений и решим какие из них подходят (более или менее) или не подходят для описанной ситуации.

Начнем с ArgumentException.

Вот что написано про него в MSDN:
The exception that is thrown when one of the arguments provided to a method is not valid.
Т.е. это исключение выбрасывается, если аргумент переданный методу является недопустимым. Так как объект представляет собой совокупность его членов, то под недопустимостью в данном контексте можно понимать и ситуацию, когда одно из свойств объекта принимает недопустимое для нашего метода значение. Т.е. в целом данный тип исключения подходит для нашей ситуации.

Рассмотрим ArgumentOutOfRangeException:

The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.
Вольный перевод: исключение выбрасывается, когда значение аргумента находится вне допустимого для вызываемого метода диапазона значений.
На мой взгляд этот тип исключения подразумевает, что значение самого аргумента (а не его свойства) должно принадлежать определенному диапазону, т.е. что сам аргумент должен быть числом или каким-то перечислимым типом. В нашей же ситуации аргумент может являться любым объектом, да и проблема не в самом аргументе, а в его свойстве, поэтому я считаю, что данный тип исключения не очень подходит для рассматриваемой ситуации.

InvalidEnumArgumentException:

В MSDN ошибочно написано:
The exception thrown when using invalid arguments that are enumerators.
Тут конечно должно быть не enumerators, а enumerations, ну да простим им эту опечатку :)
Название исключения сначала воодушевляет (по крайней мере меня), но проблема, как и в случае с ArgumentOutOfRangeException в том, что данный тип исключения как бы намекает, что сам аргумент должен быть перечислением, а не его свойство. Так что думаю, что, к сожалению, и этот типа исключения нам не подходит.

NotSupportedException:

The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality.
Предложение "method is not supported" - это не наш случай, все-таки метод-то наш поддерживается, проблема просто в свойстве аргумента. А вот вторая часть описания ("does not support the invoked functionality") более интересная.
Представим, что у нас есть объект конфигурации типа MessageConfiguration, описывающий настройки для отправки сообщения. Одним из свойств этого объекта может быть какое-то перечисление, например свойство AttachmentCompression { None, Zip, 7Zip } (для особо въедливых читателей: да, я понимаю что такое перечисление может нарушать Open/Closed Principle, но это лишь пример, да и не всегда нужно сильно париться по поводу OCP). Т.е. как-то так:

class MessageConfiguration
{
...
public AttachmentCompression AttachmentCompression { get; set; }
...
}

enum AttachmentCompression
{
None,
Zip,
7Zip
}
Представим, что сообщение из нашей программы можно отправлять разными путями (в виде email через smtp, через веб-сервис и т.д.) и в одном из вариантов не поддерживается сжатие attachment'а. По-моему это подходит под слова "does not support the invoked functionality" и в некоторых ситуациях NotSupportedException можно бросать при недопустимом значении перечисления. Однако проблема в том, что в нашем исходном примере мы хотим бросить исключение из ветки default, что скорее подразумевает, что мы "нарвались" на нераспознанное значение. NotSupportedException же лучше кидать если значение распознано (мы знаем о нем и допускаем, что оно может нам попасться), но сознательно не поддерживается, например в таком коде:

switch(someArg.SomeEnumProperty)
{
case A:
...
case B:
...
case C:
case D:
// мы знаем про значения C & D, но не поддерживаем их в этом методе
throw new NotSupportedException(...);
  default:
// на случай если передали несуществующее значение перечисление
// либо если в будущем добавилось значение, не поддерживаемое текущей версией метода
throw new ArgumentException(...);
}
Таким образом, NotSupportedException не очень подходит для нашего случая (когда исключение кидается из ветки default), хотя его использование допустимо в других случаях.

Давайте посмотрим на описание InvalidOperationException из MSDN:

The exception that is thrown when a method call is invalid for the object's current state.
Т.е. данный тип исключения должен использоваться, когда вызов метода недопустим при текущем соостоянии объекта, метод которого вызывается. Например, если мы закрыли закрыли соединение, мы не можем использовать метод отправки данных. Или наоборот, мы не можем использовать методы передачи данных, если мы только создали объект, но физически еще не открыли соединение. Поэтому, InvalidOperationException не подходит для нашего примера.

Справедливости ради, можно отметить, что InvalidOperationException можно использовать, например, в такой ситуации:

DoOperation(OperationConfiguration oc, ...)
{
switch(oc.OperationType)
{
case OperationType.CreateCommand:
if(_state == State.Closed)
throw new InvalidOperationException("Can not create command if connection is not open.");
...
}
}
Но такая ситуация уже отлична от рассматриваемой изначально. Т.е. InvalidOperationException может использоваться при недопустимом для некоторого состояния объекта значении перечисления, но этот пост все-таки о другой ситуации.

Итоги

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

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

SomeMethod(SomeEnumType arg, ...)
то в такой ситуации при недопустимом значении аргумента-перечисления можно было бы выбросить уже и ArgumentOutOfRangeException и InvalidEnumArgumentException, т.к. их описания подходят под такую ситуацию. Если же говорить более конкретно, то в такой ситуации я бы выбрал именно InvalidEnumArgumentException как наиболее точно описывающий ситуацию тип исключения.

Вот такие вот размышления. Надеюсь, что данный пост окажется кому-то полезным. И если теперь хотя бы одним ApplicationException в случае недопустимого значения перечисления станет меньше, то значит я написал все это не зря :)
Previous post Next post
Up