1

I am trying to do some data ingestion from different data files (Excel) with two known formats (represented by Classes D1 and D2, derived from Class B which contains common elements). I have to create many sets of D1 and D2 instances before doing the final data set assembly, and each set is created through a typed Class ExemplarIssue.

Is there a way to create an instance of my data elements (D1, D2) using parameters without getting the BC32085 error? My minimal example is included here, the detail I provide below is to provide some context in how I got here.

Public Class ExemplarIssue(Of T As {B, New})
    Public Sub New()
    End Sub

    Public Sub Test1()
        Dim x As New T
        Dim y As New T("1", "2") 'Error BC32085 Arguments cannot be passed To a 'New' used on a type parameter. 
        Dim z As New Collections.Generic.List(Of T)
        z.Add(y)
    End Sub
End Class

Public MustInherit Class B
    Public Sub New()
    End Sub

    Public Sub New(s1 As String, s2 As String)
    End Sub
End Class

Public Class D1
    Inherits B

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(s1 As String, s2 As String)
        MyBase.New(s1, s2)
    End Sub
End Class

Public Class D2
    Inherits B
    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(s1 As String, s2 As String)
        MyBase.New(s1, s2)
    End Sub
End Class

The work involves passing formatted data (s2 in well-understood formats) which is ingested by the Class into useable properties. There is a whole bunch of data checks that happen within the encapsulated class.

I am trying to make the ingested data immutable (i.e. only expose read-only properties). As such, I would like to constrain use of the date element Classes so that creating one does not make any sense without the data to be stored. This is usually done with a parameterised New(...) routine, and not including the unparameterised New() routine.

I am trying to keep my code DRY - not make a similar ExemplarIssue Class for each Dx. And in order to find and process each file I have created a class that runs a routine (Test1 in this case). I am using a Class because there will be many of these mini data sets that I must process later to create a master clean data set. The custom typed class allows me to add additional checks and functions that I could not get just by using a generic collection. These additional functions includes having a data ingest error log, hence why it is encapsulated in a class rather than as a function in a main module.

AJD
  • 2,400
  • 2
  • 12
  • 22
  • *"Classes `D1` and `D2`, inherited from Class `B`"*. You are mixing your terminology. `D1` and `D2` inherit `B` and are derived from `B`. – jmcilhinney Aug 11 '23 at 02:32
  • Given that neither `D1` nor `D2` have a constructor with parameters, how could your code possibly work? If it compiled, it would throw an exception at run time anyway. You seem to be under the impression that creating a derived type can invoke a base constrictor but it can't without a derived constrictor doing so explicitly. – jmcilhinney Aug 11 '23 at 02:40
  • @jmcilhinney: This was a minimally typed example - the real example has D1 and D2 constructors with corresponding parameters. Will fix in the example. – AJD Aug 11 '23 at 03:05
  • It still can't work because the code can't know that those are the only two types derived from B that will be specified so it can't know that any specified type will have such a constructor. The New constraint applies only to parameterless constructors. The complexity of being able to specify an arbitrary number of parameters of arbitrary types was presumably considered not worth it. – jmcilhinney Aug 11 '23 at 03:20
  • As an alternative, you could consider using reflection to get the constructor from the type and invoke through the ConstructorInfo. This would allow passing parameters. There wouldn't be any way to reflect this with a generic constraint or a compile-time check, though, if a type violated the contract it would fail at run-time. – Craig Aug 11 '23 at 14:49

2 Answers2

1

The New constraint for generic type parameters applies to a parameterless constructor only. In order to handle other constructors, they'd have to provide a syntax to specify an arbitrary number of method parameters of arbitrary types. I would imagine that the complexity was deemed to not be worth it, particularly as that would be making generics less generic. If the common base type has corresponding properties then you can use an object initialiser to provide those values after invoking the parameterless constructor.

jmcilhinney
  • 50,448
  • 5
  • 26
  • 46
0

@jmcilhinney provides the technically correct answer - not by design. This is also backed up by and answer to "Restricting a generic type parameters to have a specific constructor (C#)" (https://stackoverflow.com/a/18565782/9101981).

My first work-around was to take the Protected IngestData method and make it Public, along with adding a non-parameterised 'New()' constructor. This then exposes the class instances to being altered by code at a later stage. For my own project this is an acceptable risk, but I am trying to write 'better' code.

I then looked at "How do you make a Generic Generic Factory? (C#)" (https://stackoverflow.com/a/3390813/9101981) and did some exploring. For my use case, I did not want to expose the factory so that is can only be used when I deliberately constrain it to be used. So, instead of creating a singleton, I made it non-creatable.

Public MustInherit Class ExemplarFactory(Of T As {B})
    Protected Function createB2(s1 As String, s2 As String) As B
        If GetType(T) = GetType(D1) Then Return New D1(s1, s2)
        If GetType(T) = GetType(D2) Then Return New D2(s1, s2)
        Return Nothing
    End Function
End Class

As a result, then ExemplarIssue and minimal example code looks like the following. I have added some extra bits so that I could create tests to ensure my idea worked.

Public Class ExemplarIssue(Of T As {B})
    Inherits ExemplarFactory(Of T)

    Public Property TestList As New Collections.Generic.List(Of T)

    Public Sub New(s3 As String)
        ' dummy parameter to match real life example. This parameter drives the loop in Test1
    End Sub

    Public Sub Test1(s2 As String)
        Dim w As T
        For i As Long = 1 To 5
            ' dummy loop , "i" is a contexttual loop in the real life example
            'Also the first parameter is for testing purposes and woudl be replaced with a real value in the real life example
            w = CType(createB2(GetType(T).Name, s2 & Format(i)), T)
            _TestList.Add(w)
        Next
    End Sub

End Class

Public MustInherit Class B

    Public ReadOnly Property ID As String
    Public ReadOnly Property CName As String

    Public Sub New(s1 As String, s2 As String)
        _ID = s1
        _CName = s2
    End Sub

End Class

Public Class D1
    Inherits B
    Public Sub New(s1 As String, s2 As String)
        MyBase.New(s1, s2)
    End Sub

End Class

Public Class D2
    Inherits B
    Public Sub New(s1 As String, s2 As String)
        MyBase.New(s1, s2)
    End Sub
End Class

You will see that the parameter-less constructors have been removed. The question is, why did I go to the extent of making a factory when I could have just done this within the ExemplarIssue code? This allows for easy re-use. It also means that if I add another format (D3), I only have to update the factory, not find the right lines of code in the original class. By making it MustInherit, I prevent accidental creation, and by making the creation method Protected I prevent the accidental creation outside when I intend in the main class.

Finally - the results of the test, and the test code I ran:

    Count of items - C1
5
D1: Test parameter1
D1: Test parameter2
D1: Test parameter3
D1: Test parameter4
D1: Test parameter5
Count of items - C2
5
D2: Test parameter - C21
D2: Test parameter - C22
D2: Test parameter - C23
D2: Test parameter - C24
D2: Test parameter - C25
    <TestMethod()> Public Sub TestFactory1()
        Dim C1 As MyNamespace.ExemplarIssue(Of MyNamespace.D1)
        Dim C2 As MyNamespace.ExemplarIssue(Of MyNamespace.D2)
        Dim dTest1 As MyNamespace.D1 = New MyNamespace.D1("D1 from Dim", "freestanding test")

        C1 = New MyNamespace.ExemplarIssue(Of MyNamespace.D1)("useless parameter")
        C1.Test1("Test parameter")

        Console.WriteLine("Count of items - C1")
        Console.WriteLine(C1.TestList.Count)
        For Each bItem In C1.TestList
            Console.WriteLine(String.Format("{0}: {1}", bItem.ID, bItem.CName))
        Next

        C2 = New MyNamespace.ExemplarIssue(Of MyNamespace.D2)("useless parameter")
        C2.Test1("Test parameter - C2")
        Console.WriteLine("Count of items - C2")
        Console.WriteLine(C2.TestList.Count)
        For Each bItem In C2.TestList
            Console.WriteLine(String.Format("{0}: {1}", bItem.ID, bItem.CName))
        Next
    End Sub
AJD
  • 2,400
  • 2
  • 12
  • 22