Skip to main content

Generic overloads and ref parameter hell (the return of the Stack overflow)

In a framework that I have written for my dev team, I have a class that has a method like so:

  1. public class ObjectBase
  2. {
  3.     public void Clone(ref ObjectBase target, ObjectBase parent);
  4. }

This method (virtual in my library, in fact) has two functions:  When the target ref is null, then a new object should be created, but when it's not, then the contents of the object on which it's being called should be cloned into the passed object.  The parent of the new object is required because in the framework our objects have keys that can come from multiple sources - including an expression on the parent.

Anyway, whilst this method is particularly useful, it's a little unfriendly to use from your code when you don't have a direct ObjectBase reference to pass, but instead a reference to an object derived from it:

  1. public class MyObject : ObjectBase
  2. {
  3. }
  4.  
  5.  
  6. public static void main()
  7. {
  8.     MyObject obj = new MyObject();
  9.     MyObject clone = null;
  10.     obj.Clone(ref clone, null);
  11. }

That red line indicates where the compiler blows - when the parameter type is 'ref T', you can't pass a reference to 'TDerived : T', because although the two types are compatible, the method expects a 'ref T', not anything else, and at that level 'ref T' and 'ref TDerived' are two incompatible types.

Not a problem, we can of course get around this:

  1. public static void main()
  2. {
  3.     MyObject obj = new MyObject();
  4.     MyObject clone = null;
  5.     ObjectBase clonebase = null;
  6.     obj.Clone(ref clonebase, null);
  7.     clone = clonebase as MyObject;
  8. }

I don't need to say it, but I will anyway: URGH!

So, I thought, 'I know, I'll create a helpful generic method InObjectBase instead'.  Here it is:

  1. public void Clone<T>(ref T target, ObjectBase parent)
  2.     where T : class, ObjectBase
  3. {
  4.     ObjectBase basetarget = target;
  5.     Clone(ref basetarget, parent);
  6.     if (target == null)
  7.         target = basetarget as T; //'class' generic constraint
  8.                                                             //allows us to use 'as'
  9. }

Now I know the old adage about 'assume' making an ass out of you and me - but feeling very clever, I rolled out the code and started it up.  I get a StackOverflowException almost immediately - and I'm concerned - because the code which uses the new generic version hasn't even been called!

So I go and investigate that method, and see that it is, in fact the second line - intellisense shows that it is now a recursive method.   Even worse, every other call to 'Clone' (used perhaps about 1,000 times in many different projects at this point!) will now point to the generic version instead of the non-generic - meaning that I've just broken everybody's code.

Why?

The compiler matches methods initially by name, and then by signature (minus return type).  It also factors in 'convertibility' from method call parameters to the types of the parameters in those methods that might be called, in case exact matches cannot be found.  When the compiler performs a search for the target method of a method call, as well, it has to choose a sort order for the candidate methods when that method call is to an overloaded method.  If you take a look at the work that the System.Reflection.Binder class has to do you'll get a good idea of this.

It would appear that generic method overloads are always placed at the 'top' of the sort order, meaning that they will always be used, even if there is a specific non-generic overload that matches the input parameters exactly.

Interestingly - this behaviour also seems to be the case even when the use of constraints on a generic type parameter effectively remove the generic method from being a possible candidate method for a call.  My framework has an extra class on top of 'ObjectBase' which in fact any custom types would inherit from, so I modified the constraint on my generic to specify that type instead, and move the generic method declaration to that type (to avoid it appearing in misleading situations):

  1. public class ObjectCustomBase : ObjectBase
  2. {
  3.     public void Clone<T>(ref T target, ObjectBase parent)
  4.         where T : class, ObjectCustomBase
  5.    {
  6.      //can safely cast down to ObjectBase from ObjectCustomBase
  7.      ObjectBase basetarget = target;
  8.      Clone(ref basetarget, parent); //<--compiler error
  9.      if(target == null)
  10.         target = basetarget as T;  
  11.    }
  12. }

This is where I start to wonder if there is a bug in the compiler, we get a compilation error on the line indicated in the comments with the following error:

The type 'ObjectBase' cannot be used as type parameter 'T' in the generic type or method 'ObjectCustomBase.Clone<T>(ref T, ObjectBase)'. There is no implicit reference conversion from 'ObjectBase' to 'ObjectCustomBase'.

Even though we now have an unambiguous call to the base 'Clone' method in ObjectBase - because its signature matches exactly the parameter types being passed in, and yet the compiler is totally ignoring it in favour of the generic, whose type constraint means that it should not be entertained at all.  If anyone has any ideas how to sidestep this I'd be interested to here about them.

Annoyed

What's annoying is, whereas you can force a call to a generic method overload (say, for the purposes of intellisense or whatever) by explicitly specifying the angle brackets - but you cannot explicitly call a non-generic overload.  Nor can I think of a suitable way that the language spec can be modified to include such a feature.  Basically here we have a feature of the language that prevents us from doing something that would otherwise be trivial.

Solutions

There are two. 

1) Silly, but allows us to maintain our intended design - reflect-invoke the non-generic overload of the 'Clone' method instead of directly calling it.  We can cache the MethodInfo for it to avoid having to repeat those slow operations (see my previous posts on C# Meta-programming), however, it's a lot of work when we could instead just...

2) Change the generic method name!

Comments

Popular posts from this blog

Asp.Net 2 and 4 default application pool generates CS0016 IIS7.5

Before I start – if you’ve found a bunch of other articles about this around the net, tried the fixes that are mentioned and still not getting any joy – then read on – you might find this solves your problem. Earlier today I discovered that when I run any ASP.Net 2 or 4 application through IIS7.5 using the default application pools (which use ApplicationPoolIdentity) on Windows 2008 R2 x64 I get an error message similar to this: Server Error in '/MvcApplication31' Application. Compilation ErrorDescription: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS0016: Could not write to output file 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\mvcapplication31\222b4fe6\4e80a86\App_global.asax.clb4bsnc.dll' -- 'The directory name is invalid. '
Source Error:[No relevant source lines]
Sou…

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly.Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘Types supported by the Data Contract Serializer’.The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me!Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable Asp.Net compatibil…

Adding ‘Deny’ functionality to AuthorizeAttribute in Asp.Net Web API

For the web service project I’m working on at the moment I need to be able to treat authorization differently based on the hostname of the URL that requests are made through.To state more clearly – these web services will have a ‘sandbox’ mode in addition to the real mode, and the mode a request will operate under is determined as part of the controller-selection phase early in the Web API request lifecycle.  So, say that my web services will be hosted on services.acme.com; the sandbox will simply be sandbox.services.acme.com.Please note – a discussion of how this is implemented is entirely outside the scope of this article; but I’ll just say that I’ve developed an in-house multi-tenancy layer for both MVC 4 and Web API that allows us to define ‘brands’ and, under those, you can then redefine content, controllers, and even the DI container that is used.These services are going to require caller-level authentication for most operations via SCRAM Authentication (RFC 5802), and as such m…