6

我正在尝试找到一种将最多 5 个 Func 累积应用于同一个 IEnumerable 的好方法。这是我想出的:

private Func<SurveyUserView,bool> _getFilterLambda(IDictionary<string, string> filters)
{
    Func<SurveyUserView, bool> invokeList = delegate(SurveyUserView surveyUserView)
    { 
        return surveyUserView.deleted != "deleted"; 
    };

    if (filters.ContainsKey("RegionFilter"))
    {
        invokeList += delegate(SurveyUserView surveyUserView)
        {
            return surveyUserView.Region == filters["RegionFilter"];
        };
    }

    if (filters.ContainsKey("LanguageFilter"))
    {
        invokeList += delegate(SurveyUserView surveyUserView)
        {
            return surveyUserView.Locale == filters["LanguageFilter"];
        };
    }

    if (filters.ContainsKey("StatusFilter"))
    {
        invokeList += delegate(SurveyUserView surveyUserView)
        { 
            return surveyUserView.Status == filters["StatusFilter"]; 
        };
    }

    if (filters.ContainsKey("DepartmentFilter"))
    {
        invokeList += delegate(SurveyUserView surveyUserView)
        {
            return surveyUserView.department == filters["DepartmentFilter"];
        };
    }

    return invokeList;
}

我认为它会以累积的方式应用这些,但是,我可以从结果中看到它实际上只是应用了最后一个(DepartmentFilter)。

有 2^4 种可能的组合,所以如果/否则,蛮力是行不通的。(仅当字典中存在相应的键时,我才想使用特定的 lambda。)

编辑:这是我接受的解决方案,但在评估时会导致 StackOverflowException 。有人知道为什么吗?

private Func<SurveyUserView,bool> _getFilterLambda(IDictionary<string, string> filters )
    {

        Func<SurveyUserView, bool> resultFilter = (suv) => suv.deleted != "deleted";                                                        

        if (filters.ContainsKey("RegionFilter"))
        {
            Func<SurveyUserView, bool> newFilter =
                (suv) => resultFilter(suv) && suv.Region == filters["RegionFilter"];
            resultFilter = newFilter;
        }

        if (filters.ContainsKey("LanguageFilter"))
        {
            Func<SurveyUserView, bool> newFilter =
                 (suv) => resultFilter(suv) && suv.Locale == filters["LanguageFilter"];
            resultFilter = newFilter;
        }

        if (filters.ContainsKey("StatusFilter"))
        {
            Func<SurveyUserView, bool> newFilter =
                (suv) => resultFilter(suv) && suv.Status == filters["StatusFilter"];
            resultFilter = newFilter;
        }

        if (filters.ContainsKey("DepartmentFilter"))
        {
            Func<SurveyUserView, bool> newFilter =
                (suv) => resultFilter(suv) && suv.department == filters["DepartmentFilter"];
            resultFilter = newFilter;
        }

        return resultFilter;
    }

编辑:这是一个很好的解释,为什么这会导致朋友和导师 Chris Flather 的 StackOverflowException -

The important thing to understanding why the infinite recursion occurs is understanding when the symbols in a lambda are resolved (i.e. at runtime and not at definition).

Take this simplified example:

Func<int, int> demo = (x) => x * 2;
Func<int, int> demo2 = (y) => demo(y) + 1;
demo = demo2;
int count = demo(1);

If it were resolved statically at definition this would work and be the same as:

Func<int, int> demo2 = (y) => (y * 2) + 1;
Int count = demo2(1);

But it doesn’t actually attempt to figure out what the demo embedded in demo2 does until runtime – at which time demo2 has been redefined to demo. Essentially the code now reads:

Func<int, int> demo2 = (y) => demo2(y) + 1;
Int count = demo2(1);
4

4 回答 4

4

Instead of trying to combine the delegates this way, you could build new delegates that use the existing one with your AND condition:

Func<SurveyUserView, bool> resultFilter = (suv) => true;

if (filters.ContainsKey("RegionFilter"))
{
    var tmpFilter = resultFilter;
    // Create a new Func based on the old + new condition
    resultFilter = (suv) => tmpFilter(suv) && suv.Region == filters["RegionFilter"];
}

if (filters.ContainsKey("LanguageFilter"))
{
   // Same as above...

//... Continue, then:

return resultFilter;

That being said, it may be easier to pass your original IQueryable<SurveyUserView> or IEnumerable<SurveyUserView> into this method, and just add .Where clauses directly to filter. You could then return the final query without executing it, with the filters added on.

于 2012-06-01T02:13:51.677 回答
2

I would think that using the Where(...) extension on what is, presumably, an IQueryable<SurveyUserView> and return a IQueryable<SurveyUserView> instead of a Func<...>:

// Assuming `q` is a `IQueryable<SurveyUserView>`

if(filters.ContainsKeys["Whatever"])
{
  q = q.Where(suv => suv.Status == filters["Whatever"];
}

The Anding is implicit.

于 2012-06-01T02:18:23.190 回答
2
    private Func<SurveyUserView, bool> _getFilterLabda(IDictionary<string, string> filters)
    {
        Func<SurveyUserView, bool> invokeList = surveyUserView => surveyUserView.deleted != "deleted");

        if (filters.ContainsKey("RegionFilter"))
        {
            invokeList += surveyUserView => surveyUserView.Region == filters["RegionFilter"]);
        }

        if (filters.ContainsKey("LanguageFilter"))
        {
            invokeList += surveyUserView => surveyUserView.Locale == filters["LanguageFilter"];
        }

        if (filters.ContainsKey("StatusFilter"))
        {
            invokeList += surveyUserView => surveyUserView.Status == filters["StatusFilter"];
        }

        if (filters.ContainsKey("DepartmentFilter"))
        {
            invokeList += surveyUserView => surveyUserView.department == filters["DepartmentFilter"]);
        }

        return invokeList;
    }
    ...
    Func<SurveyUserView, bool> resultFilter = suv => _getFilterLabda(filters)
         .GetInvocationList()
         .Cast<Func<SurveyUserView, bool>>()
         .All(del => del(suv))
于 2012-06-01T03:01:40.767 回答
1

Here is my favorite method of accomplishing what you are asking for.

private Func<SurveyUserView, bool> _getFilterLambda(IDictionary<string, string> filters)
{
    List<Func<SurveyUserView, bool>> invokeList = new List<Func<SurveyUserView, bool>>();

    invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.deleted != "deleted");

    if (filters.ContainsKey("RegionFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Region == filters["RegionFilter"]);
    }

    if (filters.ContainsKey("LanguageFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Locale == filters["LanguageFilter"]);
    }

    if (filters.ContainsKey("StatusFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Status == filters["StatusFilter"]);
    }

    if (filters.ContainsKey("DepartmentFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.department == filters["DepartmentFilter"]);
    }

    return delegate (SurveyUserView surveyUserView)
    {
        bool unfiltered = true;

        foreach(var filter in invokeList)
        {
            unfiltered = unfiltered && filter(surveyUserView);
        }

        return unfiltered;
    };
}

We build a list of each of the delegates you want to apply; and then return another seperate delegate that iterates over that list combining each of the filters with a simple logical AND.

This works because the delegate we're returning closes over invokeList; creating a kind of private variable that stores all of our new delegates that travels with our returned delegate.

An alternative which is slightly closer syntactically to your original is:

private Func<SurveyUserView, bool> _getFilterLambda(IDictionary<string, string> filters)
{
    Func<SurveyUserView, bool> invokeList = (SurveyUserView surveyUserView) => surveyUserView.deleted != "deleted";

    if (filters.ContainsKey("RegionFilter"))
    {
        invokeList += (SurveyUserView surveyUserView) => surveyUserView.Region == filters["RegionFilter"];
    }

    if (filters.ContainsKey("LanguageFilter"))
    {
        invokeList += (SurveyUserView surveyUserView) => surveyUserView.Locale == filters["LanguageFilter"];
    }

    if (filters.ContainsKey("StatusFilter"))
    {
        invokeList += (SurveyUserView surveyUserView) => surveyUserView.Status == filters["StatusFilter"];
    }

    if (filters.ContainsKey("DepartmentFilter"))
    {
        invokeList += (SurveyUserView surveyUserView) => surveyUserView.department == filters["DepartmentFilter"];
    }

    return delegate (SurveyUserView surveyUserView)
    {
        bool unfiltered = true;

        // implicit cast from Delegate to Func<SurveyUserView, bool> happening on next line
        foreach (Func<SurveyUserView, bool> filter in invokeList.GetInvocationList())
        {
            unfiltered = unfiltered && filter(surveyUserView);
        }

        return unfiltered;
    };
}

In this version we're really just using invokeList as a list for the delegates; we call GetInvocationList() (a method on the Delegate class Func derives from) to get a list of all the delegates that are combined to make the multi-cast delegate.

I personally prefer the first version because it is clearer what is happening behind the scenes.

Both of these are really the same as Jacob Seleznev's answer which I somehow missed before responding. They just bring the final delegate inside the method so that the method itself still satisfies Trey's original contract.

Finally, if all of the filters are order-independent with no side-effects we can write a version that will run the filters in parallel.

private Func<SurveyUserView, bool> _getFilterLambdaParallel(IDictionary<string, string> filters)
{
    List<Func<SurveyUserView, bool>> invokeList = new List<Func<SurveyUserView, bool>>();

    invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.deleted != "deleted");

    if (filters.ContainsKey("RegionFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Region == filters["RegionFilter"]);
    }

    if (filters.ContainsKey("LanguageFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Locale == filters["LanguageFilter"]);
    }

    if (filters.ContainsKey("StatusFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.Status == filters["StatusFilter"]);
    }

    if (filters.ContainsKey("DepartmentFilter"))
    {
        invokeList.Add((SurveyUserView surveyUserView) => surveyUserView.department == filters["DepartmentFilter"]);
    }

    return delegate (SurveyUserView surveyUserView)
    {
        int okCount = 0;
        Parallel.ForEach(invokeList, delegate (Func<SurveyUserView, bool> f)
        {
            if (f(surveyUserView))
            {
                System.Threading.Interlocked.Increment(ref okCount);
            }
        });
        return okCount == invokeList.Count;
    };
}

We use Parallel.ForEach to execute the filters in parallel. There is a slight complication that prevents us from using our simple boolean AND - there is no guarantee that the logical AND will happen atomically creating a nasty race condition.

To fix that we simply count the number of filters that were passed through using Interlocked.Increment which is guaranteed to be atomic. If all the filters passed through successfully then we know we can return true; otherwise the and would have failed.

The equivalent for doing a logical OR here would have to check with okCount was more than zero.

于 2014-07-15T05:59:29.357 回答