2

There is an unexplained ambiguity in C#, where I explicitly try to call a constructor but the compiler thinks it is a different constructor. I will start with showing a short C# architecture we use. Then show a small "working" example I created, and the possible solution to this, but still I like to understand why this happens.

The Architecture:

enter image description here

  1. CLR DLL which bridges the C++ API.
  2. C# API which uses the bridge level.
  3. C# Client applications that use the C# API.
  • Note that the C# Clients are not allowed to use the CLR level.

Example I created

A class in the CLR DLL:

#pragma once

#include <string>

using namespace System;

namespace Inner {
    public ref class AInner
    {
    public:

        AInner() : _data(new std::wstring(L"")) {}

        ~AInner() {
            delete _data;
        }

        property String^ Val
        {
            String^ get()
            {
                return gcnew String((*_data).data());
            }

            void set(String^ value) {
                System::IntPtr pVal = System::Runtime::InteropServices::Marshal::StringToHGlobalUni(value);
                *_data = (const wchar_t*)pVal.ToPointer();
                System::Runtime::InteropServices::Marshal::FreeHGlobal(pVal);
            }
        }

    private:

        std::wstring* _data;
    };
}

Class wrapping the CLR level, in a DLL:

using System;

using Inner;

namespace Outer
{
    public class A
    {
        public A()
        {
            _inner.Val = String.Empty;
        }

        public A(string val)
        {
            init(val);
        }

        public string Val
        {
            get
            {
                return _inner.Val;
            }
            set
            {
                _inner.Val = value;
            }
        }

        internal A(AInner inner)
        {
            _inner = inner;
        }

        private void init(string Val)
        {
            _inner = new AInner();
            _inner.Val = String.Empty;
        }

        private AInner _inner;
    }
}

Note that there is an internal constructor and a public constructor.

Executable Client using the C# API DLL:

using Outer;

namespace OneClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string myString = "Some String";
            A testA = new A(myString);
        }
    }
}

Twist in the story:

In the DLL wrapping the CLR level, not ALL API should be used by external clients, but can be used by internal clients, thus the internals are exposed to the internal clients by adding [assembly: InternalsVisibleTo("OneClient")] to the 'AssemblyInfo.cs' of the DLL wrapping the CLR level.

The issue

When compiling the Client code I get the following error: error CS0012: The type 'AInner' is defined in an assembly that is not referenced. You must add a reference to assembly 'InnerOne, Version=1.0.7600.28169, Culture=neutral, PublicKeyToken=null'.

  1. I cannot use InnerOne because clients are not allowed to use this level.
  2. The client is exposed to both A(string val) and A(AInner inner) constructors.

Possible Workarounds:

  1. Remove the [assembly: InternalsVisibleTo("OneClient")] - This is unacceptable due to other classes internals that the specific client needs to use.
  2. Change the A(string val) constructor to A(string val, bool unique=true) and use it A testA = new A(myString, true) - Not a nice solution.
  3. Use default constructor A() and call testA.Val = myString; - This is actually OK but to much code.
  4. Change the client code from A testA = new A(myString) to A testA = new A(val:myString); - This is actually the chosen solution.

Question

Why does this ambiguity happen?

  • I call the A(string val) with the myString which is actually a string value This is very strange.

Is this a bug in Microsoft compiler?

Example Sources: Source Code One.zip

Juv
  • 744
  • 7
  • 12
  • 1
    I really don't see any ambiguity, you are using the namespace "Inner" in your wrapper class, however, the compiler does not know where to find it, do you agree? If you have a type defined in another assembly, unless you reference that assembly you won't be able to use the classes that are visible. Furthermore, any constructor you create will still be throwing the same compile error. – Bruno Oct 23 '20 at 20:13
  • Yes I see that now, basically the answer from @johnathan-barclay explains that the complier needs to see the references in order to distinguish between the constructors/functions. – Juv Oct 23 '20 at 20:24

2 Answers2

2

Why does this ambiguity happen?

Because to satisfy the constructor overload resolution, the compiler needs to know what all the argument types are, and it doesn't know what an AInner is.

Why not expose the AInner version as a factory method:

static internal A Create(AInner inner)
{
    return new A { _inner = inner };
}
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
  • If I understand you correctly: because the constructor with `AInner` is exposed, and the client actually sees it, it doesn't know what `AInner` is without its reference, and can't distinguish it from the `string` constructor (can't choose). As for your question, I think you nailed it, we do have a Create method but the factory is working slightly different, and since its a vast API existing for almost 20 years now, it won't be easy to change. – Juv Oct 23 '20 at 20:15
1

I don't see any issue in this, the problem is we are used to do the things in a wrong/briefly way.

The correct answer fot this is:

A testA = new A(val:myString);

Furthermore, all your calls (in this way is a call to a constructor/initializer but it's a call anyway) should be with the parameter name. No one (even me) writes them, but...

Leandro Bardelli
  • 10,561
  • 15
  • 79
  • 116