Is it possible to create in C# a predicate with a custom values, or templated values

Viewed 446

I am relatively new to C# and come from a C++ background. I have been looking a list searches and the usages of Find.

I am trying to define a function that takes a parameter, and returns an entry from a list. for example

class Product
{
    public string id;
    public string description;
}

public List<Product> products = new List<Product>();
// Add some items to the list

My question is, can I turn this following custom "Find" function using C# features like Predicates/Funcs

Product GetProductByID(string id)
{
    foreach (Product p in products)
    {
        if (p.id == id)
        {
            return p;
        }
    }
}

I have tried using Predicates, but I don't seem to be able to find anyway to feed a value in to the function to do the boolean expression check against. I understand how I would write a Predicate to find a speciic item in a list, but not a dynamic one that could be passed in.

Ideally I would then like to go one step further and Template it so the Predicate could be used for any list of items that have a similar structure.

i.e if we also had

class Employee
{
    public string id;
    public int age;
}

public List<Employee> employees = new List<Employee>();
// Add some items to the list

It would be really cool to use the same defined Predicate, but on this list of a totally different type. Ie something like

// psudo
Predicate<T, string> predicate = return <T>.id == id;

and it work because T is always of a type that has a string member called id

I Hope that all makes sense.

4 Answers

I understand how I would write a Predicate to find a speciic item in a list, but not a dynamic one that could be passed in.

This is where lambda expressions come in, allowing you to create an instance of a delegate type that capture variables in scope. For example:

public Predicate<Product> CreateProductPredicateForId(string id)
{
    return product => product.id == id;
}

Now in terms of making that generic, you'd need to have an interface that specifies an Id property. You can then write a generic method. For example:

public interface IEntity
{
    string Id { get; }
}
...

public Predicate<T> CreatePredicateForId<T>(string id) where T : IEntity
{
    return entity => entity.Id == id;
}

(note: throughout this I'm using Func<T, bool> - but the exact same logic applies to Predicate<T> - they have the equivalent signature, but you tend to see more of the former than the latter these days; that said: List<T> uses the latter)

Generics supports this, but via variance rules; you would have to declare an interface of the form:

interface IHazId
{
    int Id { get; }
}
class Foo : IHazId // your types need to implement it, too!
{
    public int Id { get; set; }
}

and then you can use it in a general way:

    void SomeCallingCode()
    {
        // get the predicate, however
        int id = 42;
        Func<IHazId, bool> predicate = obj => obj.Id == id;

        // use it
        SomeTypeSpecificMethod(predicate);
    }
    void SomeTypeSpecificMethod(Func<Foo, bool> predicate)
        => throw new NotImplementedException("not shown");

with the compiler/runtime allowing the Func<IFoo, bool> to satisfy a type-specific Func<Foo, bool> requirement, because it knows that all Foo are IHazId. You can also do this with generics with a constraint:

    void SomeCallingCode()
    {
        // get the predicate, however
        int id = 42;
        Func<IHazId, bool> predicate = obj => obj.Id == id;

        // use it
        SomeGenericMethod<Foo>(predicate);
    }
    void SomeGenericMethod<T>(Func<T, bool> predicate)
        where T : IHazId
        => throw new NotImplementedException("not shown");

However, in reality, this is rarely useful, since in most cases you already know the types, and just want to do:

int id = 42;
SomeTypeSpecificMethod(x => x.Id == id);

Note also that the variance rules that allow you to treat Func<IHazId, bool> as Func<SomeType, bool> only work for reference type SomeType - which is to say: class, not struct.

Product GetProductByID(string id)
{
    foreach (Product p in products)
    {
        if (p.id == id)
        {
            return p;
        }
    }
}

this could be replaced with

products.FirstOrDefault(p => p.Id == id)

The second part of you question could be easilly solved with inheritance, like

public class ProductA
{
    public string Id { get; set; }
}

public class ProductB : ProductA
{
    
}

List<ProductA> a = new List<ProductA>();
List<ProductB> b = new List<ProductB>();

Func<ProductA, bool> findFunction = (p) => p.Id == id;

var p = a.FirstOrDefault(findFunction);
p = b.FirstOrDefault(findFunction);

You could use LINQ

Product myProduct = products.Where(p => p.Id == id).FirstOrDefault();

You need using System.Linq; on top of your file to use it.

Related