Search code examples
c#expressionexpression-treescovariancecontravariance

Contravariance in Expressions


I'm trying to create a Generic Action Delegate

  delegate void ActionPredicate<in T1, in T2>(T1 t1, T2 t2);

and

public static ActionPredicate<T,string> GetSetterAction<T>(string fieldName) 
    {

        ParameterExpression targetExpr = Expression.Parameter(typeof(T), "Target");
        MemberExpression fieldExpr = Expression.Property(targetExpr, fieldName);
        ParameterExpression valueExpr = Expression.Parameter(typeof(string), "value");

        MethodCallExpression convertExpr = Expression.Call(typeof(Convert), "ChangeType", null, valueExpr, Expression.Constant(fieldExpr.Type));

        UnaryExpression valueCast = Expression.Convert(convertExpr, fieldExpr.Type);
        BinaryExpression assignExpr = Expression.Assign(fieldExpr, valueCast);
        var result = Expression.Lambda<ActionPredicate<T, string>>(assignExpr, targetExpr, valueExpr);
        return result.Compile();
    }

and here is my caller

 ActionPredicate<busBase, string> act = DelegateGenerator.GetSetterAction<busPerson>("FirstName");

and here is the business object

 public abstract class busBase 
{

}
public class busPerson : busBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    public string GetFullName()
    {
        return string.Format("{0} {1}", FirstName, LastName);
    }
}

and here is the error what i get during compilation

Cannot implicitly convert type 'BusinessObjects.ActionPredicate<BusinessObjects.busPerson,string>' to 'BusinessObjects.ActionPredicate<BusinessObjects.busBase,string>'. An explicit conversion exists (are you missing a cast?)    

My GetSetterAction is returning ActionPerdicate where as here T is busPerson and i am trying to store it in ActionPredicate keeping in mind about Contravariance. But it fails. i dont know how to proceed further. Please Help..!


Solution

  • Generic contravariance does not allow you to assign a delegate D<TDerived> to a delegate D<TBase> because of the reason demonstrated below (using Action<T1> here):

    Action<string> m1 = MyMethod; //some method to call
    Action<object> m2 = m1; //compiler error - but pretend it's not.
    object obj = new object();
    
    m2(obj);  //runtime error - not type safe
    

    As you can see, if we were allowed to do this assignment, we would then be breaking type-safety because we'd be able to try and invoke the delegate m1 by passing and instance of object and not string. Going the other way, however, i.e. copying a delegate reference to a type whose parameter type is more derived than the source is fine. MSDN has a more complete example of generic co/contra variance.

    Therefore you will either need to change the declaration of act to ActionPredicate<busPerson, string> act or, more likely, consider writing the GetSetterAction method to always return ActionPredicate<busBase, string>. If you do that, you should also add the type constraint

    where T1 : busBase
    

    To the method, and you'll also need to change how your expression is built, replace the first two lines as follows:

    ParameterExpression targetExpr = Expression.Parameter(typeof(busBase), "Target");
    //generate a strongly-typed downcast to the derived type from busBase and
    //use that as the type on which the property is to be written
    MemberExpression fieldExpr = Expression.Property(
      Expression.Convert(targetExpr, typeof(T1)), fieldName);
    

    Adding the generic constraint is a nice touch to ensure that this downcast will always be valid for any T1.

    On a slightly different note - what was wrong with the Action<T1, T2> delegate? It seems to do exactly the same thing as yours? :)