Creating a function that converts functions of one

2019-04-10 23:38发布

问题:

For some fancy reflection stuff, I've got a function of type Func and need to pass it into a function that accepts type Func where T is not known until run time. For example:

public bool MyOperation(Func<string,bool> op) {
  return _myValues.Any(op);
}

public static bool InvokeOperationMethod(MethodInfo info, object obj,Func<object,bool> opAsObject)
{
   info.Invoke(obj, opAsObject);
}

The issue is that since I've got a lambda of a weaker type, I can't pass it as an argument of a stronger type. So I tried to make a helper that will create a function that converts a lambda of a weaker type into a stronger type. For example, I could call

var converter = CreateConverter(typeof(string));
Func<object,bool> asObject = o => o.ToString() == "a string"; //Dump example
Func<string,bool> asString = (Func<string,bool>)converter(asObject);
Assert.IsTrue(asInt("a string"));

Of course in the actual code this the destination type is not known until run time, and the actual predicate is not some trivial test.

This is my attempt:

/// <summary>
/// Converts a predicate of Func<object,bool> to
/// Func<Type,bool> of the given type.
/// </summary>
/// <param name="destType">Type of the dest.</param>
/// <param name="predicate">The predicate.</param>
/// <returns></returns>
public static TransformPredicate CreateConverter(Type destType)
{
    // This essentially creates the following lambda, but uses destType instead of T
    // private static Func<Func<object, bool>, Func<T, bool>> Transform<T>()
    // { 
    //     return (Func<object,bool> input) => ((T x) => input(x));
    // }
    var input = Expression.Parameter(typeof(Func<object, bool>), "input");

    var x = Expression.Parameter(destType, "x");
    var convert = Expression.Convert(x, typeof(object));
    var callInputOnX = Expression.Invoke(input, convert);
    var body2 = Expression.Lambda(callInputOnX, x);
    var body1 = Expression.Lambda(typeof(TransformPredicate),body2, input);
    return (TransformPredicate) body1.Compile();
}

public delegate object TransformPredicate(Func<object,bool> weak);

This actually works just fine, except that it runs really slowly since it's implicitly invoking CreateDelegate on each invocation. So I tried to call CreateDelegate myself by adding:

var destFunc = typeof(Func<,>).MakeGenericType(destType, typeof(bool));
var endType = typeof(Func<,>).MakeGenericType(typeof(Func<object, bool>), destFunc);
return (TransformPredicate)compiled.Method.CreateDelegate(endType);

This results in an error:

System.NotSupportedException: Derived classes must provide and implementation.

Any ideas how I can call CreateDelegate myself?

回答1:

Actually, as long long as the target type is a reference type, you don't have to do anything. The type parameter T in Func<T, TResult> is contravariant, which means you can do the cast directly. Because of this, the following code works fine:

Func<object,bool> asObject = o => o.ToString() == "a string";
Func<string,bool> asString = (Func<string,bool>)asObject;
asString("a string");

EDIT: It's not the compiler doing the conversion, it's the CLR that understands that Func<object, bool> can be safely casted to Func<string, bool>. So, the following code will work just fine:

class Program
{
    static void Main()
    {
        InvokeOperationMethod(
            typeof(Program).GetMethod("MyOperation"),
            new Program(), o => o.ToString() == "42");
    }

    public bool MyOperation(Func<string, bool> op)
    {
        return op("43");
    }

    public static bool InvokeOperationMethod(
        MethodInfo info, object obj, Func<object, bool> opAsObject)
    {
        return (bool)info.Invoke(obj, new object[] { opAsObject });
    }
}

EDIT 2: If you need this to work for value types too, you will need to box the parameter somehow. The boxing itself is simple:

private static Func<T, bool> BoxParameter<T>(Func<object, bool> op)
{
    return x => op(x);
}

But then you need to invoke it, and since you don't know T at compile time, you need to use reflection for that. Something like:

public static bool InvokeOperationMethod(
    MethodInfo method, object obj, Func<object, bool> opAsObject)
{
    var targetType = method.GetParameters()
                           .Single()
                           .ParameterType
                           .GetGenericArguments()[0];

    object opAsT;
    if (targetType.IsValueType)
    {
        opAsT =
            typeof(Program).GetMethod("BoxParameter",
            BindingFlags.NonPublic | BindingFlags.Static)
                           .MakeGenericMethod(targetType)
                           .Invoke(null, new object[] {opAsObject});
    }
    else
    {
        opAsT = opAsObject;
    }

    return (bool)method.Invoke(obj, new[] { opAsT });
}