问题
I am trying to pass in a string value ("GreaterThan", "Equals", etc.) for a numeric operand expression to a parameter. I have created the code below which works, but it's "clunky". I don't like the if
blocks and I think there's a way to do this with a custom LINQ comparison predicate. I tried to follow the reply posted in this post, but I can't seem to follow it. Any ideas on how to clean up my method?
Here's code showing how I want to pass a string value of "GreaterThan" to a function
var myValues = new Dictionary<string, int> {{"Foo", 1}, {"Bar", 6}};
var failed = DoAnyValuesFail(myValues, "GreaterThan", 4);
Here's the sample method that I wrote that's "clunky":
public bool DoAnyValuesFail(Dictionary<string, int> dictionary, string expression, int failureValue)
{
var failureValues = new List<KeyValuePair<string, int>>();
if (expression == "GreaterThan")
failureValues = dictionary.Where(x => x.Value > failureValue).ToList();
if (expression == "LessThan")
failureValues = dictionary.Where(x => x.Value < failureValue).ToList();
if (expression == "Equals")
failureValues = dictionary.Where(x => x.Value == failureValue).ToList();
return failureValues.Any();
}
--- UPDATE - Final Version ---
I think part of the confusion in the responses below is that I'm not that up to speed on my terminology of fuctions, predicates and delegates. Sorry about that. Regardless, I did want to clarify one thing and that is that the values of "GreaterThan", "LessThan" and "Equals" come from a configuration file, so they need to be "Magic Strings" that adjust at run time.
Therefore, based on the feedback from Matthew Haugen and Enigmativity, I have come up with the following code that I think works best for my needs. I'm open to any suggestions if you think it's wrong or needs adjusted.
// These values actually come from a configuration file... shown here as hard coded just for illustration purposes
var failureValue = 2;
var numericQualifier = "<";
// This comes from my external data source
var myValues = new Dictionary<string, int> { { "Foo", 1 }, { "Bar", 6 } };
// This is the delegate (am I using that term correctly?) called Compare which is setup as an extension method
var failureValues = myValues.Where(x => numericQualifier.Compare()(x.Value, failureValue)).ToList();
if (failureValues.Any())
Console.WriteLine("The following values failed: {0}", string.Join(", ", failureValues));
This then is my Compare
extension method:
public static class MyExtensions
{
public static Func<int, int, bool> Compare(this string expression)
{
switch (expression)
{
case "GreaterThan":
case ">":
return (v, f) => v > f;
case "LessThan":
case "<":
return (v, f) => v < f;
case "Equals":
case "=":
return (v, f) => v == f;
default:
throw new ArgumentException(string.Format("The expression of '{0}' is invalid. Valid values are 'GreaterThan', 'LessThan' or 'Equals' or their respective symbols (>,<,=)", expression));
}
}
}
回答1:
Given your requirement to match the expression against a string I would be inclined to do this:
private Dictionary<string, Func<int, int, bool>> _predicates =
new Dictionary<string, Func<int, int, bool>>
{
{ "GreaterThan", (v, f) => v > f },
{ "LessThan", (v, f) => v < f },
{ "Equals", (v, f) => v == f },
};
public bool DoAnyValuesFail(
Dictionary<string, int> dictionary,
string expression,
int failureValue)
{
return _predicates.ContainsKey(expression)
? dictionary.Any(kvp => _predicates[expression](kvp.Value, failureValue))
: false;
}
However, as others have said I think this is a better option:
public bool DoAnyValuesFail(
Dictionary<string, int> dictionary,
Func<int, bool> predicate)
{
return dictionary.Any(kvp => predicate(kvp.Value));
}
And then simply call it like so:
var failed = DoAnyValuesFail(myValues, x => x > 4);
But then you are only one step away from making it even simpler:
var failed = myValues.Any(x => x.Value > 4);
No DoAnyValuesFail
method required - meaning simpler code, less potential bugs, and no "magic" strings.
This code is much clearer and actually more terse than your original line.
回答2:
I'd start by making it an enum
rather than a `string.
public enum ComparisonType
{
GreaterThan,
LessThan,
Equal,
}
Then, I'd change it to something like this. This will also improve performance, since only one matching value is required to return.
public bool DoAnyValuesFail(Dictionary<string, int> dictionary, ComparisonType expression, int failureValue)
{
switch (expression)
{
case ComparisonType.Equals:
return dictionary.Any(x => x.Value == failureValue);
case ComparisonType.GreaterThan:
return dictionary.Any(x => x.Value > failureValue);
case ComparisonType.LessThan:
return dictionary.Any(x => x.Value < failureValue);
default:
throw new NotSupportedException();
}
}
Of course, it's not all that much cleaner than what you've got. It's probably more reliable than depending on those string
inputs, and that makes it a bit more readable. And not going via the List<>
helps in my opinion. But I don't think there's much you can do beyond that. I mean, you could store the Func<T, bool>
in a value that gets assigned to in the switch
then use it afterward, which would normalize return dictionary.Any(...)
, but I feel like that would make it less readable.
Ultimately I think it's fine as it is. Anything you do with an Expression
will just take away from readability with functionality this simple.
回答3:
You could Rewrite your method signature to use a Delegate like this:
public bool DoAnyValuesFail(Dictionary<string, int> dictionary,Func<int,bool> predicate)
{
var failureValues = new List<KeyValuePair<string, int>>();
failureValues = dictionary.Where(x => predicate(x.Value)).ToList();
return failureValues.Any();
//instead of the code above you could simply do
//return dictionary.Any(x => predicate(x.Value));
}
Then when you call it you supply the desired expression like this:
var myValues = new Dictionary<string, int> { { "Foo", 1 }, { "Bar", 6 } };
var failed = DoAnyValuesFail(myValues, x => x < 4); //4 is what you had as the failureValue
来源:https://stackoverflow.com/questions/26394130/creating-a-custom-predicate-using-a-string-value-for-the-numeric-operand