Какой синтаксис является наиболее плавным и привлекательным для подтверждения правильности параметров в C #?

Распространенной проблемой на любом языке является утверждение, что параметры, отправленные в метод, соответствуют вашим требованиям, а если нет, то отправка красивых информативных сообщений об ошибках. Такой код повторяется снова и снова, и мы часто пытаемся создать для него помощников. Однако в C # кажется, что эти помощники вынуждены иметь дело с некоторым дублированием, навязанным нам языком и компилятором. Чтобы показать, что я имею в виду, позвольте мне представить некоторый необработанный код без помощников, за которым следует один возможный помощник. Затем я укажу на дублирование в помощнике и точно сформулирую свой вопрос.

Во-первых, код без помощников:

public void SomeMethod(string firstName, string lastName, int age)
{
     if(firstName == null)
     {
          throw new WhateverException("The value for firstName cannot be null.");
     }

     if(lastName == null)
     {
          throw new WhateverException("The value for lastName cannot be null.");
     }

     // Same kind of code for age, making sure it is a reasonable range (< 150, for example).
     // You get the idea
}

}

Теперь код с разумной попыткой помощи:

public void SomeMethod(string firstName, string lastName, int age)
{
      Helper.Validate( x=> x !=null, "firstName", firstName);
      Helper.Validate( x=> x!= null, "lastName", lastName);
}

Главный вопрос заключается в следующем: обратите внимание, как код должен передавать значение параметра и имя параметра ("firstName" и имя). Таким образом, в сообщении об ошибке может быть сказано: «Бла-бла-бла, значение параметра firstName». Вы нашли способ обойти это с помощью отражения или чего-то еще? Или способ сделать это менее болезненным?

И в более общем плане, нашли ли вы какие-либо другие способы упростить задачу проверки параметров при одновременном уменьшении дублирования кода?

РЕДАКТИРОВАТЬ: Я читал людей, говорящих об использовании свойства Parameters, но так и не нашел способа обойти дублирование. Кому-нибудь повезло с этим?

Спасибо!


person Charlie Flowers    schedule 21.03.2009    source источник
comment
Не рекомендуется использовать класс ApplicationException.   -  person mmx    schedule 21.03.2009
comment
Хорошо, я изменил его на Исключение, просто чтобы не отвлекаться. Кстати, если рекомендация основана на комментариях парней, написавших книгу Framework Guidelines, то я не совсем уверен (хотя эти ребята написали несколько хороших вещей). Но это обсуждение в другой день.   -  person Charlie Flowers    schedule 21.03.2009
comment
Хенк: Неправильно. Любое использование ApplicationException не рекомендуется. См. Раздел «Примечания» здесь, особенно второй абзац: msdn.microsoft.com / ru-ru / library /   -  person Ryan Lundy    schedule 14.12.2009
comment
@Kyralessa, если это единственное упоминание об этом, это довольно надуманные аргументы. Нет одной веской причины, почему вы не должны использовать исключение приложения в этом разделе примечаний. В нем даже прямо говорится, что изначально предполагалось, что настраиваемые исключения должны быть производными от класса ApplicationException; однако на практике не было обнаружено, что это дает значительную ценность. поэтому в основном MS признала, что класс ApplicationException не нужен, а не то, что его было плохо использовать.   -  person Chris Marisic    schedule 16.03.2011


Ответы (14)


Это может быть в некоторой степени полезно:

Дизайн по контракту / C # 4.0 / без исключения ArgumentNullException

person core    schedule 21.03.2009
comment
Спасибо! Это привело к двум подходам, которые поразили меня! И я могу использовать их оба. - person Charlie Flowers; 22.03.2009
comment
Эта реализация плавной проверки довольно хороша, да. - person core; 22.03.2009
comment
Да, это очень красиво. Я не понимал, что вы можете вызвать метод расширения для переменной, имеющей значение NULL. Я не знаю, хотел ли Андерс, чтобы это произошло, или это побочный эффект. В любом случае это очень полезно. - person Charlie Flowers; 22.03.2009
comment
Чарли, да, я все думал, хорошо, теперь это не может быть правдой. А как насчет исключения NullReferenceException? пока я не прокрутил вниз, чтобы увидеть методы расширения. Классное дело. - person core; 23.03.2009
comment
Этот ответ научил меня больше всего. Должен отдать должное, если причитается. Спасибо всем за отличные отзывы! - person Charlie Flowers; 24.03.2009

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

[Pure]
public static double GetDistance(Point p1, Point p2)
{
    CodeContract.RequiresAlways(p1 != null);
    CodeContract.RequiresAlways(p2 != null); 
    // ...
}
person John Feminella    schedule 21.03.2009
comment
Эй, это круто ... тем более, что это будет встроено в язык C # 4.0. Но все же ... что мы можем сделать тем временем? - person Charlie Flowers; 21.03.2009
comment
Что ж, они будут встроены в .NET 4, но вы все еще можете использовать их прямо сейчас. Они не зависят от чего-либо специфического для .NET 4, поэтому вы можете полностью вернуться к версии 2.0, если хотите. Проект находится здесь: research.microsoft.com/en-us/projects/contracts < / а> - person John Feminella; 21.03.2009
comment
Вы можете работать со своими собственными классами, которые выглядят так, но генерировать исключения для библиотек. Довольно просто написать его. - person user7116; 22.03.2009
comment
Но пробовали ли вы когда-нибудь использовать кодовые контракты в более крупных проектах? К сожалению, его нельзя использовать, время компиляции слишком увеличивается. - person Mic; 10.08.2012

Вау, я нашел здесь кое-что действительно интересное. Крис выше дал ссылку на другой вопрос о переполнении стека. Один из ответов указывал на сообщение в блоге, в котором описывается, как получить такой код:

public static void Copy<T>(T[] dst, long dstOffset, T[] src, long srcOffset, long length)
{
    Validate.Begin()
            .IsNotNull(dst, “dst”)
            .IsNotNull(src, “src”)
            .Check()
            .IsPositive(length)
            .IsIndexInRange(dst, dstOffset, “dstOffset”)
            .IsIndexInRange(dst, dstOffset + length, “dstOffset + length”)
            .IsIndexInRange(src, srcOffset, “srcOffset”)
            .IsIndexInRange(src, srcOffset + length, “srcOffset + length”)
            .Check();

    for (int di = dstOffset; di < dstOffset + length; ++di)
        dst[di] = src[di - dstOffset + srcOffset];
}

Я не уверен, что это лучший ответ, но он определенно интересен. Вот сообщение в блоге , от Рика Брюстера.

person Charlie Flowers    schedule 22.03.2009

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

Вот мое решение.

Краткое изложение - с учетом этого фрагмента кода:

int x = 3;
string t = "hi";
Assert(() => 5*x + (2 / t.Length) < 99);

Функция My Assert может распечатать следующую сводку того, что ей передано:

(((5 * x) + (2 / t.Length)) < 99) == True where
{
  ((5 * x) + (2 / t.Length)) == 16 where
  {
    (5 * x) == 15 where
    {
      x == 3
    }
    (2 / t.Length) == 1 where
    {
      t.Length == 2 where
      {
        t == "hi"
      }
    }
  }
}

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

person Daniel Earwicker    schedule 21.03.2009
comment
Вау, отлично. Не знаю, почему я не подумал об этом (хорошие идеи часто вызывают это чувство). Я пишу приложение для расчетов, для которого это может быть очень хорошо с точки зрения объяснения полученных ответов. - person Charlie Flowers; 21.03.2009
comment
Комбинация деревьев отражений и выражений позволяет многое, что застает вас врасплох, особенно если (как и я) вы пришли из фона C / C ++, где среда выполнения не существует. Мы должны заново изобрести кучу вещей, которыми Лисперы занимались на протяжении полувека! - person Daniel Earwicker; 21.03.2009
comment
Хорошая идея, и ее тоже можно использовать для калькуляционного приложения! - person flq; 21.03.2009

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

Подход этого парня позволяет вам написать следующее:

public class WebServer
    {
        public void BootstrapServer( int port, string rootDirectory, string serverName )
        {
            Guard.IsNotNull( () => rootDirectory );
            Guard.IsNotNull( () => serverName );

            // Bootstrap the server
        }
    }

Обратите внимание, что нет строки, содержащей «rootDirectory», и нет строки, содержащей «serverName» !! И все же его сообщения об ошибках могут содержать что-то вроде «Параметр rootDirectory не должен быть пустым».

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

И реализация довольно проста, а именно:

public static class Guard
    {
        public static void IsNotNull<T>(Expression<Func<T>> expr)
        {
            // expression value != default of T
            if (!expr.Compile()().Equals(default(T)))
                return;

            var param = (MemberExpression) expr.Body;
            throw new ArgumentNullException(param.Member.Name);
        }
    }

Обратите внимание, что здесь используется «статическое отражение», поэтому в узком цикле или в чем-то подобном вы можете захотеть использовать подход Рика Брюстера, описанный выше.

Как только я опубликую это, я проголосую за Криса и ответ на другой вопрос SO. Это хорошая штука !!!

person Charlie Flowers    schedule 22.03.2009
comment
Привет, я немного поздно заметил это, но я думаю, что ему немного не хватает общей силы выражений. Посмотрите на мой ответ еще раз - вам не нужно писать отдельную функцию для каждого типа утверждений, которые вы, возможно, захотите сделать. Просто напишите выражение в обычном синтаксисе C #: X.Assert(() => serverName != null); Нет веских причин накапливать набор специальных методов (IsNull, IsNotNull, AreEqual и т. Д.) По одному для каждого типа утверждения, потому что мы можем получить полное описание любого выражения и всего значения в нем, каким бы ни было выражение. - person Daniel Earwicker; 06.08.2009
comment
Эрвикер, это интересное наблюдение. Мне это нравится, хотя это не хитрый данк, и могут быть случаи, когда я выберу многометодный подход (IsNull, IsNotNull и т. Д.) Или, по крайней мере, я буду спорить с самим собой по этому поводу. Сообщение об ошибке может быть красивее, говоря, что FirstName не может быть нулевым, чем утверждение (FirstName! = Null) не удалось. Кроме того, если у вас есть сотня мест, где вы утверждаете (something! = Null), это похоже на дублирование кода. Я не уверен, является ли это вредоносным дублированием или нет. Зависит, наверное. Но прикольное наблюдение, спасибо. - person Charlie Flowers; 17.08.2009

Используя мою библиотеку The Helper Trinity:

public void SomeMethod(string firstName, string lastName, int age)
{
    firstName.AssertNotNull("firstName");
    lastName.AssertNotNull("lastName");

    ...
}

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

person Kent Boogaart    schedule 21.03.2009
comment
+1. Мне нравится использование методов расширения для минимизации количества слов. Вместо этого я бы назвал метод MustNotBeNull или что-то в этом роде, но это просто личные предпочтения - person Orion Edwards; 22.03.2009
comment
Что происходит в этом коде, если firstName имеет значение null? Разве не последовало бы NullRef? Никогда не пытался проверить, могут ли методы расширения по-прежнему присоединяться к нулевому указателю. - person Jarrod Dixon♦; 22.03.2009
comment
@Jarrod, методы расширения являются статическими, поэтому в методе расширения вы можете проверить, имеет ли цель this значение null. - person ProfK; 22.03.2009
comment
@ProfK - круто! +1 к этому ответу, тогда. - person Jarrod Dixon♦; 23.03.2009
comment
Я также не знал, что вы можете вызвать метод расширения для null, пока не увидел здесь некоторые ответы. Это фантастика, потому что открывает множество вещей, которые приводят к более плавному синтаксису. - person Charlie Flowers; 23.03.2009

Вот мой ответ на проблему. Я называю это «Guard Claws». Он использует синтаксический анализатор IL из общих библиотек Lokad, но имеет более простой подход к формулировке фактических защитных предложений:

string test = null;
Claws.NotNull(() => test);

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

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

Ссылки не работают, вот URL: http://github.com/littlebits/guard_claws/

person brendanjerwin    schedule 05.07.2009
comment
Прохладный. Есть ли планы использовать params args для проверки нескольких переменных? - person Charlie Flowers; 07.07.2009

Общие библиотеки Lokad также имеет реализацию на основе синтаксического анализа IL, которая позволяет избежать дублирования имени параметра в строке.

Например:

Enforce.Arguments(() => controller, () => viewManager,() => workspace);

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

e.g.

Enforce.Argument(() => username,    StringIs.Limited(3, 64), StringIs.ValidEmail);
person Nick Cox    schedule 18.06.2009

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

Helper.Validate( firstName != null || !string.IsNullOrEmpty(directoryID),
                 "The value for firstName cannot be null if a directory ID is not supplied." );
person tvanfosson    schedule 21.03.2009

Не знаю, переносится ли эта техника с C / C ++ на C #, но я сделал это с помощью макросов:


    #define CHECK_NULL(x)  { (x) != NULL || \
           fprintf(stderr, "The value of %s in %s, line %d is null.\n", \
           #x, __FILENAME__, __LINE__); }
 
person Adam Liss    schedule 21.03.2009
comment
Это именно то, на что я надеюсь, но не думаю, что это можно сделать напрямую в C #. - person Charlie Flowers; 21.03.2009
comment
C # не поддерживает уловки препроцессора, которые есть в компиляторах C ++. - person Brian Rasmussen; 21.03.2009

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

Среди них .. System.ArgumentException, System.ArgumentNullException ...

person markt    schedule 21.03.2009
comment
Хорошо, но пожалуйста ... не будем зацикливаться на типе исключения. Это вспомогательный элемент по отношению к основной проблеме, поэтому я просто набрал первое, что пришло мне в голову. - person Charlie Flowers; 21.03.2009
comment
Хорошо ... но я считаю, что всякий раз, когда вы генерируете исключения, создание полезного типа исключения абсолютно необходимо. - person markt; 21.03.2009

Postsharp или другой AOP фреймворк.

person Kris Erickson    schedule 21.03.2009

Это применимо не везде, но может помочь во многих случаях:

Я предполагаю, что SomeMethod выполняет какую-то поведенческую операцию с данными «фамилия», «имя» и «возраст». Оцените свой текущий дизайн кода. Если три части данных требуют класса, поместите их в класс. В этом классе вы также можете поставить свои чеки. Это освободит SomeMethod от проверки ввода.

Конечный результат будет примерно таким:

public void SomeMethod(Person person)
{
    person.CheckInvariants();
    // code here ...
}

Вызов будет примерно таким (если вы используете .NET 3.5):

SomeMethod(new Person { FirstName = "Joe", LastName = "White", Age = 12 });

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

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    public void CheckInvariants()
    {
        assertNotNull(FirstName, "first name");
        assertNotNull(LastName, "last name");
    }

    // here are your checks ...
    private void assertNotNull(string input, string hint)
    {
        if (input == null)
        {
            string message = string.Format("The given {0} is null.", hint);
            throw new ApplicationException(message);
        }
    }

Вместо синтаксического сахара .NET 3.5 вы также можете использовать аргументы конструктора для создания объекта Person.

person Theo Lenndorff    schedule 22.03.2009

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

person Henrik Gustafsson    schedule 22.03.2009
comment
В этой точке зрения есть правда. Я не обязательно говорю о строгом соблюдении контрактов (я думаю, что в некоторых случаях это хорошо, но не во всех). Но все же некоторые методы должны выполнять определенные проверки, и часто вам нужны помощники для этих случаев. - person Charlie Flowers; 23.03.2009
comment
Конечно, такого рода проверки полезны во многих ситуациях, я просто хотел получить противоположное мнение для будущих поколений :) - person Henrik Gustafsson; 23.03.2009