Creating a Custom Predicate using a String Value for the Numeric Operand

一个人想着一个人 提交于 2019-12-11 14:43:55

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!