Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Object instantiation fails when using overloaded constructor

Tags:

c#

.net

I recently stumbled upon an odd issue which I could not explain and I would be glad if someone could clarify why it happens.

The issue I've encountered is as follows:

I have an interface which is implemented, like so:

namespace InterfaceTwo
{
    public interface IA { }
}

namespace InterfaceTwo
{
    public class A : IA { }
}

And another interface which is implemented in a different project, like so:

namespace InterfaceOne
{
    public interface IB { }
}

namespace InterfaceOne
{
    public class B : IB { }
}

I have an object which uses those interfaces in it's constructors, like so:

using InterfaceOne;
using InterfaceTwo;

namespace MainObject
{
    public class TheMainObject
    {
        public TheMainObject(IA iaObj) { }

        public TheMainObject(IB iaObj) { }
    }
}

And finally, I have a class which aggregates the above object, like so:

using InterfaceTwo;
using MainObject;

namespace ReferenceTest
{
    public class ReferenceTest
    {
        public void DoSomething()
        {
            var a = new A();
            var theMainObject = new TheMainObject(a);
        }
    }
}

Oddly, this code won't compile with the following error:

The type 'InterfaceOne.IB' is defined in an assembly that is not referenced.
You must add a reference to assembly 'InterfaceOne, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
c:\users\harry.baden\documents\visual studio 2013\Projects\ReferenceTest\ReferenceTest\ReferenceTest.cs 11 13 ReferenceTest

I also found that if I change one of the overloads to contain an extra parameter - it does compile... What got me thinking that the problem might be related to some sort of reflection issue which the compiler is running.

Thanks,

Barak.

like image 409
BarakH Avatar asked Apr 30 '15 14:04

BarakH


2 Answers

namespace dependency issue. The error message pretty mush said it: your TheMainObject depends on InterfaceOne and must be properly referenced

this isn't directly related to constructor overloading...

Update: It is more of a compiler behavior. To determine which overloaded method to use, the compiler has to

  1. check all methods with the same name and same # of parameters to see if all the parameter types are referenced
  2. then pick one method that matches the caller's parameter type (explicitly or implicitly).

We can verify step 1 and step 2 are separated with the following code:

using InterfaceOne;
using InterfaceTwo;
namespace MainObject
{
    public class TheMainObject
    {
        public TheMainObject(IA obj) { }
        public TheMainObject(IB obj, int x) { }
    }
}

using InterfaceTwo;
using MainObject;
namespace ReferenceTest
{
    public class ReferenceTest
    {
        public static void DoSomething()
        {
            var a = new A();
            var theMainObject = new TheMainObject(a); //no error
        }
    }
}

The above code compiles because TheMainObject(IB obj, int x) is not a candidate for new TheMainObject(a). However if the constructor is defined as

public TheMainObject(IB obj) { }

or

public TheMainObject(IB obj, int x = 0) { }

a reference to InterfaceTwo.IB is required.

You can avoid this kind of reference check by calling the constructor at run-time, but this is error-prone and you should be cautious. For example:

public static void DoSomething()
{
    var a = new A(); 
    TheMainObject theMainObject = null; 
    var ctor = typeof (TheMainObject).GetConstructor(new[] {typeof (IA)}); 
    if (ctor != null) {
        theMainObject = (TheMainObject) ctor.Invoke(new object[] {a});
    }
}

I did a little more research and found the following resources. Basically the type widening/narrowing step needs to know about all the types involved. (The VB version is just for reference because the C# spec is for VS.Net 2003).

Overload Resolution C#

Overload Resolution Visual Basic

like image 192
Tzu Avatar answered Oct 16 '22 04:10

Tzu


See this for an explanation of a similar problem that I encountered. To quote the answer from the link:

The C# standard specifies that overload resolution (section 7.5.3) is performed by comparing each matching signature to determine which is a better fit. It doesn't say what happens when a reference is missing, so we must infer that it still needs to compare those unreferenced types.

In your example, it should be evident what overload you're using, but the compiler is not smart enough and will still try to compare both overloads, which is why both references are required.

Perhaps the easiest - but not the prettiest - solution (if you don't want to include the missing reference - which you may have a good reason not to) is to add an additional dummy parameter, effectively making it obvious to the compiler which overload you're calling; or transforming the two TheMainObject constructors into two methods with different names, e.g. TheMainObjectA(IA iaObj) and TheMainObjectB(IB ibObj) - i.e. avoiding overloading altogether.

Another possible solution is to use the dynamic keyword (for .NET 4.0 and above), although some people might discourage this as it can result in runtime errors if you're not careful:

public class TheMainObject
{
    public TheMainObject(dynamic obj)
    {
        if (obj is IA)
        {
            // work with IA ...
        }
        else if (obj is IB)
        {
            // work with IB ...
        }
        else
        {
            // exception ...
        }
    }
}

This way, the compiler doesn't generate an error since the obj parameter is evaluated at runtime - your original code will work. If you choose to use this solution, also consider checking for RuntimeBinderException to avoid accidentally accessing invalid (non-existent) members of the dynamic type.

like image 42
w128 Avatar answered Oct 16 '22 03:10

w128