7

I have the following Function (try-catch removed):

    Friend Shared Function ConvertOrDefault(Of T As {Structure, IConvertible})(convertFrom As Object, ignoreCase As Boolean) As T
        Dim retVal As T
        If Not GetType(T).IsEnum Then
            Throw New ArgumentException("Type must be enum")
        ElseIf convertFrom Is Nothing OrElse Not TypeOf convertFrom Is String Then
            Return New T
        ElseIf [Enum].TryParse(convertFrom.ToString(), ignoreCase, retVal) Then
            Return retVal
        Else
            Return New T
        End If
End Function

Which converts the given type to an enum (hence the constraints), if it is one.

That's fine, but I then have another method (simplified below) that does more general casting, and I want it to use that method if the type passed in is an enum:

Friend Shared Function Convert(Of T)(value as Object) As T
    If GetType(T).IsEnum Then
         Return Enums.ConvertOrDefault(Of T)(value, True)
    Else : return DirectCast(value, T)
    End If
End Function

For the call to Enums.ConvertOrDefault, this gives the errors:

Type argument 'T' does not inherit from or implement the constraint type 'System.IConvertible'
Type argument 'T' does not satisfy the 'Structure' constraint for type parameter 'T'

How can I say "it's OK, I know it's an Enum so it's fine"?

--- Edit ---

One (very ugly) way to do it is as follows:

Dim type As Type = GetType(T)

If type.IsEnum Then
    Select Case type.Name
        Case "EnumTypeOne"
            Return DirectCast(DirectCast(Enums.ConvertOrDefault(Of EnumTypeOne)(value, True), Object), T)
         ' ...

But that's hideous. Surely there's a way to generalise that?

-- Edit 2: Purpose --

I'm reading data from an Oracle database, which stores the Enums (of which I have several) as strings; as well as storing other data in various formats (Byte() as RAW, TimeSpan as IntervalDS, etc). I then use the Convert function as a generic function where, given the result of datareader(column), I can convert that object into the appropriate type.

All of the Oracle.DataAccess.Client.OracleDataReader's .Get... functions take an index rather than a column name; and as I can't guarantee the order of the columns, and for the sake of readability, using the column name makes more sense - but then I have to parse the output myself.

So my code is doing something like:

Dim id as Byte() = Convert(dataReader("id_column"))
Dim something as SomeEnum = Convert(dataReader("somethingCol"))
'...

I could deliberately call Enum.ConvertOrDefault instead of Convert when I'm expecting an Enum, but that seems to break the principle of a general method, which I think makes more sense... and would also allow me to reuse that method in other contexts.

Hope that helps clarify a bit.

--- Edit 3 ---

I tried this idea, from the comments:

Friend Shared Function Convert(Of T As {New})(value as Object) as T

and

Friend Shared Function ConvertOrDefault(Of T As{New}) convertFrom As Object, ignoreCase As Boolean) As T
    If Not GetType(T).IsEnum Then
        Throw New ArgumentException("Type must be enum")
    ElseIf convertFrom Is Nothing OrElse Not TypeOf convertFrom Is String Then
        Return New T
    End If
    Try
        Return CType([Enum].Parse(GetType(T), convertFrom.ToString(), ignoreCase), T)
    Catch ex As Exception
    End Try

    ' default
    Return New T
End Function

But this gives errors when I call the Convert method for types like String or Byte(), saying

"Type argument 'String' must have a public parameterless instance constructor to satisfy the 'New' constraint for type parameter 'T'

simonalexander2005
  • 4,338
  • 4
  • 48
  • 92
  • A slightly messy way would be to add `Enum` to the constraints but then force an error in the instantiation. This does do something very similar to a constraints failure. – Paul Aug 21 '15 at 09:57
  • I'm not sure I understand, sorry. Add `Enum` where? – simonalexander2005 Aug 21 '15 at 10:02
  • Just ignore me - forgot that you can't add Enum to the constraints... :-/ – Paul Aug 21 '15 at 10:12
  • Yeah, I thought that was the case - and even if you could, it wouldn't remove the problem of passing a more general generic type into a constrained method – simonalexander2005 Aug 21 '15 at 10:13
  • You could remove the constraints and test inside the function, but that's simply not elegant. – Paul Aug 21 '15 at 10:18
  • and it also prevents the constrained function from using `Return New T` if the constraints aren't there – simonalexander2005 Aug 21 '15 at 10:20
  • Can you [Edit] to explain a little what you want these to do/how you'd use them. I *think* there is a different way to go about it, but its hard to tell what the functional requirements are. – Ňɏssa Pøngjǣrdenlarp Aug 24 '15 at 15:51
  • @Plutonix does that help? – simonalexander2005 Aug 25 '15 at 08:35
  • Can you not write an overload ConvertOrDefault(Of T as {Enum})? – David W Aug 26 '15 at 14:19
  • @DavidW No, that's not allowed - you can't add `Enum` to constraints – simonalexander2005 Aug 26 '15 at 14:25
  • @simonalexander2005 My fault - misapplying constraint for generic type. Thanks. However, there is an ugly trick discussed in http://stackoverflow.com/questions/1331739/enum-type-constraints-in-c-sharp that might apply here for the enum constraint issue – David W Aug 26 '15 at 14:28
  • 1
    @DavidW Thanks for that! That's interesting. I'll see if it works when I get some time. It is very ugly though! – simonalexander2005 Aug 26 '15 at 14:32
  • "All of the Oracle.DataAccess.Client.OracleDataReader's .Get... functions take an index rather than a column name" - Please verify that the sole intent of your Convert Functions is to avoid using the the datareader's GetOrdinal method to retrieve the index of a given field name. Or did I miss something? – TnTinMn Aug 27 '15 at 03:08
  • interesting, I didn't know about GetOrdinal, and your comment prompted me to do some research... OK, so I can use GetOrdinal() to get the column, but that still doesn't help completely - as OracleDataReader.Get... doesn't have every available type - GetByteArray doesn't exist, or GetMySpecificEnum ... but your comment has certainly helped me to make my code more efficient, thanks :) – simonalexander2005 Aug 27 '15 at 08:08
  • To keep consistency with using the datareader Getxxx methods, you could write the additional ones you require as Extension Methods to the datareader class. – TnTinMn Aug 27 '15 at 20:49
  • It finally occurred to me that reason for the Structure constraint on ConvertOrDefault is due too the use of `Enum.TryParse` that also imposes that constraint. Also, your assertion above that the Structure constraint is required to allow 'Return New T` is false; the use of the `New` constraint will allow that; i.e. `ConvertOrDefault(Of T As {New})`. With that stated, you could use the `Enum.Parse` method inside a try-catch block as the Parse method does not impose the Structure constraint. – TnTinMn Aug 27 '15 at 20:50
  • see edit 3 - I'm having trouble because constraining `Enums.ConvertOrDefault` to `New` means I have to do the same for `Convert` - which means I can't call it with a `String`, or a `Byte()` type, for example – simonalexander2005 Aug 28 '15 at 09:36
  • Ok, sorry about not checking the implication of the New constraint on your Convert function. The thing is though that your ConvertOrDefault can only accept Enum types without throwing a error. With Enum types (ValueType), there is no need to explicitly call `New TEnum`. Your statement `Dim retVal As T` will initialize `retVal` to the default value, so just return that instead of `New TEnum`. – TnTinMn Aug 28 '15 at 14:24
  • That works :D Thanks! If you write that as an answer you can have the bounty. It's a shame we couldn't answer the question as it stands though - "How can I say "it's OK, I know it's an Enum so it's fine"?", or, can you further limit T when passing it to another method – simonalexander2005 Aug 28 '15 at 14:30
  • @simonalexander2005 Do you have to use actual enums? Or do you just need a fixed set of values to match against? – Tyree Jackson Aug 28 '15 at 16:10

3 Answers3

3

You may want to consider using a different kind of enumerated set of values. You may be able to use the polymorphic/class/subclassable enum pattern instead.

The ones I use usually have a TrySelect method for resolving underlying values to the enum. Also, you can support multiple underlying values of different types for each enum value. For example:

public class BoolEnum
{
    private static Dictionary<bool, BoolEnum>   allValuesByNaturalValue = new Dictionary<bool, BoolEnum>();
    private static Dictionary<string, BoolEnum> allValuesByTextValue    = new Dictionary<string, BoolEnum>();
    private static Dictionary<int, BoolEnum>    allValuesByInteger      = new Dictionary<int, BoolEnum>();

    private string boolText;
    private int    integerValue;
    private bool   naturalValue;

    public static readonly BoolEnum True  = new BoolEnum(true, "Is True", 1);
    public static readonly BoolEnum False = new BoolEnum(false, "Is False", 0);

    private BoolEnum(bool naturalValue, string boolText, int integerValue)
    {
        this.naturalValue = naturalValue;
        this.boolText     = boolText;
        this.integerValue = integerValue;
        allValuesByNaturalValue.Add(naturalValue, this);
        allValuesByTextValue.Add(boolText, this);
        allValuesByInteger.Add(integerValue, this);
    }

    public static BoolEnum TrySelect(bool naturalValue, BoolEnum defaultValue)
    {
        BoolEnum returnValue;
        if (allValuesByNaturalValue.TryGetValue(naturalValue, out returnValue)) return returnValue;
        return defaultValue;
    }

    public static BoolEnum TrySelect(string boolText, BoolEnum defaultValue)
    {
        BoolEnum returnValue;
        if (allValuesByTextValue.TryGetValue(boolText, out returnValue)) return returnValue;
        return defaultValue;
    }

    public static BoolEnum TrySelect(int integerValue, BoolEnum defaultValue)
    {
        BoolEnum returnValue;
        if (allValuesByInteger.TryGetValue(integerValue, out returnValue)) return returnValue;
        return defaultValue;
    }

    public static implicit operator bool(BoolEnum boolEnum)
    {
        return boolEnum != null ? boolEnum.naturalValue : false;
    }

    public static implicit operator string(BoolEnum boolEnum)
    {
        return boolEnum != null ? boolEnum.boolText : "Is False";
    }

    public static implicit operator int(BoolEnum boolEnum)
    {
        return boolEnum != null ? boolEnum.integerValue : 0;
    }

    public bool   NaturalValue { get { return this.naturalValue; } }
    public string BoolText     { get { return this.boolText; } }
    public int    IntegerValue { get { return this.integerValue; } }

    public static IReadOnlyCollection<BoolEnum> AllValues        { get { return allValuesByNaturalValue.Values.ToList().AsReadOnly(); } }
    public static IReadOnlyCollection<bool>     AllBooleanValues { get { return allValuesByNaturalValue.Keys.ToList().AsReadOnly(); } }
    public static IReadOnlyCollection<string>   AllTextValues    { get { return allValuesByTextValue.Keys.ToList().AsReadOnly(); } }
    public static IReadOnlyCollection<int>      AllIntegerValues { get { return allValuesByInteger.Keys.ToList().AsReadOnly(); } }

    public override string ToString()
    {
        return "[" + this.naturalValue.ToString() + ", \"" + this.boolText.ToString() + "\", " + this.integerValue.ToString() + "]";
    }

}

Then you can add methods to your enums for more specialized operations. You could build maps with your enums that map column to index position, etc. You can also easily iterate over the set of enumerated values or underly values easily using one the All* properties (BoolEnum.AllValues, BoolEnum.AllBooleanValues, BoolEnum.AllTextValues, BoolEnum.AllIntegerValues).

FYI> It is not that difficult to implement this using generics so that most of the boilerplate is DRYed away. The subclassable example (disclaimer: that is my article on this) is based on the use of a generic base enum class.

Here is a dotnetfiddle showing the above example enum in action: https://dotnetfiddle.net/O5YY47

Here is a VB.Net version of the above:

Public Class BoolEnum
    Private Shared allValuesByNaturalValue As New Dictionary(Of Boolean, BoolEnum)()
    Private Shared allValuesByTextValue As New Dictionary(Of String, BoolEnum)()
    Private Shared allValuesByInteger As New Dictionary(Of Integer, BoolEnum)()

    Private m_boolText As String
    Private m_integerValue As Integer
    Private m_naturalValue As Boolean

    Public Shared ReadOnly [True] As New BoolEnum(True, "Is True", 1)
    Public Shared ReadOnly [False] As New BoolEnum(False, "Is False", 0)

    Private Sub New(naturalValue As Boolean, boolText As String, integerValue As Integer)
        Me.m_naturalValue = naturalValue
        Me.m_boolText = boolText
        Me.m_integerValue = integerValue
        allValuesByNaturalValue.Add(naturalValue, Me)
        allValuesByTextValue.Add(boolText, Me)
        allValuesByInteger.Add(integerValue, Me)
    End Sub

    Public Shared Function TrySelect(naturalValue As Boolean, defaultValue As BoolEnum) As BoolEnum
        Dim returnValue As BoolEnum
        If allValuesByNaturalValue.TryGetValue(naturalValue, returnValue) Then
            Return returnValue
        End If
        Return defaultValue
    End Function

    Public Shared Function TrySelect(boolText As String, defaultValue As BoolEnum) As BoolEnum
        Dim returnValue As BoolEnum
        If allValuesByTextValue.TryGetValue(boolText, returnValue) Then
            Return returnValue
        End If
        Return defaultValue
    End Function

    Public Shared Function TrySelect(integerValue As Integer, defaultValue As BoolEnum) As BoolEnum
        Dim returnValue As BoolEnum
        If allValuesByInteger.TryGetValue(integerValue, returnValue) Then
            Return returnValue
        End If
        Return defaultValue
    End Function

    Public Shared Widening Operator CType(boolEnum As BoolEnum) As Boolean
        Return If(boolEnum IsNot Nothing, boolEnum.naturalValue, False)
    End Operator

    Public Shared Widening Operator CType(boolEnum As BoolEnum) As String
        Return If(boolEnum IsNot Nothing, boolEnum.boolText, "Is False")
    End Operator

    Public Shared Widening Operator CType(boolEnum As BoolEnum) As Integer
        Return If(boolEnum IsNot Nothing, boolEnum.integerValue, 0)
    End Operator

    Public ReadOnly Property NaturalValue() As Boolean
        Get
            Return Me.m_naturalValue
        End Get
    End Property
    Public ReadOnly Property BoolText() As String
        Get
            Return Me.m_boolText
        End Get
    End Property
    Public ReadOnly Property IntegerValue() As Integer
        Get
            Return Me.m_integerValue
        End Get
    End Property

    Public Shared ReadOnly Property AllValues() As IReadOnlyCollection(Of BoolEnum)
        Get
            Return allValuesByNaturalValue.Values.ToList().AsReadOnly()
        End Get
    End Property
    Public Shared ReadOnly Property AllBooleanValues() As IReadOnlyCollection(Of Boolean)
        Get
            Return allValuesByNaturalValue.Keys.ToList().AsReadOnly()
        End Get
    End Property
    Public Shared ReadOnly Property AllTextValues() As IReadOnlyCollection(Of String)
        Get
            Return allValuesByTextValue.Keys.ToList().AsReadOnly()
        End Get
    End Property
    Public Shared ReadOnly Property AllIntegerValues() As IReadOnlyCollection(Of Integer)
        Get
            Return allValuesByInteger.Keys.ToList().AsReadOnly()
        End Get
    End Property

    Public Overrides Function ToString() As String
        Return "[" + Me.m_naturalValue.ToString() + ", """ + Me.m_boolText.ToString() + """, " + Me.m_integerValue.ToString() + "]"
    End Function

End Class

And here is the dotnetfiddle for the VB.Net version: https://dotnetfiddle.net/HeCA5r

Community
  • 1
  • 1
Tyree Jackson
  • 2,588
  • 16
  • 22
2

You are using VB.NET, a language that is already pretty friendly to dynamic typing. There's little you can do with generic type constraints on Enums, a pretty hard limitation in .NET. Core problem is that enum types cannot behave generically, their storage size depends on the specific type. Which can be 1, 2, 4 or 8 bytes, depending on the base type. That's a very big deal to generics, the cookie-cutter (aka MSIL) is different.

So just punt the problem, VB.NET provides the ball, in a case like this you really like the Conversion.CTypeDynamic() helper function. You just need a bit of extra code to deal with null objects and case sensitivity. You also should consider handling DBNull when you do this for dbase field conversions:

Friend Function Convert(Of T)(convertFrom As Object, Optional ignoreCase As Boolean = True) As T
    If convertFrom Is Nothing Then Return Nothing
    If GetType(T) = GetType(DBNull) Then Return Nothing
    If GetType(T).IsEnum Then
        Return CTypeDynamic(Of T)([Enum].Parse(GetType(T), convertFrom.ToString(), ignoreCase))
    Else
        Return CTypeDynamic(Of T)(convertFrom)
    End If
End Function

Note the other VB.NET implementation detail in this code, you don't need new T. Nothing is already a perfectly fine value for an enum. And you don't need to throw any exceptions, CTypeDynamic already complains with a documented exception message that is localized.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • My version of [Enum].Parse (.NET 4.6.2) still throws System.ArgumentException when convertFrom string is not found in the Enum names. Also, the return type cannot be Nothing. The default Enum value is always returned. – Dan Randolph Dec 14 '20 at 20:59
1

That works :D Thanks! If you write that as an answer you can have the bounty. It's a shame we couldn't answer the question as it stands though - "How can I say "it's OK, I know it's an Enum so it's fine"?", or, can you further limit T when passing it to another method.

The implementation of what I recommended is shown below. As an attempt to explain why you can not do what you want to do, perhaps this will help. Since in Convert(Of T) and T can represent any type, there is no way for the compiler to guaranty type safety when passing it to a more constrained generic type; there is CType function for generic types.

I do not understand why the Enum.TryParse method invokes a Structure constraint. Looking at the source code it still checks the Type.IsEnum property, so it seems superfluous to try and impose a partial constraint.

Friend Shared Function Convert(Of T)(value As Object) As T
   If GetType(T).IsEnum Then
      Return ConvertOrDefault(Of T)(value, True)
   Else
      Return DirectCast(value, T)
   End If
End Function

Friend Shared Function ConvertOrDefault(Of TEnum)(convertFrom As Object, ignoreCase As Boolean) As TEnum
   ' Since this function only excepts Enum types, declaring the return value
   ' will initialize it to zero.
   Dim retVal As TEnum
   Dim typeTEnum As System.Type = GetType(TEnum)
   If typeTEnum.IsEnum Then
      Dim convertFromString As String = TryCast(convertFrom, String)
      If convertFrom IsNot Nothing AndAlso convertFromString IsNot Nothing Then
         Try
            retVal = DirectCast(System.Enum.Parse(typeTEnum, convertFromString), TEnum)
         Catch ex As ArgumentNullException
            ' eat it
         Catch ex As ArgumentException
            ' eat it
         Catch ex As OverflowException
            ' eat it
         End Try
      End If
   Else
      Throw New ArgumentException("Type must be enum")
   End If
   Return retVal
End Function
TnTinMn
  • 11,522
  • 3
  • 18
  • 39