ArgumentNullException - как упростить?

Я заметил, что этот код очень много выделяется в моих конструкторах:

if (someParam == null) throw new ArgumentNullException("someParam");
if (someOtherParam == null) throw new ArgumentNullException("someOtherParam");
...

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

public static class ExceptionHelpers
{
   public static void CheckAndThrowArgNullEx(IEnumerable<KeyValuePair<string, object>> parameters)
   {
      foreach(var parameter in parameters)
         if(parameter.Value == null) throw new ArgumentNullException(parameter.Key);
   }
}

Однако использование этого будет выглядеть примерно так:

ExceptionHelper.CheckAndThrowArgNullEx(new [] {
    new KeyValuePair<string, object>("someParam", someParam),
    new KeyValuePair<string, object>("someOtherParam", someOtherParam),
    ... });

... что на самом деле не способствует упорядочению кода. Tuple.Create() вместо KVP не работает, потому что GTP файлы Tuple не являются ковариантными (хотя IEnumerable GTP есть). Есть идеи?

Ответ 1

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

Я создал пару статических методов, которые работают с лямбда-выражениями определенной формы (EDIT - небольшое изменение; методы не могут быть общими или они потребуют, чтобы все выражения возвращали один и тот же тип. Func в порядке, с дополнительным условием в методе GetName, чтобы развернуть бросок):

public static class ExpressionReader
{
    /// <summary>
    /// Gets the name of the variable or member specified in the lambda.
    /// </summary>
    /// <param name="expr">The lambda expression to analyze. 
    /// The lambda MUST be of the form ()=>variableName.</param>
    /// <returns></returns>
    public static string GetName(this Expression<Func<object>> expr)
    {
        if (expr.Body.NodeType == ExpressionType.MemberAccess)
            return ((MemberExpression) expr.Body).Member.Name;

        //most value type lambdas will need this because creating the 
        //Expression from the lambda adds a conversion step.
        if (expr.Body.NodeType == ExpressionType.Convert
                && ((UnaryExpression)expr.Body).Operand.NodeType 
                     == ExpressionType.MemberAccess)
            return ((MemberExpression)((UnaryExpression)expr.Body).Operand)
                   .Member.Name;

        throw new ArgumentException(
           "Argument 'expr' must be of the form ()=>variableName.");
    }
}

public static class ExHelper
{
    /// <summary>
    /// Throws an ArgumentNullException if the value of any passed expression is null.
    /// </summary>
    /// <param name="expr">The lambda expressions to analyze. 
    /// The lambdas MUST be of the form ()=>variableName.</param>
    /// <returns></returns>
    public static void CheckForNullArg(params Expression<Func<object>>[] exprs)
    {
        foreach (var expr in exprs)
            if(expr.Compile()() == null)
                throw new ArgumentNullException(expr.GetName());
    }
}

... которые могут быть использованы таким образом:

//usage:

ExHelper.CheckForNullArg(()=>someParam, ()=>someOtherParam);

Это уменьшает шаблон до одной линии, без сторонних инструментов. ExpressionReader и, следовательно, метод генерации исключений работают на любой лямбда формы() => variableName, которая компилируется в вызывающем, что означает, что она работает для локальных переменных, параметров, полей экземпляров и свойств экземпляра, по крайней мере. Я не проверял, работает ли он на статике.

Ответ 2

Обновление для С# 7

Вы можете использовать выражение throw с нулевым коалесцирующим оператором. Вот пример с этой страницы:

public string Name
{
    get => name;
    set => name = value ?? 
        throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null");
}

Оригинальный ответ

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

public static T ThrowIfNull<T>(this T argument, string argumentName)
{
    if (argument == null)
    {
        throw new ArgumentNullException(argumentName);
    }
    return argument;
}

Применение:

this.something = theArgument.ThrowIfNull("theArgument");
// or in C# 6
this.something = theArgument.ThrowIfNull(nameof(theArgument));

(Хотя некоторые люди считают странным вызывать метод расширения для нулевого экземпляра)

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

public static void CheckAndThrowArgNullEx(params object[] argsAndNames)
{
    for (int i = 0; i < argsAndNames.Length; i += 2)
    {
        if (argsAndNames[i] == null)
        {
            string argName = (string)argsAndNames[i + 1];
            throw new ArgumentNullException(argName);
        }
    }
}

и использование будет:

CheckAndThrowArgNullEx(arg1, "arg1", arg2, "arg2");
// or in C# 6
CheckAndThrowArgNullEx(arg1, nameof(arg1), arg2, nameof(arg2));

С другой стороны, как упоминает KeithS в комментариях, было бы лучше реализовать это как набор перегрузок, а не использовать params object[] следующим образом:

static void Check(object arg1, string arg1Name) { ... }
static void Check(object arg1, string arg1Name, object arg2, string arg2Name) { ... }
// and so on...

Ответ 3

Есть несколько способов сделать это.

Вариант A:

Разделите свои функции на две - валидацию и реализацию (вы можете увидеть примеры этого в Jon Skeet EduLinq).

Вариант B:

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

Вариант C:

Использование аспектно-ориентированных технологий, таких как кодовое ткачество, для извлечения этих проверок в один аспект. (как ответил Дж. Торрес).

Вариант D:

Используйте SpeС#, как комментирует CodeInChaos.

Вариант E:

???

Ответ 4

public class TestClass
{
    public TestClass()
    {
       this.ThrowIfNull(t=>t.Str, t=>t.Test);
       //OR
       //this.ThrowIfNull(t => t.X)
       //    .ThrowIfNull(t => t.Test);
    }
    string Str = "";
    public TestClass Test {set;get;}
}


public static class SOExtension
{
    public static T ThrowIfNull<T>(this T target, params Expression<Func<T, object>>[] exprs)
    {
        foreach (var e in exprs)
        {
            var exp = e.Body as MemberExpression;
            if (exp == null)
            {
                throw new ArgumentException("Argument 'expr' must be of the form x=>x.variableName");
            }

            var name = exp.Member.Name;
            if (e.Compile()(target) == null)
                throw new ArgumentNullException(name,"Parameter [" + name + "] can not be null");

        }
        return target;
    }
}

Ответ 5

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

Обновление: см. Новые функции Validating-parameters в PostSharp 3

Ответ 6

Как насчет метода расширения?

public static void ThrowExceptionIfNull(this object argument, string argumentName)
{
    if(argument == null)
        throw new ArgumentNullException(argumentName);
} 

Тогда ваш код, по крайней мере, читается немного плавно:

someParam.ThrowExceptionIfNull("someParam");

В противном случае я бы согласился с другими, чтобы разделить функциональность или использовать AOP (то есть PostSharp)

Ответ 7

Уже существует много правильных решений, но здесь я беру:

using System.Diagnostics;
using System.Reflection;

public SomeConstructor(int? par1, int? par2, string par3)
{
    CheckThrowNull(par1, par2, par3);
    //rest of constructor code...
}

///<param name="values"> Values must be given in order </param>
public static void CheckThrowNull(params object[] values)
{
    StackTrace stackTrace = new StackTrace();
    ParameterInfo[] parameters = stackTrace.GetFrame(1).GetMethod().GetParameters(); //get calling method parameters (or constructor)
    if (parameters.Length != values.Length)
    {
        throw new ArgumentException("Incorrect number of values passed in");
    }
    for (int i = 0; i < parameters.Length; i++)
    {
        if (values[i] == null)
        {   
            //value was null, throw exception with corresponding parameter name
            throw new ArgumentNullException(parameters[i].Name);
        }
    }
}

Общая идея заключается в том, что установлены два параллельных массива, один из которых - ParameterInfo, и один, содержащий значения параметра. Последнее должно пройти, потому что значения параметров нелегко (и, я думаю, невозможно), получаемые посредством отражения. Чтобы дать кредит там, где это произошло, я нашел способ получить метод вызова здесь: http://www.csharp-examples.net/reflection-calling-method-name/

Лично мне не нравится использовать System.Diagnosics, кроме отладки, поэтому я бы сделал небольшую модификацию, получив код:

CheckThrowNull(MethodBase.GetCurrentMethod(), par1, par2, par3);

и способ, являющийся

CheckThrowNull(MethodBase method, params object[] values)
{
    ParameterInfo[] parameters = method.GetParameters();
    //rest of code same
}

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

Ответ 8

Попробуйте следующее: Одна строка.

accounts = accounts ?? throw new ArgumentNullException(nameof(accounts));

Кроме того, используйте nameof(), если переменная когда-либо переименована, вам не придется выслеживать все "переменные", пусть это имя nameof().

Ответ 10

Я написал тестовое приложение с несколькими вариантами извлечения имени аргумента (через анонимный класс + reflection/MemberExpression/Func/etc)

Github ссылается на контрольные источники: https://github.com/iXab3r/NullCheckCompetition

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

.NET 40/X64

Ошибка (т.е. Выполняется метод IS null и метод извлечения имени)

  • FailAnonymousClass 67.87 нс
  • FailDoubleLambda 643.98 ns
  • FailLazyAnonymousClass 69.36 ns
  • FailRawCheck 1,08 нс
  • FailSingleLambda 643.27 нс

Успех (т.е. Аргумент не равен нулю)

  • SuccessAnonymousClass 6.33 нс
  • SuccessDoubleLambda 8.48 нс
  • SuccessLazyAnonymousClass 8.78 нс
  • SuccessRawCheck 1,08 нс
  • SuccessSingleLambda 628.28 нс

Ответ 11

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

static void SampleMethod(string arg1)
{
    ThrowIfNull(() => arg1);
    // continue to other normal stuff here...
}

public static void ThrowIfNull<T>(Func<T> lambda) 
    where T : class
{
    if (lambda() == null)
    {
        throw new ArgumentNullException(lambda.Target.GetType().GetFields()[0].Name);
    }
}

Ответ 12

Я думаю, что большинство из них в порядке, но ни один из них на самом деле не улучшает то, что у вас уже есть, поэтому я бы просто пошел на KIS, Keep It Simple, и с этого вы начали.

Он чистый, чрезвычайно читаемый и быстрый. Единственное, что это немного