0

My goal is to have a series of overloads, where the correct version of a method gets called depending on the type of the parameter (known only at runtime). However, I've run into an interesting problem in a case where the method I want to overload is a constructor.

Take the following inheritance structure:

Public MustInherit Class A
    Public Property Common As String
End Class

Public Class X
    Inherits A

    Public Property Unique1 As String
    Public Property Unique2 As String
End Class

Public Class Y
    Inherits A

    Public Property Unique3 As String
    Public Property Unique4 As String
End Class

Base class A is inherited by both X and Y.

Now take this class which I'll use to show the problem:

Public Class Foo
    Public Sub New(v As X)
        Common = v.Common
        Prop1 = v.Unique1
        Prop2 = v.Unique2
        Prop3 = "Some value"
        Prop3 = String.Empty
    End Sub

    Public Sub New(v As Y)
        Common = v.Common
        Prop1 = "Some value"
        Prop2 = String.Empty
        Prop3 = v.Unique3
        Prop4 = v.Unique4
    End Sub

    Public ReadOnly Property Common As String
    Public ReadOnly Property Prop1 As String
    Public ReadOnly Property Prop2 As String
    Public ReadOnly Property Prop3 As String
    Public ReadOnly Property Prop4 As String

    Public Shared Sub Bar(v As X)
    End Sub

    Public Shared Sub Bar(v As Y)
    End Sub
End Class

There is a normal method Bar with an overload, and also a constructor New with an overload. The first New has the same signature as the first Bar, and the second New has the same signature of the second Bar.

Finally take this test code:

Public Sub Test()
    Dim Param As Object = New X

    'This works fine
    Foo.Bar(Param)

    'This gives a compile error
    Dim Thing As New Foo(Param)
End Sub

The compiler seems to have no problem with the call to Bar, but for the constructor call I get the following compile error:

Overload resolution failed because no accessible 'New' can be called without a narrowing conversion:
'Public Sub New(v As X)': Argument matching parameter 'v' narrows from 'Object' to 'X'.
'Public Sub New(v As Y)': Argument matching parameter 'v' narrows from 'Object' to 'Y'.

Why does the constructor call cause an error while the call to Bar does not.

Also, if I change the Param declaration to Dim Param As A = New X, then neither of them will compile.

I feel like I should understand this one, but for whatever reason I don't. Could someone fill me in on why this doesn't work, and maybe suggest a work-around?

Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • 1
    You have `Option Strict Off`, set it `On` and it will be more clear. `Dim Param As New X()` would work for both. – Jimi Apr 02 '21 at 19:29
  • @Jimi `Dim Param As New X` would work, but that's missing the point. "My goal is to have a series of overloads, where the correct version of a method gets called depending on the type of the parameter **(known only at runtime)**." – Keith Stein Apr 02 '21 at 19:51
  • Then implement an Interface instead of inheriting from an abstract class. You'll also have Generics support. The point of having the `Option Strict` support is that you'd see that both constructs fail at compile time, so you don't need to ask yourself *why one fails?*, because both actually fail, it's just temporarily hidden from you. – Jimi Apr 02 '21 at 20:05
  • @Jimi Both pairs *don't* fail. The `Bar` overloads work as expected. `Dim Param As Object = New X` results in `Bar(v As X)` being called, and `Dim Param As Object = New Y` results in `Bar(v As Y)` being called. Only the constructor overloads produce any error. – Keith Stein Apr 02 '21 at 20:11
  • @Jimi Implementing an interface doesn't work for me because `X` and `Y` (in the real code) have different members that are unique, and the different method overloads need to handle the two types differently. – Keith Stein Apr 02 '21 at 20:13
  • You're still referring to code tested with `Option Strict Off`. Setting it `On`, both the construction of `Foo` and the call to `Bar()` are not allowed, because implicit conversion is not allowed (as it should be). It's like allowing implicit conversion between types, hiding narrowing or widening conversions. So you sum integers with strings with automatic *hopeful* conversions between one type or another. Good luck. -- Implementing an Interface has nothing to do with specific implementations or the presence of specialized method/properties in the classes that implement the Interface. – Jimi Apr 02 '21 at 20:21
  • You can even use an Interface that doesn't define any method or property and you have Generics support anyway. – Jimi Apr 02 '21 at 20:22
  • @Jimi "implicit conversion is not allowed (as it should be)". "As it should be" is an opinion. `Option Strict` is an option. I understand there are advantages, but I'd like to avoid discussing the pros/cons in these comments. In the above code, `Option Strict` is `Off`; implicit conversion is allowed. That said, it seems I don't quire understand how you mean for me to implement and use an interface. If you want to post an answer with some code showing me an example of that, I'd be happy to see it. – Keith Stein Apr 02 '21 at 20:31
  • Sorry, but I agree with @Jimi here. You will be chasing lots of potential issues with your code if you rely on runtime to sort this out. (What will you do when someone passes in an instance of class `Z` which inherits `A`, and there is no corresponding constructor or shared method?) I searched, and could not find a simple answer to why `Option Strict Off` seems to be ignored for constructors. – Sean Skelly Apr 02 '21 at 22:12
  • If you want our help to solve your bigger goal (take object of unknown type that derives from `A`, and at runtime and ensure the proper function is run on it), you'll need to give us more information about what that function might look like. Add more meat to the bones of your sample code, and then we can probably show you what an interface solution looks like. – Sean Skelly Apr 02 '21 at 22:13
  • @SeanSkelly In the case of such a `Z` class, I would expect an exception. As for your request for more information, I've added to my example code. Let me know if you need any further information and I'll try to update my question accordingly. – Keith Stein Apr 02 '21 at 22:28
  • 1
    Sorry, the problem with `Option Strict Off` is not just that it's `Off` (with all that this directly implicates), but that you then structure your application with sort of a loosely typed model in mind, hence too many parts of the code rely on this non-feature (made to ease VB6 migration), trying to ignore the strongly-typed Framework you're working with. This generates code that just hides the problems. -- You're trying to build a Generic structure, but you don't want to talk about Interfaces. .Net uses interfaces for this, not the `Object` type and inheritance is limited to a single class. – Jimi Apr 02 '21 at 23:23
  • Everything you’ve already been told is more than valid. But another option you can go with is to have a constructor that actually takes an object parameter, then within that check the type and call the appropriate class constructor – Hursey Apr 03 '21 at 08:09
  • In short though, what you are encountering is 100% correct and valid. You are calling the constructor with an object param, not any of the classes you’ve defined – Hursey Apr 03 '21 at 08:12
  • @Hursey "_... check the type and call the appropriate class constructor_". Aren't you kinda describing a Factory pattern approach here? Sounds valid, though an Interface would be the simpler approach I think. – SteveCinq Apr 05 '21 at 16:28

2 Answers2

0

While it's still unclear exactly what you are trying to achieve, an Answer is the only reasonable place to share code. Here is an attempt at solving your problem with Option Strict On, using an interface to define the properties that a class must have in order to be passed to a Foo for its construction.

Note the comments in the code which also help explain things.

This abstracts things so that Foo doesn't have to know about all the types derived from A - it only knows about the interface. In fact, it 'inverts' the relationship so that A and its derived types know what is necessary for Foo (per the interface). The rest is the implementation of X and Y, where the definitions of Props 1 through 4 now live (instead of in the various overloaded Foo constructors). This collapses the number of constructors for Foo down to just one.

The logic for translating properties of a class derived from A into Foo's properties has to live somewhere. By pushing this logic out of Foo and instead into the derived classes, you can avoid the narrowing issues that Option Strict Off defers until runtime. In addition, adding a new Z class derived from A is easy, and you don't have to modify Foo to immediately use it.

But again, as it's not perfectly clear what you intend to do with this sample code, it's hard to know if this approach 'works' for what you are thinking of, or not.

Option Strict On

Module Module1
    Sub Main()
        Dim Param As A = New X
        Dim Thing As New Foo(Param)

        Param = New Y
        Thing = New Foo(Param)

        'if you make a new class Z which Inherits A, it will immediately be translatable to Foo
        'albeit with all String.Empty properties unless you override the properties from A
    End Sub

End Module

'Defines what a Foo wants, what a Foo needs
Public Interface IPropertiesForFoo
    ReadOnly Property Common As String
    ReadOnly Property Prop1 As String
    ReadOnly Property Prop2 As String
    ReadOnly Property Prop3 As String
    ReadOnly Property Prop4 As String
End Interface

Public MustInherit Class A
    Implements IPropertiesForFoo

    Public Property Common As String


#Region "IPropertiesForFoo implementation"
    'these are Overridable, so derived classes can choose what to change and what not to
    'note these are all Protected, so only derived classes know about them.  Users of A may not care.

    'This is just one choice;
    ' you could also use Throw New NotImplementedException (instead of Return String.Empty)
    ' and force derived classes to handle every property

    Protected Overridable ReadOnly Property IPropertiesForFoo_Prop1 As String Implements IPropertiesForFoo.Prop1
        Get
            Return String.Empty
        End Get
    End Property

    Protected Overridable ReadOnly Property IPropertiesForFoo_Prop2 As String Implements IPropertiesForFoo.Prop2
        Get
            Return String.Empty
        End Get
    End Property

    Protected Overridable ReadOnly Property IPropertiesForFoo_Prop3 As String Implements IPropertiesForFoo.Prop3
        Get
            Return String.Empty
        End Get
    End Property

    Protected Overridable ReadOnly Property IPropertiesForFoo_Prop4 As String Implements IPropertiesForFoo.Prop4
        Get
            Return String.Empty
        End Get
    End Property

    'private, and doesn't need to be Overridable, as Common can map directly
    Private ReadOnly Property IPropertiesForFoo_Common As String Implements IPropertiesForFoo.Common
        Get
            Return Common
        End Get
    End Property
#End Region
End Class

Public Class X
    Inherits A

    Public Property Unique1 As String
    Public Property Unique2 As String

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop1 As String
        Get
            Return Unique1
        End Get
    End Property

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop2 As String
        Get
            Return Unique2
        End Get
    End Property

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop3 As String
        Get
            Return "Some value"
        End Get
    End Property

    'doesn't need to override Prop4; leave it as String.Empty
End Class

Public Class Y
    Inherits A

    Public Property Unique3 As String
    Public Property Unique4 As String

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop1 As String
        Get
            Return "Some value"
        End Get
    End Property

    'doesn't need to override Prop2; leave it as String.Empty

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop3 As String
        Get
            Return Unique3
        End Get
    End Property

    Protected Overrides ReadOnly Property IPropertiesForFoo_Prop4 As String
        Get
            Return Unique4
        End Get
    End Property
End Class


Public Class Foo

    Public Sub New(v As IPropertiesForFoo)
        Common = v.Common
        Prop1 = v.Prop1
        Prop2 = v.Prop2
        Prop3 = v.Prop3
        Prop4 = v.Prop4
    End Sub

    Public ReadOnly Property Common As String
    Public ReadOnly Property Prop1 As String
    Public ReadOnly Property Prop2 As String
    Public ReadOnly Property Prop3 As String
    Public ReadOnly Property Prop4 As String


End Class

For that matter, depending on what the rest of Foo actually does, you may not even need Foo - just pass around your instances of A, since they are also IPropertiesForFoo. Then pull out their properties labeled as Prop1, Prop2, as needed. (Again, your simplified sample source doesn't hint at the larger context enough to know if this approach fits well, or not.)

Sean Skelly
  • 1,229
  • 7
  • 13
0

I'm sure I'll get plenty of people telling me again that I shouldn't be doing things this way, but here's what I did to actually solve the problem as I needed it:

Public Class Foo
    Public Sub New(v As X)
    End Sub

    Public Sub New(v As Y)
    End Sub

    Public Shared Function Create(v As X) As Foo
        Return New Foo(v)
    End Function

    Public Shared Function Bar(v As Y) As Foo
        Return New Foo(v)
    End Function
End Class

Which lets me use Foo like this:

Dim Param As Object = New Y
Foo.Create(Param)

Yes, the above uses late binding and loosely-typed code. But it also keeps redundant code to a minimum, requires no lengthy interface definition or implementation, is still completely predictable, and does exactly what I want. I do consider features like this useful and valid when used in the right context.

I do still wish I could get some answer as to why the overload resolution works with a normal method but not a constructor. But for now I guess I'll just have to settle for this work-around.

Keith Stein
  • 6,235
  • 4
  • 17
  • 36