0

I am trying to get a generic method to extend Array from the following piece of code:

Public Class clsField
    Public idx As String
    Public name As String
    Public weight As Long

    Public Sub New(i As String, n As String, w As Long)
        idx = i : name = n : weight = w
    End Sub
End Class

Public Class Container
    Public fields As clsField() ' filled in by a JSON parser (order matters)

    ' returns a list sorted by clsField.weight preserving order for elements with same 'weight' value
    Public Function getFields() As List(Of KeyValuePair(Of String, clsField))
        Dim auxList As List(Of KeyValuePair(Of String, clsField))

        If (fields Is Nothing) OrElse (fields.Count < 1) Then Return New List(Of KeyValuePair(Of String, clsField))
        ' .ToList to transform IEnumerable to the return type
        auxList = Array.ConvertAll(fields, New Converter(Of clsField, KeyValuePair(Of String, clsField))(AddressOf FieldToPair)).ToList
        Return auxList.OrderBy(Function(x) x.Value.weight).ToList()
    End Function

    Public Shared Function FieldToPair(fld As clsField) As KeyValuePair(Of String, clsField)
        Return New KeyValuePair(Of String, clsField)(fld.idx, fld)
    End Function
End Class

I am stuck with the Converter(Of TInput, TOutput) Delegate, used by Array.ConvertAll, which will not accept a new parameter, provided that I can pass a function to specify the key that should be used on TInput:

Private Function ClassToPair(Of T)(obj As T, getProperty As Func(Of T, Object)) As KeyValuePair(Of String, T)
    Return New KeyValuePair(Of String, T)(getProperty(obj), obj)
End Function

Perhaps there is a way to Overload Array.ConvertAll and create an alternative Delegate to Converter, with a signature that allows to complete the following code (that obviously do not compile for ConvertAll and AddressOf ClassToPair; added here to reflect the idea) :

Module ArrayExtension ' custom method for array
    ' returns a list sorted by clsField.weight preserving order for elements with same 'weight' value
    ' getKey is used to transform the array into a List (Of KeyValuePair (Of String, T)) -> using the Converter
    ' getSortProperty is used to change the sorting 'property'
    <Extension()>
    Public Function toSortedPairedList(Of T)(arr As T(), Optional getKey As Func(Of T, String) = Nothing,
           Optional getSortProperty As Func(Of KeyValuePair(Of String, T), Object) = Nothing) _
           As List(Of KeyValuePair(Of String, T))
        Dim auxList As List(Of KeyValuePair(Of String, T))

        If (arr Is Nothing) OrElse (arr.Count < 1) Then Return New List(Of KeyValuePair(Of String, T))

        ' .ToList to transform IEnumerable to the return type
        auxList = Array.ConvertAll(arr, New Converter(Of T, KeyValuePair(Of String, T))(AddressOf ClassToPair)).ToList
        Return auxList.OrderBy(getSortProperty).ToList()

    End Function

    Private Function ClassToPair(Of T)(obj As T, getProperty As Func(Of T, Object)) As KeyValuePair(Of String, T)
        Return New KeyValuePair(Of String, T)(getProperty(obj), obj)
    End Function
End Module

So, no way to pass the getKey function to the Converter...

For the first example its usage would be something like:

Public Function getFields() As List(Of KeyValuePair(Of String, clsField))
    Dim auxList As List(Of KeyValuePair(Of String, clsField))

    If (fields Is Nothing) OrElse (fields.Count < 1) Then Return New List(Of KeyValuePair(Of String, clsField))

    Return fields.toSortedPairedList(Function(x) x.idx, Function(y) y.Value.weight)
End Function
rellampec
  • 698
  • 6
  • 22
  • This is not possible to do without **storing** the `Func` `getProperty` to access the property of the contained object `T` [... `array() as T` ] that tells which is the `TKey` to create the `KeyValuePair` during the Conversion... Therefore the function that implements the conversion should store `getProperty` first in a variable that can be **accessed** and called from `ClassToPair`. So to say it should be implemented in two times: **1.** store `getProperty`, **2.** `Array.ConvertAll`... – rellampec Feb 22 '17 at 22:06

1 Answers1

0

I will provide an answer to my question.

Although I do not like this approach for cleanness reasons (i.e. too many type parameters for the method), it seems the only approach to solve it by the use of Extension Methods.

It has the advantage of enabling the use of the Converter, working around the restrictions that the Delegate signature introduces, by storing the KeySelector into a generic, module variable: Private accessKey As Object.

My guess is that the best approach would be an implementation of a full generic class that do not rely only in KeyValuePairs, but with a wider set of conversions.

First of all, in the aim of storing the KeySelector that will help to ConvertAll of a List(Of TValue) into a List(Of KeyValuePair(Of TKey, TValue)), a generic class is necessary so we can store a generic lambda with no compilation errors (reference) (there seems not to be a better way to work around the limitations that the Converter Delegate introduces):

' to store a generic lambda: https://stackoverflow.com/a/3116009/6215377
' the class shows up to be necessary, as it cannot be stored by the use of generic delegates 
' otherwise, you will always end up having to instantiate to specific types for KeyValuePair, 
' losing so the generic declaration
Interface IAccesser(Of TValue, TKey)
    Function accessProperty(input As TValue) As TKey
End Interface

Public Class PropertyAccessor(Of TValue, TKey)
    Implements IAccesser(Of TValue, TKey)
    Private pFunc As Func(Of TValue, TKey)

    Public Sub New(f As Func(Of TValue, TKey))
        pFunc = f
    End Sub
    Function accessProperty(o As TValue) As TKey Implements IAccesser(Of TValue, TKey).accessProperty
        Return pFunc(o)
    End Function
End Class

Now the Extension Module re-adapted, provided that the getKey function passed as argument to toSortedPairedList is stored into the generic object of the module, accessKey As Object, before calling ConvertAll with ClassToPair as Converter (the one that will use accessKey). This will require some type castings from the Object to the class PropertyAccessor:

Imports System.Linq.Enumerable
Imports System.Runtime.CompilerServices ' for extensions

Module ArrayExtension  ' custom method for array
    Private accessKey As Object ' to store the generic lambda

    Private Function ClassToPair(Of TValue, TKey)(obj As TValue) As KeyValuePair(Of TKey, TValue) ' the Converter
        ' this is the one that avoids to mess around with delegates (as instances of delegates cannot be generic)
        Dim a As PropertyAccessor(Of TValue, TKey) = DirectCast(accessKey, PropertyAccessor(Of TValue, TKey)) 
        Return New KeyValuePair(Of TKey, TValue)(a.accessProperty(obj), obj)
    End Function

    <Extension()> ' the type params list gets long, as we target it to be generic
    Public Function toSortedPairedList(Of TValue, TKey, TSort, TReturn)(arr As TValue(), getKey As Func(Of TValue, TKey),
           Optional getSortProperty As Func(Of KeyValuePair(Of TKey, TValue), TSort) = Nothing) _
           As List(Of KeyValuePair(Of TKey, TValue))

        If (getKey Is Nothing) OrElse (arr Is Nothing) OrElse (arr.Count < 1) Then Return New List(Of KeyValuePair(Of TKey, TValue)) ' empty list (instead of nothing)
        Dim a As PropertyAccessor(Of TValue, TKey) = New PropertyAccessor(Of TValue, TKey)(getKey)
        accessKey = a ' here we store / assign, so we can use it within the Converter function ClassToPair (with the delegate signature that introduced the problem)

        ' Typecasting Generic parameter: https://stackoverflow.com/q/2891797/6215377 (can throw an exception; i.e. TSort = Integer, TKey = non-numeric String)
        ' NOTE: this part is not essential (just an improvement)
        ' NOTE II: we leave the Exception Catch to the caller (an improvement would be to throw an adapted Exception from here)
        If getSortProperty Is Nothing Then getSortProperty = Function(x) CType(CObj(a.accessProperty(x.Value)), TSort) ' defaulting to sort by getKey(obj)

        Dim auxList As List(Of KeyValuePair(Of TKey, TValue))
        auxList = Array.ConvertAll(arr, New Converter(Of TValue, KeyValuePair(Of TKey, TValue))(AddressOf ClassToPair(Of TValue, TKey))).ToList()
        Return auxList.OrderBy(getSortProperty).ToList()  ' .ToList to transform IEnumerable to the return type
    End Function

    ' Array Extension: - https://stackoverflow.com/a/30151099/4352306 
    ' irrelevant to this question (to Push into array)
    <Extension()>
    Public Sub Add(Of T)(ByRef arr As T(), item As T)
        If arr IsNot Nothing Then
            Array.Resize(arr, arr.Length + 1)
            arr(arr.Length - 1) = item
        Else
            ReDim arr(0)
            arr(0) = item
        End If
    End Sub
End Module

And, finally, this is the tester, to show its usage:

Public Class clsField
    Public idx As String
    Public name As String
    Public weight As Long

    Public Sub New(i As String, n As String, w As Long)
        idx = i : name = n : weight = w
    End Sub

    Public Overrides Function ToString() As String
        Return String.Format("{0}: {1} - {2}", name, idx, weight)
    End Function
End Class

Public Class Container
    Public fields() As clsField

    ' here we call the extended method
    Public Function getSortedPairs() As List(Of KeyValuePair(Of String, clsField))
        Return fields.toSortedPairedList(Of String, Long, List(Of KeyValuePair(Of String, clsField)))(Function(f) f.idx, Function(f) f.Value.weight)
    End Function

    ' it calls to the function above and converts back to List(Of clsField)
    ' NOTE: not necessary; added to show more ideas of its usability
    Public Function getSortedFields() As List(Of clsField) 
        Return getSortedPairs.ConvertAll(Function(pair) pair.Value)
    End Function
End Class

Public Class Consumer
    Public cont As Container

    Public Sub New()
        cont = New Container
        cont.fields.Add(New clsField("ffq", "foo30004", 33))
        cont.fields.Add(New clsField("ffc", "foo9997", 55))
        cont.fields.Add(New clsField("ffp", "foo9908", 55))
        cont.fields.Add(New clsField("ffo", "foo100001", 22))
        cont.fields.Add(New clsField("ffx", "foo8885", 33))
        cont.fields.Add(New clsField("ffz", "foo70002", 22))
        cont.fields.Add(New clsField("ffy", "foo8806", 33))
        cont.fields.Add(New clsField("ffa", "foo9009", 55))
        cont.fields.Add(New clsField("ffb", "foo8000", 55))
        cont.fields.Add(New clsField("ffn", "foo7003", 22))
    End Sub

    Public Sub printSortedFields()
        For Each e As clsField In cont.getSortedFields()
            Console.WriteLine(e.ToString())
            Debug.Print(e.ToString())
        Next
    End Sub

    Public Sub Main()
        printSortedFields()
    End Sub
End Class

The test writes this output (entries created so last digit of name confirms the testing):

foo100001: ffo - 22
foo70002: ffz - 22
foo7003: ffn - 22
foo30004: ffq - 33
foo8885: ffx - 33
foo8806: ffy - 33
foo9997: ffc - 55
foo9908: ffp - 55
foo9009: ffa - 55
foo8000: ffb - 55

Hope this helps to someone with similar issues. Although I do not see this as a definitive solution, it provides some directions to take or discard ways to overcome difficulties of type conversions when using generic methods and lambdas to sort arrays and lists.

Best wishes

NOTE: the ordering method was designed so it preserves the original inserting order for elements with same value.

Community
  • 1
  • 1
rellampec
  • 698
  • 6
  • 22