简体   繁体   中英

In LINQ, how do I modify an existing LINQ extension method to add a “By” selector, i.e. add Func<T> TResult to the function signature?

I am very curious to know how to modify an existing LINQ function to add Func<T> TResult to the function signature, ie allow it to use a selector as in (o => o.CustomField) .

For example, in C#, I can use .IsDistinct() to check if a list of integers are distinct. I can also use .IsDistinctBy(o => o.SomeField) to check if the integers in field o.SomeField are distinct. I believe that, behind the scenes, .IsDistinctBy(...) has something like the function signature Func<T> TResult appended to it?

My question is this: what is the technique for taking an existing LINQ extension function, and converting it so it can have a parameter (o => o.SomeField) ?

Here is an example.

This extension function checks to see if a list is increasing monotonically (ie values are never decreasing, as in 1,1,2,3,4,5,5):

main()
{
   var MyList = new List<int>() {1,1,2,3,4,5,5};
   DebugAssert(MyList.MyIsIncreasingMonotonically() == true);
}

public static bool MyIsIncreasingMonotonically<T>(this List<T> list) where T : IComparable
{
    return list.Zip(list.Skip(1), (a, b) => a.CompareTo(b) <= 0).All(b => b);
}

If I want to add a "By", I add a parameter Func<T> TResult . But how do I modify the body of the function to make it select by (o => o.SomeField) ?

main()
{
   DebugAssert(MyList.MyIsIncreasingMonotonicallyBy(o => o.CustomField) == true);
}

public static bool MyIsIncreasingMonotonicallyBy<T>(this List<T> list, Func<T> TResult) where T : IComparable
{
    // Question: How do I modify this function to make it  
    // select by o => o.CustomField?
    return list.Zip(list.Skip(1), (a, b) => a.CompareTo(b) <= 0).All(b => b);
}

Consider an implementation like the following, which enumerates the given IEnumerable<T> only once. Enumerating can have side-effects, and callers typically expect a single pass-through if that's possible.

public static bool IsIncreasingMonotonically<T>(
    this IEnumerable<T> _this)
    where T : IComparable<T>
{
    using (var e = _this.GetEnumerator())
    {
        if (!e.MoveNext())
            return true;
        T prev = e.Current;
        while (e.MoveNext())
        {
            if (prev.CompareTo(e.Current) > 0)
                return false;
            prev = e.Current;
        }
        return true;
    }
}

Your enumerable.IsIncreasingMonotonicallyBy(x => x.MyProperty) overload that you describe can now be written as follows.

public static bool IsIncreasingMonotonicallyBy<T, TKey>(
    this IEnumerable<T> _this,
    Func<T, TKey> keySelector)
    where TKey : IComparable<TKey>
{
    return _this.Select(keySelector).IsIncreasingMonotonically();
}

Just apply the Func to a and b:

public static bool MyIsIncreasingMonotonicallyBy<T, TResult>(this IEnumerable<T> list, Func<T, TResult> selector)
    where TResult : IComparable<TResult>
{
    return list.Zip(list.Skip(1), (a, b) => selector(a).CompareTo(selector(b)) <= 0).All(b => b);
}

One above is close to right but there are issues:

  1. Your list has possible multiple enumeration of IEnumeration

     public static bool MyIsIncreasingMonotonicallyBy<T, TResult>( this IEnumerable<T> list, Func<T, TResult> selector) where TResult : IComparable<TResult> { var enumerable = list as IList<T> ?? list.ToList(); return enumerable.Zip( enumerable.Skip(1), (a, b) => selector(a).CompareTo(selector(b)) <= 0 ).All(b => b); } 

PS I believe you need to remove the "this" because Extension method can only be declared in non-generic, non-nested static class.


In response to Frédéric Hamidi:

Consider the following:

IEnumerable<string> names = GetNames();
foreach (var name in names)   Console.WriteLine("Found " + name);
var allNames = new StringBuilder();
foreach (var name in names)   allNames.Append(name + " ");

Assuming that GetNames() returns an IEnumerable, we are, effectively, doing extra work by enumerating this collection twice in the two foreach statements. If GetNames() results in a database query, you end up doing that query twice, while both times getting the same data.

This kind of problem can be easily fixed – simply force the enumeration at the point of variable initialization by converting the sequence to a list(or you could do array). Both array and list types implement the IEnumerable interface.

// the existing LINQ operator that you want to modify:
public static … Foo<T>(this IEnumerable<T> xs, …)
{
    …
}

// the extended ("modified") operator:
public static … FooBy<T, U>(this IEnumerable<T> xs, Func<T, U> func)
{
    return xs.Select(func).Foo(…);
}       // --^^^^^^^^^^^^^-------
        //   there you go!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM