What is the smoothest, most appealing syntax you&#

2019-01-30 02:15发布

问题:

A common problem in any language is to assert that parameters sent in to a method meet your requirements, and if they don't, to send nice, informative error messages. This kind of code gets repeated over and over, and we often try to create helpers for it. However, in C#, it seems those helpers are forced to deal with some duplication forced upon us by the language and compiler. To show what I mean, let me present some some raw code with no helpers, followed by one possible helper. Then, I'll point out the duplication in the helper and phrase my question precisely.

First, the code without any helpers:

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
}

}

Now, the code with a reasonable attempt at a helper:

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

The main question is this: Notice how the code has to pass the value of the parameter and the name of the parameter ("firstName" and firstName). This is so the error message can say, "Blah blah blah the value for the firstName parameter." Have you found any way to get around this using reflection or anything else? Or a way to make it less painful?

And more generally, have you found any other ways to streamline this task of validating parameters while reducing code duplication?

EDIT: I've read people talking about making use of the Parameters property, but never quite found a way around the duplication. Anyone have luck with that?

Thanks!

回答1:

This may be somewhat helpful:

Design by contract/C# 4.0/avoiding ArgumentNullException



回答2:

You should check out Code Contracts; they do pretty much exactly what you're asking. Example:

[Pure]
public static double GetDistance(Point p1, Point p2)
{
    CodeContract.RequiresAlways(p1 != null);
    CodeContract.RequiresAlways(p2 != null); 
    // ...
}


回答3:

Wow, I found something really interesting here. Chris above gave a link to another Stack Overflow question. One of the answers there pointed to a blog post which describes how to get code like this:

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];
}

I'm not convinced it is the best answer yet, but it certainly is interesting. Here's the blog post, from Rick Brewster.



回答4:

I tackled this exact problem a few weeks ago, after thinking that it is strange how testing libraries seem to need a million different versions of Assert to make their messages descriptive.

Here's my solution.

Brief summary - given this bit of code:

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

My Assert function can print out the following summary of what is passed to it:

(((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"
      }
    }
  }
}

So all the identifier names and values, and the structure of the expression, could be included in the exception message, without you having to restate them in quoted strings.



回答5:

Alright guys, it's me again, and I found something else that is astonishing and delightful. It is yet another blog post referred to from the other SO question that Chris, above, mentioned.

This guy's approach lets you write this:

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

            // Bootstrap the server
        }
    }

Note that there is no string containing "rootDirectory" and no string containing "serverName"!! And yet his error messages can say something like "The rootDirectory parameter must not be null."

This is exactly what I wanted and more than I hoped for. Here's the link to the guy's blog post.

And the implementation is pretty simple, as follows:

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);
        }
    }

Note that this makes use of "static reflection", so in a tight loop or something, you might want to use Rick Brewster's approach above.

As soon as I post this I'm gonna vote up Chris, and the response to the other SO question. This is some good stuff!!!



回答6:

Using my library The Helper Trinity:

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

    ...
}

Also supports asserting that enumeration parameters are correct, collections and their contents are non-null, string parameters are non-empty etcetera. See the user documentation here for detailed examples.



回答7:

Here's my answer to the problem. I call it "Guard Claws". It uses the IL parser from the Lokad Shared Libs but has a more straightforward approach to stating the actual guard clauses:

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

You can see more examples of it's usage in the specs.

Since it uses real lambdas as input and uses the IL Parser only to generate the exception in the case of a violation it should perform better on the "happy path" than the Expression based designs elsewhere in these answers.

The links are not working, here is the URL: http://github.com/littlebits/guard_claws/



回答8:

The Lokad Shared Libraries also have an IL parsing based implementation of this which avoids having to duplicate the parameter name in a string.

For example:

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

Will throw an exception with the appropriate parameter name if any of the listed arguments is null. It also has a really neat policy based rules implementation.

e.g.

Enforce.Argument(() => username,    StringIs.Limited(3, 64), StringIs.ValidEmail);


回答9:

My preference would be to just evaluate the condition and pass the result rather than passing an expression to be evaluated and the parameter on which to evaluate it. Also, I prefer to have the ability to customize the entire message. Note that these are simply preferences -- I'm not saying that your sample is wrong -- but there are some cases where this is very useful.

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


回答10:

Don't know if this technique transfers from C/C++ to C#, but I've done this with macros:


    #define CHECK_NULL(x)  { (x) != NULL || \
           fprintf(stderr, "The value of %s in %s, line %d is null.\n", \
           #x, __FILENAME__, __LINE__); }
 


回答11:

In this case, rather than use your own exception type, or really general types like ApplicationException.. I think it is best to use the built in exception types that are specifically intended for this use:

Among those.. System.ArgumentException, System.ArgumentNullException...



回答12:

Postsharp or some other AOP framework.



回答13:

It does not apply everywhere, but it might help in many cases:

I suppose that "SomeMethod" is carrying out some behavioral operation on the data "last name", "first name" and "age". Evaluate your current code design. If the three pieces of data are crying for a class, put them into a class. In that class you can also put your checks. This would free "SomeMethod" from input checking.

The end result would be something like this:

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

The call would be something like this (if you use .NET 3.5):

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

under the assumption that the class would look like this:

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);
        }
    }

Instead of the syntactic sugar of .NET 3.5 you can also use constructor arguments to create a Person object.



回答14:

Just as a contrast, this post by Miško Hevery on the Google Testing Blog argues that this kind of parameter checking might not always be a good thing. The resulting debate in the comments also raises some interesting points.



标签: c# c#-3.0 lambda