Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delegate.CreateDelegate won't box a return value - deliberate, or an ommission?

Tags:

c#

.net

I have a static method:

public class Example
{
    //for demonstration purposes - just returns default(T)
    public static T Foo<T>() { return default(T); }
}

And I need to be able to invoke it using a Type parameter calls to which could be numerous, so my standard pattern is to create a thread-safe cache of delegates (using ConcurrentDictionary in .Net 4) which dynamically invoke the Foo<T> method with the correct T. Without the caching, though, the code is this:

static object LateFoo(Type t) 
{ 
  //creates the delegate and invokes it in one go
  return (Func<object>)Delegate.CreateDelegate( 
    typeof(Func<object>), 
    typeof(Example).GetMethod("Foo", BindingFlags.Public | BindingFlags.Static). 
      MakeGenericMethod(t))(); 
}

This is not the first time I've had to do this - and in the past I have use Expression trees to build and compile a proxy to invoke the target method - to ensure that return type conversion and boxing from int -> object (for example) is handled correctly.

Update - example of Expression code that works

static object LateFoo(Type t)
{
  var method = typeof(Example)
               .GetMethod("Foo", BindingFlags.Public | BindingFlags.Static)
               .MakeGenericMethod(t); 
  //in practise I cache the delegate, invoking it freshly built or from the cache
  return Expression.Lambda<Func<IField, object>>(Expression.Convert(
    Expression.Call(method), typeof(object))).Compile()();
}

What's slightly amusing is that I learned early on with expressions that an explicit Convert was required and accepted it - and in lieu of the answers here it does now make sense why the .Net framework doesn't automatically stick the equivalent in.

End update

However, this time I thought I'd just use Delegate.CreateDelegate as it makes great play of the fact that (from MSDN):

Similarly, the return type of a delegate is compatible with the return type of a method if the return type of the method is more restrictive than the return type of the delegate, because this guarantees that the return value of the method can be cast safely to the return type of the delegate.

Now - if I pass typeof(string) to LateFoo method, everything is fine.

If, however, I pass typeof(int) I get an ArgumentException on the CreateDelegate call, message: Error binding to target method. There is no inner exception or further information.

So it would seem that, for method binding purposes, object is not considered more restrictive than int. Obviously, this must be to do with boxing being a different operation than a simple type conversion and value types not being treated as covariant to object in the .Net framework; despite the actual type relationship at runtime.

The C# compiler seems to agree with this (just shortest way I can model the error, ignore what the code would do):

public static int Foo()
{
    Func<object> f = new Func<object>(Foo);
    return 0;
}

Does not compile because the Foo method 'has the wrong return type' - given the CreateDelegate problem, C# is simply following .Net's lead.

It seems to me that .Net is inconsistent in it's treatment of covariance - either a value type is an object or it's not; & if it's not it should not expose object as a base (despite how much more difficult it would make our lives). Since it does expose object as a base (or is it only the language that does that?), then according to logic a value type should be covariant to object (or whichever way around you're supposed to say it) making this delegate bind correctly. If that covariance can only be achieved via a boxing operation; then the framework should take care of that.

I dare say the answer here will be that CreateDelegate doesn't say that it will treat a box operation in covariance because it only uses the word 'cast'. I also expect there are whole treatises on the wider subject of value types and object covariance, and I'm shouting about a long-defunct and settled subject. I think there's something I either don't understand or have missed, though - so please enlighten!

If this is unanswerable - I'm happy to delete.

like image 413
Andras Zoltan Avatar asked Jan 11 '12 10:01

Andras Zoltan


3 Answers

You can only convert a delegate in this way if the parameters and return value can be converted using a representation conserving conversion.

  • Reference types can only be converted to other reference types in this way
  • Integral values can be converted to other integer values of the same size (int, uint, and enums of the same size are compatible)

A few more relevant blog articles:

This dichotomy motivates yet another classification scheme for conversions (†). We can divide conversions into representation-preserving conversions (B to D) and representation-changing conversions (T to U). (‡) We can think of representation-preserving conversions on reference types as those conversions which preserve the identity of the object. When you cast a B to a D, you’re not doing anything to the existing object; you’re merely verifying that it is actually the type you say it is, and moving on. The identity of the object and the bits which represent the reference stay the same. But when you cast an int to a double, the resulting bits are very different.

This is why covariant and contravariant conversions of interface and delegate types require that all varying type arguments be of reference types. To ensure that a variant reference conversion is always identity-preserving, all of the conversions involving type arguments must also be identity-preserving. The easiest way to ensure that all the non-trivial conversions on type arguments are identity-preserving is to restrict them to be reference conversions. http://blogs.msdn.com/b/ericlippert/archive/2009/03/19/representation-and-identity.aspx

"but how can a value type, like int, which is 32 bits of memory, no more, no less, possibly inherit from object? An object laid out in memory is way bigger than 32 bits; it's got a sync block and a virtual function table and all kinds of stuff in there." Apparently lots of people think that inheritance has something to do with how a value is laid out in memory. But how a value is laid out in memory is an implementation detail, not a contractual obligation of the inheritance relationship! When we say that int inherits from object, what we mean is that if object has a member -- say, ToString -- then int has that member as well. http://ericlippert.com/2011/09/19/inheritance-and-representation/

like image 146
CodesInChaos Avatar answered Oct 23 '22 00:10

CodesInChaos


It seems to me that .Net is inconsistent in it's treatment of covariance - either a value type is an object or it's not; if it's not it should not expose object as a base

It depends on what the meaning of "is" is, as President Clinton famously said.

For the purposes of covariance, int is not object because int is not assignment compatible with object. A variable of type object expects a particular bit pattern with a particular meaning to be stored in it. A variable of type int expects a particular bit pattern with a particular meaning, but a different meaning than the meaning of a variable of object type.

However, for the purposes of inheritance, an int is an object because every member of object is also a member of int. If you want to invoke a method of object -- ToString, say -- on int, you are guaranteed that you can do so, because an int is a kind of object, and an object has ToString.

It is unfortunate, I agree, that the truth value of "an int is an object" varies depending on whether you mean "is assignment-compatible with" or "is a kind of".

If that covariance can only be achieved via a boxing operation; then the framework should take care of that.

OK. Where? Where should the boxing operation go? Someone, somewhere has to generate a hunk of IL that has a boxing instruction. Are you suggesting that when the framework sees:

Func<int> f1 = ()=>1;
Func<object> f2 = f1;

then the framework should automatically pretend that you said:

Func<object> f2 = ()=>(object)f1();

and thereby generate the boxing instruction?

That's a reasonable feature, but what are the consequences? Func<int> and Func<object> are reference types. If you do f2 = f1 on reference types like this, do you not expect that f2 and f1 have reference identity? Would it not be exceedingly strange for this test case to fail?

f2 = f1;
Debug.Assert(object.ReferenceEquals(f1, f2));

Because if the framework implemented that feature, it would.

Similarly, if you said:

f1 = MyMethod;
f2 = f1;

and you asked the two delegates whether they referred to the same method or not, would it not be exceedingly weird if they referred to different methods?

I think that would be weird. However, the VB designers do not. If you try to pull shenanigans like that in VB, the compiler will not stop you. The VB code generator will generate non-reference-equal delegates for you that refer to different methods. Try it!

Moral of the story: maybe C# is not the language for you. Maybe you prefer a language like VB, where the language is designed to take a "make a guess about what the user probably meant and just make it work" attitude. That's not the attitude of the C# designers. We are more "tell the user when something looks suspiciously wrong and let them figure out how they want to fix it" kind of people.

like image 35
Eric Lippert Avatar answered Oct 22 '22 23:10

Eric Lippert


Even though I think @CodeInChaos is absolutely right, I can't help pointing this Eric Lippert's blog post out. In reply to the last comment to his post (at the very bottom of the page) Eric explains the rationale for such behaviour, and I think this is exactly what you're interested in.

UPDATE: As @Sheepy pointed out Microsoft moved old MSDN blogs into archive and removed all comments. Luckily, the Wayback Machine preserved the blog post in its original form.

like image 5
Igor Korkhov Avatar answered Oct 23 '22 01:10

Igor Korkhov