Класс статического броска: хорошая или плохая практика

#c# #design-patterns #exception

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

Вопрос:

Выбрасывание исключений часто происходит по следующему шаблону:

 if(condition) { throw exception; }
  

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

 public static class Throw
{
    public static void IfNullOrEmpty<T>(string @string, params object[] parameters) where T : Exception
    {
        Throw.If<T>(string.IsNullOrEmpty(@string), parameters);
    }

    public static void IfNullOrEmpty<T, I>(IEnumerable<I> enumerable, params object[] parameters) where T : Exception
    {
        Throw.If<T>(enumerable == null || enumerable.Count() == 0, parameters);
    }

    public static void IfNullOrEmpty(string @string, string argumentName)
    {
        Throw.IfNullOrEmpty(@string, argumentName, 
            string.Format("Argument '{0}' cannot be null or empty.", argumentName));
    }

    public static void IfNullOrEmpty(string @string, string argumentName, string message)
    {
        Throw.IfNullOrEmpty<ArgumentNullOrEmptyException>(@string, message, argumentName);
    }

    public static void IfNullOrEmpty<I>(IEnumerable<I> enumerable, string argumentName)
    {
        Throw.IfNullOrEmpty(enumerable, argumentName, 
            string.Format("Argument '{0}' cannot be null or empty.", argumentName));
    }

    public static void IfNullOrEmpty<I>(IEnumerable<I> enumerable, string argumentName, string message)
    {
        Throw.IfNullOrEmpty<ArgumentNullOrEmptyException, I>(enumerable, message, argumentName);
    }


    public static void IfNull<T>(object @object, params object[] parameters) where T : Exception
    {
        Throw.If<T>(@object == null, parameters);
    }

    public static void If<T>(bool condition, params object[] parameters) where T : Exception
    {
        if (condition) 
        {
            var types = new List<Type>();
            var args = new List<object>();
            foreach (object p in parameters ?? Enumerable.Empty<object>())
            {
                types.Add(p.GetType());
                args.Add(p);
            }

            var constructor = typeof(T).GetConstructor(types.ToArray());
            var exception = constructor.Invoke(args.ToArray()) as T;
            throw exception;
        }
    }

    public static void IfNull(object @object, string argumentName)
    {
        Throw.IfNull<ArgumentNullException>(@object, argumentName);
    }
}
  

(Примечание: ArgumentNullOrEmptyException здесь не определено, но оно делает в значительной степени то, что можно было бы ожидать.)

поэтому вместо того, чтобы постоянно писать подобные вещи

 void SomeFunction(string someParameter)
{
   if(string.IsNullOrEmpty(someParameter))
   {
      throw new ArgumentNullOrEmptyException("someParameter", "Argument 'someParameter' cannot be null or empty.");
   }
}
  

я просто делаю

 void SomeFunction(string someParameter)
{
   Throw.IfNullOrEmpty(someParameter, "someParameter"); // not .IsNullOrEmpty
}
  

мне это действительно нравится, но является ли это также хорошей практикой?

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

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

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

3. Я делал подобное раньше, но я назвал его Guard. и использовался атрибут [DebuggerStepThrough]

4. @Brian — 1 к вашему комментарию. Отличное дополнение.

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

Ответ №1:

Таким образом вы избавляетесь от небольшого дублирования кода (if … throw), так что в этом смысле это хорошая идея. Просто имейте в виду, что людям, работающим над кодом, потребуется знать Throw API, чтобы иметь возможность читать и понимать код.

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

Например, в моем текущем любимом проекте у меня есть этот класс защиты (немного сокращенный):

 public static class Guard
{
    public static void NotNullOrEmpty(Expression<Func<string>> parameterExpression)
    {
        string value = parameterExpression.Compile()();
        if (String.IsNullOrWhiteSpace(value))
        {
            string name = GetParameterName(parameterExpression);
            throw new ArgumentException("Cannot be null or empty", name);
        }
    }

    public static void NotNull<T>(Expression<Func<T>> parameterExpression)
        where T : class
    {
        if (null == parameterExpression.Compile()())
        {
            string name = GetParameterName(parameterExpression);
            throw new ArgumentNullException(name);
        }
    }

    private static string GetParameterName<T>(Expression<Func<T>> parameterExpression)
    {
        dynamic body = parameterExpression.Body;
        return body.Member.Name;
    }
}
  

Который я могу затем использовать следующим образом:

 Guard.NotNull(() => someParameter);
  

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

1. Разве использование деревьев выражений таким образом — скажем, один, два или три охранника вверху над каждым методом — не добавляет изрядных накладных расходов? (Для среды выполнения MSIL ?) Обычно я не забочусь о таких вещах, но, похоже, это могло бы легко скрыть сложность…

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

Ответ №2:

В этом шаблоне нет ничего плохого, и я видел, как это делается в ряде приложений. В основном это вопрос личного стиля.

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

 if (...) {
  throw new ArgumentExecption("paramName", LoadSomeResource(ErrorId));
}
  

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

Ответ №3:

Вместо этого я бы рассмотрел возможность использования кодовых контрактов.

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

1. @pst — Вы можете использовать Microsoft. Контракты. dll, которая равна 3.5.

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

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