mamuesstack
mamuesstack

Reputation: 1251

When to explicitly specify type arguments in generic C# method calls?

I have a generic class that offers common methods for predicates of concrete types (I use PredicateBuilder extention methods for that)

public class GenericPredicate<TItem>
{
    protected GenericPredicate(Expression<Func<TItem, bool>> predicate)
    {
        Predicate = predicate;
    }

    protected GenericPredicate()
    {
    }

    public Expression<Func<TItem, bool>> Predicate { get; protected set; }

    //Combines two predicates
    public TPred And<TPred>(TPred second) where TPred : GenericPredicate<TItem>, new()
    {
        return new TPred { Predicate = Predicate.And(second.Predicate) };
    }

    //Negates the predicate
    public TPred Not<TPred>() where TPred : GenericPredicate<TItem>, new()
    {
        return new TPred { Predicate = Predicate.Not() };
    }
}

And concrete predicates:

public class PersonPredicate : GenericPredicate<Person>
{
    protected PersonPredicate(Expression<Func<Person, bool>> predicate)
        : base(predicate)
    {
    }

    public PersonPredicate()
    {
    }

    public static PersonPredicate IsAdult()
    {
        return new PersonPredicate(p => p.Age >= 18);
    }

    public static PersonPredicate IsFemale()
    {
        return new PersonPredicate(p => Equals(p.Gender, Gender.Female));
    }
}

In the factory where I instantiate the concrete predicates I get the error

The type arguments for method Not cannot be inferred from the usage

when calling the generic Not()

PersonPredicate isFemale = PersonPredicate.IsFemale();
PersonPredicate isAdult = PersonPredicate.IsAdult();
PersonPredicate femaleAndAdult = isFemale.And(isAdult);
PersonPredicate notFemale = isFemale.Not(); //Error as described
PersonPredicate notFemaleWorkaround = isFemale.Not<PersonPredicate>();  // Works as suggested by compiler

So, the compiler doesn't know what TItem is. What makes me confused is that the generic And() method works without specifying the type arguments explicitly

Upvotes: 1

Views: 2054

Answers (2)

Jon Skeet
Jon Skeet

Reputation: 1500065

Basically, type inference for generic type parameters works based on the arguments you pass to the method.

So And() works, because you're passing in an isAdult. The compiler infers that TPred is PersonPredict as that's the type of isAdult and there are no other constraints.

For Not(), you're not passing in any arguments, so the compiler has no information about what you want TPred to be. That's what it can't infer - not TItem.

If you really wanted this to work, you could declare GenericPredicate with two type parameters, one of which is expected to be the subclass itself

public class GenericPredicate<TPred, TItem>
    where TPred : GenericPredicate<TPred, TItem>, new()

and:

public class PersonPredicate : GenericPredicate<PersonPredicate, Person>

You might want to have GenericPredicate<TPred,TItem> as a subclass of GenericPredicate<TItem>, so that other code could still accept just a GenericPredicate<TItem>. However, at this point it's all pretty convoluted - you may well better off just specifying the type argument.

Upvotes: 1

metacubed
metacubed

Reputation: 7271

In both your examples, the compiler tries to determine the type of predicate TPred that should be used to resolve the function.

In this example:

isFemale.And(isAdult);

the compiler sees that a PersonPredicate called isAdult has been passed in to the function. Thus, it can determine that in the function call, TPred is actually PersonPredicate.

In the other example,

isFemale.Not();

the compiler has no hints to understand what fits into TPred. Hence it needs specific instructions to resolve the conflict.

isFemale.Not<PersonPredicate>();

Now the compiler can resolve the call correctly.


Finally, you may think that because you are assigning the result of the function call to a PersonPredicate, the compiler should have picked this up. Unfortunately, function target resolution does not consider the assigned value. This is because there are other factors which come into play when you assign values using =. There may be implicit type casts defined, the operator may have been overloaded, or there may be an implicit constructor. So there is no guarantee that the assignee will be the same type as the actual value.

Upvotes: 1

Related Questions