1

Right now I can only use the Jumper class for objects of type "Something". I tried rewriting it as a generic class but ran into difficulties when actually calling it from my SomeForm class because "T" can be one of several types that I only know at runtime. I read that this means I am basically fighting the whole generics language design.

The sample code is VB but I could also work with C# answers.

Question: can this be redesigned with generics or what is a better alternative to generics in my case?

Public Class Jumper
    Private _enumeratorForwards As IEnumerator(Of Something)

    Public Sub JumpNext(functions As FunctionCombi)
        Forwards(functions.SelectorFunc)
    End Sub

    Private Iterator Function Forwards(selectorFunc As Func(Of Something, Boolean)) As IEnumerable(Of Something)

End Class

Public Class FunctionCombi
    Public Property SelectorFunc As Func(Of Something, Boolean)

    Sub New(_selectorFunc As Func(Of Something, Boolean))
        SelectorFunc = _selectorFunc
    End Sub

End Class

Public Class SomeForm

    'x and y would be different types
    Private _functionCombis As New Dictionary(Of SomeEnum, FunctionCombi) From {
                                                   {SomeEnum.A, New FunctionCombi(Function(x) RunSomeFunction(x)},
                                                   {SomeEnum.B, New FunctionCombi(Function(y) RunSomeOtherFunction(y))}

    Private Sub SomeHandler(sender as object, e as EventArgs)
        For i = 1 To [Enum].GetValues(GetType(SomeEnum)).Length

            'The type I would need here differs and I only know it at runtime

            Dim functionInfo As FunctionCombi = Nothing
            If Not _functionCombis.TryGetValue(i, functionInfo) Then Continue For
            Dim jumper As Jumper = sender.Tag(2)
        Next
    End Sub
End Class
Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
chriscode
  • 121
  • 1
  • 8
  • My first thought is: that's what interfaces are for. But it's hard to tell from this incomplete code sample. It does not show how or when `Something` is instantiated, or what interface it implements. It does not explain the purpose of class `Jumper`. – Ruud Helderman Feb 17 '22 at 12:48
  • Off-topic: there are [better ways to iterate through an enum](https://stackoverflow.com/questions/105372/how-to-enumerate-an-enum). – Ruud Helderman Feb 17 '22 at 13:09
  • Something is a placeholder for Microsoft office com objects that implement e.g. the 'Shape' interface or 'Slide' interface. So basically I cannot change these classes. E.g. I want to jump to a shape or slide with the Jumper class and perform some action on the shape or slide. – chriscode Feb 17 '22 at 13:26
  • You can instantiate generic types at runtime. For example, you could instantiate a list by getting the unspecialized type as e.g. `listTypeOpen = GetType(List(Of ))` and then make the specialized type using `listType = listTypeOpen.MakeGenericType(someType)`. I'm not sure if this would help you, because you would only be able to work with the result through reflection, so it's most suitable to library code that's trying to assemble an object structure (as in deserialization code). – Craig Feb 17 '22 at 14:08
  • @Craig like this? Dim listTypeOpen = GetType(List(Of )) Dim listType = listTypeOpen.MakeGenericType(GetType(Integer)) Dim test As New List(Of listType) I get the following error: Type 'listType' is not defined – chriscode Feb 17 '22 at 14:20
  • You can't do the third line. You're trying to make a compile-time instance of the list there, but this approach only works for runtime instances. As I said, you can only work through the instance using reflection. (In your example, `listType` is equivalent to `List(Of Integer)`, but the reason for doing this is that you don't know at compile time what the specialized type is going to be.) – Craig Feb 17 '22 at 14:25
  • @RuudHelderman Could you please elaborate whether Interfaces could make sense here (considering my comment about Com Objects) and if yes, how I should go about implementing it. Thanks a lot – chriscode Feb 21 '22 at 11:00
  • You're working your way towards finding an optimal common supertype for a number of classes, one of which is `Something`. To help you, I need to know what those classes have in common. In the code sample I see 'something' eventually being passed into the parameter of functions `RunSomeFunction` and `RunSomeOtherFunction`. So you expect those functions to be able to handle the other classes as well. I need the implementation of those functions. What are the functions doing with that 'something' being passed into their parameter? Is 'something' defining some method for the functions to call? – Ruud Helderman Feb 21 '22 at 11:59
  • @RuudHelderman thanks, I will try to explain it better: for now please forget about 'Something'. What the classes have in common is the interface 'Shape' (that is provided by MS Office). A 'Shape' also has a 'TextRange' interface. I want my Jumper class to be able to jump to either a Shape (if the SelectorFunc returns True for that shape) or a TextRange (if the SelectorFunc returns True for that TextRange). So basically the SelectorFunc can either be (Of Shape, Boolean) or (Of TextRange, Boolean). Therefore the _enumeratorForwards in the Jumper class can also be (Of Shape) or (Of TextRange). – chriscode Feb 21 '22 at 13:28
  • Please explain class `Jumper`. What, in your vocabulary, does it mean to "jump" to a shape? Does it simply mean, getting the first element in the `IEnumerable` for which the `SelectorFunc` returns True? If so, then my next question would be, what are you going to do with the object you found? It could be either a Shape or a TextRange, and you are getting this for a reason. There must be some kind of action you'd like to perform on that object, and apparently that is something that applies to shapes as well as text ranges. What is that action? – Ruud Helderman Feb 21 '22 at 17:45
  • Your interpretation is on point, i.e. jump means getting the next element for which SelectorFunc returns True. I think it would help me a lot if in the above code the Jumper could return a 'Shape' or 'TextRange' (that's why I was initially thinking about generics). As I see it this means somehow changing the FunctionCombi class (which would need to be able to handle Of Shape or Of TextRange) without knowing the type at compile time. Thanks a lot for your help – chriscode Feb 21 '22 at 20:44

1 Answers1

1

Generic types are most likely not what you need, as those are resolved in compile time. For different types to be resolved at runtime, use an interface.

If one would be in full control of the types involved, that would simply mean: define an interface and let both types implement that interface.

Public Interface IOfficeObject
    ' any members that Shape and TextRange have in common
End Interface

Public Interface Shape
    Inherits IOfficeObject

    ' any members specific for Shape
End Interface

Public Interface TextRange
    Inherits IOfficeObject

    ' any members specific for TextRange
End Interface

Then Jumper would simply talk to that interface, without being concerned about the underlying implementations. Jumper shouldn't be concerned about those underlying classes, otherwise it would defy the whole point of having both classes in the same IEnumerator.

Public Class Jumper
    Private _enumeratorForwards As IEnumerator(Of IOfficeObject)

    ...
End Class

You did not explain what Shape and TextRange (and any other MS interfaces you may be interested in) have in common, neither did you explain what kind of actions your code will be taking on those objects. This is a shame, as it means I'll have to make my point with some hypothetical examples.

Let's assume Shape and TextRange have the following in common:

  • a property Application of type Application, similar to this
  • a method Copy, similar to this

This makes my common interface:

Public Interface IOfficeObject
    ReadOnly Property Application As Application
    Sub Copy()
End Interface

Unfortunately, you are not in control of Shape and TextRange; they are interfaces defined by Microsoft. Apparently, Microsoft did not bother to define a common interface to be inherited by both Shape and TextRange. I don't know why; I'm not familiar with those interfaces or their history. It may have been an oversight, turning into a legacy.

You can use the adapter pattern to resolve that problem.

Public Class ShapeAdapter
    Implements IOfficeObject

    Private ReadOnly _shape As Shape

    Public Sub New(shape As Shape)
        _shape = shape
    End Sub

    Public ReadOnly Property Application As Application
        Get
            Return _shape.Application
        End Get
    End Property

    Public Sub Copy()
        _shape.Copy()
    End Sub

    ' Any other members, forwarding to _shape
End Class

Public Class TextRangeAdapter
    Implements IOfficeObject

    Private ReadOnly _textRange As TextRange

    Public Sub New(textRange As TextRange)
        _textRange = textRange
    End Sub

    Public ReadOnly Property Application As Application
        Get
            Return _textRange.Application
        End Get
    End Property

    Public Sub Copy()
        _textRange.Copy()
    End Sub

    ' Any other members, forwarding to _textRange
End Class

Producing an enumerable of objects involves wrapping each object in its adapter class. Example:

Dim listOfOfficeObjects As New List(Of IOfficeObject)
listOfOfficeObjects.Add(New ShapeAdapter(shape1))
listOfOfficeObjects.Add(New TextRangeAdapter(textRange1))
listOfOfficeObjects.Add(New ShapeAdapter(shape2))
listOfOfficeObjects.Add(New TextRangeAdapter(textRange2))

(If you prefer a factory over explicit instantiation of ShapeAdapter and TextRangeAdapter, that's fine of course.)

Class Jumper will retrieve these adapters. Consumers will always be talking to these adapters, through interface IOfficeObject. In your own code sample, I recognize two consumers, but I'm sure there will be more in the solution you are working on.

Public Function RunSomeFunction(obj As IOfficeObject) As Boolean
    ' pulling whatever is necessary from IOfficeObject to come to a return value
End Function

Public Function RunSomeOtherFunction(obj As IOfficeObject) As Boolean
    ' likewise
End Function

If a consumer needs anything from Shape or TextRange that is not exposed by the interface and/or not implemented by the adapters, then that is something that needs to be fixed in the interface and the adapters. If it cannot be implemented by the adapters, then there is a flaw in your architecture.

Ruud Helderman
  • 10,563
  • 1
  • 26
  • 45