Home > Mobile >  Difference between passing a Predicate and a method
Difference between passing a Predicate and a method

Time:10-18

I have a class Account, with a field representing the money on the account, and an array of accounts in the Main method. If I wanted to display all accounts with a non-negative balance, I would use the FindAll method in the class Array. I don't understand the difference between these two ways of doing that.

using System;

namespace Account_Predicate
{
    class Account
    {
        int money;

        public int Money { get => money; set => money = value; }
    }

    internal class Program
    {
        static void Main()
        {
            Account[] accounts = new Account[20];

            Random r = new Random();                    //
            for (int i = 0; i < accounts.Length; i  )   //
            {                                           //  fill the array with accounts with random amounts of money
                accounts[i] = new Account();            //
                accounts[i].Money = r.Next(-100, 100);  //
            }

            Display(accounts);

            Account[] results = Array.FindAll(accounts, FindNonNegative);

            Console.WriteLine("------------------");

            Display(results);

            Console.ReadKey();
        }

        static void Display(Account[] array)
        {
            for (int i = 0; i < array.Length; i  )
            {
                Console.WriteLine(array[i].Money);
            }
        }

        static bool FindNonNegative(Account account)
        {
            return account.Money >= 0;
        }
    }
}

The other way:

static void Main()
        {
            Account[] accounts = new Account[20];

            Random r = new Random();                    //
            for (int i = 0; i < accounts.Length; i  )   //
            {                                           //  fill the array with accounts with random amounts of money
                accounts[i] = new Account();            //
                accounts[i].Money = r.Next(-100, 100);  //
            }

            Display(accounts);

            Predicate<Account> predicate = FindNonNegative;

            Account[] results = Array.FindAll(accounts, predicate);

            Console.WriteLine("------------------");

            Display(results);

            Console.ReadKey();
        }

Is there any difference between passing a method directly vs passing a predicate, and if there is why would one be used instead of the other one?

CodePudding user response:

StriplingWarrior's answer is excellent, although it doesn't highlight the fact that in many real-world scenarios, the latter (using an explicit predicate) is more efficient than the former because it caches the predicate creation. This can be very impactful in tight loops.

Consider the following:

for (int i = 0; i < 10; i  )
{
    Account[] results = Array.FindAll(accounts, FindNonNegative);
}

Predicate<Account> predicate = FindNonNegative;
for (int i = 0; i < 10; i  )
{
    Account[] results = Array.FindAll(accounts, predicate);
}

This will compile to the below:

int j = 0;
while (j < 10)
{
    Array.FindAll(accounts, new Predicate<Account> (FindNonNegative));
    j  ;
}

Predicate<Account> predicate = new Predicate<Account> (FindNonNegative);
int i = 0;
while (i < 10)
{
    Array.FindAll(accounts, predicate);
    i  ;
}

While the creation of the predicate per iteration of the loop is not necessarily overly large, in tight loops this can create additional latency and allocations, since the delegate is recreated for each iteration.

The following benchmark demonstrates the cost of recreating the predicate delegate in a 100 iteration loop:

Update Disclaimer!: As is often the case with benchmarking, it is easy to get it wrong and benchmark something other than what you think you are testing, which is exactly what I did. This answer is now updated with a proper example that actually tests the predicate caching, not the work done by Random or the projecting of the matched array by Array.FindAll. The correctly updated benchmark highlights the much more significant difference caused by recreating the delegate.

[MemoryDiagnoser]
public class PredicateBenchmarks
{
    Account[] accounts;

    [GlobalSetup]
    public void Setup()
    {
        accounts = new Account[20];

        Random r = new Random();                    //
        for (int i = 0; i < accounts.Length; i  )   //
        {                                           //  fill the array with accounts with random amounts of money
            accounts[i] = new Account();            //
            accounts[i].Money = r.Next(1, 100);  //
        }
    }

    [Benchmark(Baseline = true)]
    public bool PredicateInLoop()
    {
        for (int i = 0; i < 100; i  )
        {
            bool results = DoWork(accounts, FindNonNegative);
        }

        return DoWork(accounts, FindNonNegative);
    }

    [Benchmark]
    public bool CachedPredicate()
    {
        Predicate<Account> predicate = FindNonNegative;
        for (int i = 0; i < 100; i  )
        {
            bool result = DoWork(accounts, predicate);
        }

        return DoWork(accounts, predicate);
    }

    public static bool DoWork<T>(T[] input, Predicate<T> matcher)
    {
        return matcher(input[0]); 
    }

    public class Account
    {
        int money;
        public int Money { get => money; set => money = value; }
    }

    static bool FindNonNegative(Account account) => account.Money >= 0;
}

This gives the following results:

results


BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.100-preview.4.22252.9
  [Host]     : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
  DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT


Method Mean Error StdDev Ratio RatioSD Gen 0 Allocated
PredicateInLoop 1,288.4 ns 25.64 ns 65.74 ns 1.00 0.00 1.5450 6,464 B
CachedPredicate 351.6 ns 7.20 ns 21.11 ns 0.27 0.02 0.0153 64 B

CodePudding user response:

No real difference. In both cases a Predicate<Account> is getting created from the FindNonNegative reference via implicit cast. In the second example you're just saving that predicate into a variable first.

Heinzi's example in the comment is very appropriate. I'll make a slight variation: in both of these cases, an int is being implicitly cast into an Object. In one, you're using a variable to hold that object.

object i = 1; 
Console.WriteLine(i);
Console.WriteLine(1);

CodePudding user response:

There is no difference. Consider also that rather than explicitly defining a method with the necessary signature, instantiating a Predicate delegate, and passing the delegate to the Find method, it is customary to use a lambda expression:

Account[] results = Array.FindAll(accounts, ac  => ac.Money >= 0);

CodePudding user response:

There is a difference, which can matter in certain situations, but not with regard to your code example. Predicate<T> is a (generic) delegate type. FindNonNegative is what is called a method group[1].

Given a specific delegate type, a method group can be implictly converted to that delegate type if one of the overloads in the method group has a method signature matching the delegate type.

Given a generic method like

void DoSomething<T>(Predicate<T> p)

you could call it with a delegate of a concrete Predicate<T> type like for example:

Predicate<Account> predicate = FindNonNegative;
DoSomething(predicate);

But you could not call DoSomething like:

DoSomething(FindNonNegative);

Why is that? DoSomething is a generic method with a generic type parameter T. But in the invocations of DoSomething, i haven't specified a type for T. In the first case, the compiler can infer the type for T from the type of the predicate variable.

But in the latter case, i just provided a method group. A method group itself is of no type (delegate or otherwise), so the compiler does not know what T should be. And to be able to convert the method group to some Predicate<T>, the compiler would first need to know the type for T - catch 22[2].

Thus, to make the latter case work, i would need to explicitly specify the type for T like:

DoSomething<Account>(FindNonNegative);

Side note: Then why did Array.FindAll(accounts, FindNonNegative) work, despite not specifiying T here either? Array.FindAll is declared as public static T[] FindAll<T> (T[] array, Predicate<T> match). The compiler can infer T from the provided array variable/expression, thus knowing T for converting the FindNonNegative method group into the matching Predicate<T> delegate type.


[1] "Group" because a method can have several overloads, i.e., the same method name with different number and/or types of parameters. In your specific case, FindNonNegative is a method group with precisely a single overload.

[2] For a method group with a single overload, hypothetically a compiler could technically inspect the method signature to figure out a compatible delegate type. Alas, the C# compiler (and C# language specification) has no such logic specified, as of yet...

  • Related