How to pass a predicate as parameter c#

2019-07-01 10:18发布

问题:

How can I pass a predicate into a method but also have it work if no predicate is passed? I thought maybe something like this, but it doesn't seem to be correct.

private bool NoFilter() { return true; }

private List<thing> GetItems(Predicate<thing> filter = new Predicate<thing>(NoFilter))
{
    return rawList.Where(filter).ToList();
}

回答1:

private List<thing> GetItems(Func<thing, bool> filter = null)
{
    return rawList.Where(filter ?? (s => true)).ToList();
}

In this expression s => true is the fallback filter which is evaluated if the argument filter is null. It just takes each entry of the list (as s) and returns true.



回答2:

There are two parts to this.

First, you need to adjust the NoFilter() function to be compatible with Predicate<T>. Notice the latter is generic, but the former is not. Make NoFilter() look like this:

private bool NoFilter<T>(T item) { return true; }

I know you never use the generic type argument, but it's necessary to make this compatible with your predicate signature.

For fun, you could also define NoFilter this way:

private Predicate<T> NoFilter = x => true;

Now the second part: we can look at using the new generic method as the default argument for GetItems(). The trick here is you can only use constants. While NoFilter() will never change, from the compiler's view that's not quite the same things a a formal constant. In fact, there is only one possible constant you can use for this: null. That means your method signature must look like this:

private List<thing> GetItems(Predicate<thing> filter = null)

Then you can check for null at the beginning of your function and replace it with NoFilter:

private List<thing> GetItems(Predicate<thing> filter = null)
{
    if (filter == null) filter = NoFilter;
    return rawList.Where(filter).ToList();
}

And if you also do want to explicitly pass this to the method when calling it, that would look like this:

var result = GetItems(NoFilter);

That should fully answer the original question, but I don't want to stop here. Let's look deeper.

Since you need the if condition anyway now, at this point I would tend to remove the NoFilter<T>() method entirely, and just do this:

private IEnumerable<thing> GetItems(Predicate<thing> filter = null)
{
    if (filter == null) return rawList;
    return rawList.Where(filter);
}

Note that I also changed the return type and removed the ToList() call at the end. If you find yourself calling ToList() at the end of a function just to match a List<T> return type, it's almost always much better to change the method signature to return IEnumerable<T> instead. If you really need a list (and usually, you don't), you can always call ToList() after calling the function.

This change makes your method more useful, by giving you a more abstract type that will be more compatible with other interfaces, and it potentially sets you up for a significant performance bump, both in terms of lowered memory use and in terms of lazy evaluation.

One final addition here is, if you do pare down to just IEnumerable, we can see now this method does not really provide much value at all beyond the base rawItems field. You might look at converting to a property, like this:

public IEnumerable<T> Items {get {return rawList;}}

This still allows the consumer of your type use a predicate (or not) if they want via the existing .Where() method, while also continuing to hide the underlying raw data (you can't directly just call .Add() etc on this).