Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<Flags> Enum operators #520

Open
AdamSpeight2008 opened this issue Mar 30, 2020 · 21 comments
Open

<Flags> Enum operators #520

AdamSpeight2008 opened this issue Mar 30, 2020 · 21 comments

Comments

@AdamSpeight2008
Copy link
Contributor

Enum Operators

Operating with <Flags>Enums can be a point a pain, so I've been thinking for quite a while on have the following operators. That expand the capabilities of the dictionary lookup operator, eg does the variable have the flag IsShared set? We are looking up the flag(s).

  • source ! flaganame and source !( flagsEnumExpression )
    • Are all of the matching flags set in source.
  • source !? flagname and source !?( flagsEnumExpression )
    • Are any of the matching flags set is source.
  • source !+ flagname and source !+ ( flagsEnumExpression )
    • Set the matching flags in source.
  • source !- flagname and source !- ( flagsEnumExpression )
    • Clear the matching flags in source.

The the converted to the calls or implementation of the corresponding methods. This also allows compile-time constant folding to happen. As well compile-time validation of the flagname.

Function AllSet(Of T As <Flags>Enum)(source As T, matching As T) As Boolean
  Return (source And matching) = matching
End Function

Function AnySet(Of T As <Flags>Enum)(source As T, matching As T) As Boolean
  Return (source And matching) <> 0
End Function

Function SetFlags(Of T As <Flags>Enum)(source As T, matching As T) As T
  Return (source Or matching)
End Function

Function ClearFlags(Of T As <Flags>Enum)(source As T, matching As T) As T
  Return (source And (Not matching))
End Function

Implementation

The expression are to represented via a FlagsEnumExpression that supports the above operators,
an implementation detail there will be a "effective direct cast conversion" from the textual member (eg the flag's name) to a FlagsEnumExpression (eg flags value).
<Flags>Enum -> FlagsEnumExpression

Note we'll require some smarts in the compiler to differeniate between the type character singlevariable! ' Of Type Single and that of these operators.

@VBAndCs
Copy link

VBAndCs commented Mar 30, 2020

Too symbolic. Try use keywords to express them to be a VB style.

@AdamSpeight2008
Copy link
Contributor Author

@VBAndCs any suggestions for keywords?

@paul1956
Copy link

paul1956 commented Apr 3, 2020

AllSet, AnySet, SetFlags, and ClearFlags but I am not sure why you would not just use the extensions shown except maybe for the constant folding or performance.

@AdamSpeight2008
Copy link
Contributor Author

@paul1956 The extension methods are applicable to any enum, not just those with a <Flags> attribute.

@VBAndCs
Copy link

VBAndCs commented Apr 4, 2020

Until now, Enum can't be a type param constraint. This can work today:

Module Flags

    <Extension>
    Public Function [Set](e As [Enum], ParamArray flags() As [Enum]) As [Enum]
        If flags Is Nothing OrElse flags.Count = 0 Then Return e
        Dim result As [Enum]
        For Each flag In flags
            result = [Enum].Parse(e.GetType, Convert.ToInt32(e) Or Convert.ToInt32(flag))
        Next
        Return result
    End Function

    Public Function IsSet(e As [Enum], ParamArray flags() As [Enum]) As Boolean
        If flags Is Nothing OrElse flags.Count = 0 Then Return e
        Dim result As [Enum]
        For Each flag In flags
            result = [Enum].Parse(e.GetType, Convert.ToInt32(e) And Convert.ToInt32(flag))
        Next
        Return Convert.ToInt32(result) <> 0
    End Function

End Module

But the problem is that I assumed is ToInt32 to be the Enum type. There is no easy solution to this except checking for all numeric types and writing a branch of code for each!
I hope to have a new Flag type dedicated for this, that can do low level bitwise operation with pointers directly, to be fast and accurate.

@AdamSpeight2008
Copy link
Contributor Author

In your example the parameters e and flags can contain values from different enums.

@VBAndCs
Copy link

VBAndCs commented Apr 4, 2020

True. Needs type checking. This was a quick example.

@tverweij
Copy link

tverweij commented Apr 8, 2020

Added the following to the Mercury.dll
Completely strict, including type checking and working with the correct enum underlying type.
note: only quickly tested.

        <Runtime.CompilerServices.Extension>
        Public Function SetFlags(e As [Enum], ParamArray flags() As [Enum]) As [Enum]
            If flags Is Nothing OrElse flags.Length = 0 Then Return e
            For Each flag In flags
                If e.GetType <> flag.GetType Then Throw New ArgumentException("flag Is Not of the same type as the enum")
                If Not e.HasFlag(flag) Then
                    e = DirectCast(CTypeDynamic(CTypeDynamic(Convert.ToInt64(e) + Convert.ToInt64(flag), e.GetType.GetEnumUnderlyingType), e.GetType), [Enum])
                End If
            Next
            Return e
        End Function

        <Runtime.CompilerServices.Extension>
        Public Function UnsetFlags(e As [Enum], ParamArray flags() As [Enum]) As [Enum]
            If flags Is Nothing OrElse flags.Length = 0 Then Return e
            For Each flag In flags
                If e.GetType <> flag.GetType Then Throw New ArgumentException("flag Is Not of the same type as the enum")
                If e.HasFlag(flag) Then
                    e = DirectCast(CTypeDynamic(CTypeDynamic(Convert.ToInt64(e) - Convert.ToInt64(flag), e.GetType.GetEnumUnderlyingType), e.GetType), [Enum])
                End If
            Next
            Return e
        End Function

        <Runtime.CompilerServices.Extension>
        Public Function AreFlagsSet(e As [Enum], ParamArray flags() As [Enum]) As Boolean
            If flags Is Nothing OrElse flags.Length = 0 Then Return False
            Dim result As Boolean = True
            For Each flag In flags
                result = result And e.HasFlag(flag)
            Next
            Return result
        End Function

@VBAndCs
Copy link

VBAndCs commented Apr 8, 2020

@
Nice, but this is not the perfect performance. I wrote this because it is the only way not the best way:
e = DirectCast(CTypeDynamic(CTypeDynamic(Convert.ToInt64(e) + Convert.ToInt64(flag), e.GetType.GetEnumUnderlyingType), e.GetType), [Enum])

It will be better if you provide a way to ease the enum operation. We need to deal with the exact type ot the enum, as Comvert.ToInt64 will return wrong results with other numeric types.
I imagine that should be an Enum(of T as Numeric) . One pain in generics in .NET is the missing Numeric constraint.
You should also create the overloaded operators for +-*/ And AndAlso Or OrElse Xor Not for the generic type Enum, so that we can just write:

   If e.HasFlag(flag) Then
         e = e - flag
  End If

This is how a smart VB should be.

@tverweij
Copy link

tverweij commented Apr 8, 2020

Agreed.
But at this point I (just like you) have to work with the available functionality (we can not change and add all at once - as I do the Mercury.dll, I can add things like this to it).

I used the Int64 as that is the biggest possible type for an enum, to cast it (dynamic) to the correct type - otherwise a select case was needed. For now a working solution that will be improved in the future.

@VBAndCs
Copy link

VBAndCs commented Oct 30, 2020

The true solution for this issue is to crate a Flag class. And to avoid generics and base class issues, each Flag class should be a full stand alone class, containing all what the flag needs. To make this practical, I created a flag generator app here:
https://github.com/VBAndCs/VB-Flag-auto-generator

Flags
It is a Windows Forms app, that allows you to enter the name of the Flag and its members, then press the Generate button to auto-generate a class that represents this flags and its members, and many methods to do flag operations and set and unset flags.
The underline type of the flag is Integer, and the class contains CType operators to convert between them. This is a list of the members I added to the class:
Shared Fields:
NoneSet
AllSet
Shared Properties:
Flags
FlagNames
FlagValues
Instance Properties:
Name
OnFlags
OffFlags
Instance Methods:
ToString()
ToInteger
SetFlags
SetAllExcxept
UnsetFlags
UnsetAllExcxept
ToggleFlags
ToggleAllFlags()
AreAllSet
AreAllUnset
AreAnySet
AreAnyUnset

And this is a sample of an auto-generated Flag:

Class MyFlag
    Public Shared ReadOnly X As new MyFlag("X", 1)
    Public Shared ReadOnly Y As new MyFlag("Y", 2)
    Public Shared ReadOnly Z As new MyFlag("Z", 4)
    Public Shared ReadOnly NoneSet As MyFlag = 0
    Public Shared ReadOnly AllSet As MyFlag = 7

    Public Shared ReadOnly Property Flags As MyFlag() = {X, Y, Z}
    Public Shared ReadOnly Property FlagNames As String() = {"X", "Y", "Z"}
    Public Shared ReadOnly Property FlagValues As Integer() = {1, 2, 4}
    Public ReadOnly Property Name As String

    Public ReadOnly Property OnFlags As List(Of MyFlag)
        Get
            Dim lstFlags As New List(Of MyFlag)
            For Each flag In Flags
                If (Value And flag.Value) > 0 Then lstFlags.Add(flag)
            Next
            Return lstFlags
        End Get
    End Property

    Public ReadOnly Property OffFlags As List(Of MyFlag)
        Get
            Dim lstFlags As New List(Of MyFlag)
            For Each flag In Flags
                If (Value And flag.Value) = 0 Then lstFlags.Add(flag)
            Next
            Return lstFlags
        End Get
    End Property


    Dim Value As Integer

    Private Sub New(value As Integer)
        Me.Value = value
    End Sub

    Private Sub New(name As String, value As Integer)
        _Name = name
        Me.Value = value
    End Sub

    Public Shared Widening Operator CType(value As Integer) As MyFlag
        Return New MyFlag(value)
    End Operator

    Public Shared Narrowing Operator CType(flag As MyFlag) As Integer
        Return flag.Value
    End Operator

    Public Overrides Function ToString() As String
        Dim sb As New Text.StringBuilder
        For Each flag In Flags
            If (Value And flag.Value) > 0 Then
                If sb.Length > 0 Then sb.Append(" + ")
                sb.Append(flag.Name)
            End If
        Next
        Return sb.ToString
    End Function

    Public Function ToInteger() As Integer
        Return Value
    End Function

    Public Function SetFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value

        Dim v = Value
        For Each flag In flags
            v = v Or flag.Value
        Next
        Return v
    End Function

    Public Function SetAllExcxept(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value

        Dim v As Integer = 0
        For Each flag In flags
            v = v Or flag.Value
        Next
        Return Value And Not v
    End Function


    Public Function UnsetFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value

        Dim v = Value
        For Each flag In flags
            v = v And Not flag.Value
        Next
        Return v
    End Function

    Public Function UnsetAllExcxept(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value

        Dim v As Integer = 0
        For Each flag In flags
            v = v Or flag.Value
        Next
        Return v
    End Function

    Public Function ToggleFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value

        Dim v = Value
        For Each flag In flags
            v = v Xor flag.Value
        Next
        Return v
    End Function

    Public Function ToggleAllFlags() As MyFlag
        Return Value Xor 7
    End Function

    Public Function AreAllSet(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value = 7

        For Each flag In flags
            If (Value And flag.Value) = 0 Then Return False
        Next
        Return True
    End Function

    Public Function AreAllUnset(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value = 0

        For Each flag In flags
            If (Value And flag.Value) <> 0 Then Return False
        Next
        Return True
    End Function

    Public Function AreAnySet(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value > 0

        For Each flag In flags
            If (Value And flag.Value) <> 0 Then Return True
        Next
        Return False
    End Function

    Public Function AreAnyUnset(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value < 7

        For Each flag In flags
            If (Value And flag.Value) = 0 Then Return True
        Next
        Return False
    End Function
End Class

and this is a sample top show how to use it:

Dim flag As MyFlag = MyFlag.X
        flag = flag.SetFlags(MyFlag.Z)
        Console.WriteLine(flag) ' X + Z
        Console.WriteLine(flag.AreAllSet()) ' False
        flag = flag.ToggleAllFlags()
        Console.WriteLine(flag) ' Y
        flag = flag.SetFlags(MyFlag.Z)
        Console.WriteLine(flag) ' Y + Z
        Console.WriteLine(flag.ToInteger) ' 6
        Console.WriteLine(flag.AreAllSet(MyFlag.Y, MyFlag.Z)) ' True

To Do:

  • Validate user inputs.
  • Write tests for the FlagGenerator class.
  • Crate a VS.NET extension to add a command to show the auto-generation window and add the generated file to the project.

@rskar-git
Copy link

@VBAndCs I think that Class MyFlag concept misses the point of what "enum flags" are about. For starters, MyFlag should be a value-type (i.e. Structure MyFlag) and not a reference-type. For another, MyFlag members seem to presume all of its constants are equal to some specific power of 2; however, that is not how enum flags are necessarily valued. For example, it is not unusual for a flag named "None" to exist which is set to zero; AreAllSet would fail on that.

Interpreting (or treating) an integer as a packed bits field is a very old-school programming trick. Sometimes it is necessary for interfacing with (manipulating) hardware registers (e.g. code for a device driver). Otherwise, it's merely a way to save on memory, and in some scenarios faster performance.

Enum already has the HasFlag method (https://docs.microsoft.com/en-us/dotnet/api/system.enum.hasflag). One can already do plenty with the bit-wise versions of Or, And, and Not. Just make useful extensions to set and clear bits as you may need them, since that is trivially easy to do. If you need to build-out a value from the power-of-2 constants, + is good enough (otherwise just use Or if you're paranoid). If you're careful to keep all "don't care" bits set to zero, then =0 (none set) and <>0 (any set) can be useful and fast.

If needs are somehow more sophisticated or whatever:

https://docs.microsoft.com/en-us/dotnet/api/system.collections.bitarray

https://docs.microsoft.com/en-us/dotnet/api/system.collections.specialized.bitvector32

https://stackoverflow.com/questions/14464/bit-fields-in-c-sharp

====
Side issue: It is annoying that the .NET generics system is nearly useless for value-types. However, the T4 Text Template can be of help - see T4Example.zip at #462.

@VBAndCs
Copy link

VBAndCs commented Oct 31, 2020

For starters, MyFlag should be a value-type (i.e. Structure MyFlag) and not a reference-type.

Using structures is impossible here. The Falg class is immutable (all methods returns a new instance, andd the constructor is private, so the only way to get a new Flag is by starting with the pre-defined shared flags).

MyFlag members seem to presume all of its constants are equal to some specific power of 2; however, that is not how enum flags are necessarily valued. For example, it is not unusual for a flag named "None" to exist which is set to zero; AreAllSet would fail on that.

I already generates 2 additional fields named NoneSet (Name="None", Value= 0), and AllSet(Name= "All", Value= Max allowed Integer, witch is the sum of the flags).

Interpreting (or treating) an integer as a packed bits field is a very old-school programming trick. Sometimes it is necessary for interfacing with (manipulating) hardware registers (e.g. code for a device driver). Otherwise, it's merely a way to save on memory, and in some scenarios faster performance.

This is exactly what the flag class does, and I think you should give it a try to test it in action.

Finally, flagged enums are not enough (as the discussion in this issues shows), and it is always hard for beginners to use it without a good understanding of binary system, which doesn't fit the VB description, and is an overkill for commercial app developers.

Not that I made some changes in the auto-generated class to display the names of the set flags in the ToString method, andded some new methods, and Overloaded all the arithmetic operators, as VB ignores the fact that the flag can be casted to/from Integer and refused to use =, +, >…etc with it before overriding them! I will report this bad behavior in Roslyn. As I always said, VB Should be smarter than that. I even wonder if a class overloaded = and > why on earth it needs to overload >= unless of course it means something else > or = ?! VB should use her brain to do all what is necessary before crying out for an overloaded operator.

@pricerc
Copy link

pricerc commented Oct 31, 2020

Of course there is already a discussion about enum improvements over in the run time forum.

Which might be a better place for discussions around improvements to enums, since they are framework, not VB constructs.

based on the work of Tyler Brinkley

@pricerc
Copy link

pricerc commented Oct 31, 2020

VB Should be smarter than that. I even wonder if a class overloaded = and > why on earth it needs to overload >= unless of course it means something else > or = ?! VB should use her brain to do all what is necessary before crying out for an overloaded operator.

This limitation exists in C# as well. I presume it's the same as the reason why you have to implement both = and <> (or == and != in C#), if you're implementing one of them.

I like to think that the very smart people behind the design of Roslyn did actually think about this before deciding to do it this way.

If I was to make a list of things I wanted the compiler to do for me that it doesn't already do, this would be way down somewhere near the bottom of the list.

The creating of new classes that need to override >= and <= is such a small part of a developer's work, and has so little bearing on programmer productivity, that I can't see the value in compiler developers implementing code to make assumptions that may not be valid.

@VBAndCs
Copy link

VBAndCs commented Oct 31, 2020

is a small part ....

Small indeed:

    Public Shared Widening Operator CType(value As Integer) As MyFlag
        Return New MyFlag(value)
    End Operator

    Public Shared Narrowing Operator CType(flag As MyFlag) As Integer
        Return flag.Value
    End Operator


    Public Shared Operator +(flag As MyFlag, value As Integer) As Integer
        Return flag.Value + value
    End Operator

    Public Shared Operator -(flag As MyFlag, value As Integer) As Integer
        Return flag.Value - value
    End Operator

    Public Shared Operator *(flag As MyFlag, value As Integer) As Integer
        Return flag.Value * value
    End Operator

    Public Shared Operator /(flag As MyFlag, value As Integer) As Integer
        Return flag.Value / value
    End Operator

    Public Shared Operator ^(flag As MyFlag, value As Integer) As Long
        Return flag.Value ^ value
    End Operator

    Public Shared Operator Or(flag As MyFlag, value As Integer) As MyFlag
        Return New MyFlag(flag.Value Or value)
    End Operator

    Public Shared Operator And(flag As MyFlag, value As Integer) As MyFlag
        Return New MyFlag(flag.Value And value)
    End Operator

    Public Shared Operator Xor(flag As MyFlag, value As Integer) As MyFlag
        Return New MyFlag(flag.Value Xor value)
    End Operator

    Public Shared Operator Not(flag As MyFlag) As MyFlag
        Return New MyFlag(Not flag.Value)
    End Operator

    Public Shared Operator IsTrue(flag As MyFlag) As Boolean
        Return flag.Value > 0
    End Operator

    Public Shared Operator IsFalse(flag As MyFlag) As Boolean
        Return flag.Value = 0
    End Operator

    Public Shared Operator =(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value = value
    End Operator

    Public Shared Operator <>(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value <> value
    End Operator

    Public Shared Operator >(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value > value
    End Operator

    Public Shared Operator <(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value < value
    End Operator

    Public Shared Operator >=(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value >= value
    End Operator

    Public Shared Operator <=(flag As MyFlag, value As Integer) As Boolean
        Return flag.Value <= value
    End Operator

    Public Shared Operator +(flag1 As MyFlag, flag2 As MyFlag) As Integer
        Return flag1.Value + flag2.Value
    End Operator

    Public Shared Operator -(flag1 As MyFlag, flag2 As MyFlag) As Integer
        Return flag1.Value - flag2.Value
    End Operator

    Public Shared Operator *(flag1 As MyFlag, flag2 As MyFlag) As Integer
        Return flag1.Value * flag2.Value
    End Operator

    Public Shared Operator /(flag1 As MyFlag, flag2 As MyFlag) As Double
        Return flag1.Value / flag2.Value
    End Operator

    Public Shared Operator ^(flag1 As MyFlag, flag2 As MyFlag) As Long
        Return flag1.Value ^ flag2.Value
    End Operator

    Public Shared Operator Or(flag1 As MyFlag, flag2 As MyFlag) As MyFlag
        Return New MyFlag(flag1.Value Or flag2.Value)
    End Operator

    Public Shared Operator And(flag1 As MyFlag, flag2 As MyFlag) As MyFlag
        Return New MyFlag(flag1.Value And flag2.Value)
    End Operator

    Public Shared Operator Xor(flag1 As MyFlag, flag2 As MyFlag) As MyFlag
        Return New MyFlag(flag1.Value Xor flag2.Value)
    End Operator

    Public Shared Operator =(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value = flag2.Value
    End Operator

    Public Shared Operator <>(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value <> flag2.Value
    End Operator

    Public Shared Operator >(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value > flag2.Value
    End Operator

    Public Shared Operator <(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value < flag2.Value
    End Operator

    Public Shared Operator >=(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value >= flag2.Value
    End Operator

    Public Shared Operator <=(flag1 As MyFlag, flag2 As MyFlag) As Boolean
        Return flag1.Value <= flag2.Value
    End Operator

@pricerc
Copy link

pricerc commented Oct 31, 2020

@VBAndCs

how many times are you going to write that?

and how long does it take you?

compared with how many classes you write, and how much time you spend developing your business application.

I've said similar elsewhere - the compiler should make:

90% easy
9% not hard
1% achievable, if hard

those % are based on how often a feature will be getting used by a programmer.

Your collection of operators for the 'MyFlag' class is about 10 minutes work. If you only have to do that once during a 100 hour project, then it falls into the 1% bracket, which the compiler should make achievable, which it does.

As far as I'm concerned, overloading operators is a very 'niche' thing to do. In 18 years of coding in .NET, I can count on one hand the number of times I've bothered to do it, and that was mostly out of academic interest, because it isn't actually necessary - there's nothing you can do with an operator that you can't already do with methods.

@pricerc
Copy link

pricerc commented Oct 31, 2020

Since we're talking about Flags Enums, which are conceptually arrays of Boolean values, can you explain the point of the following non-Boolean operators when dealing with a Flags Enum? As far as I'm concerned, they make no sense:

  • + (Add)
  • ' (Subtract)
  • * (Multiply)
  • / (Divide)
  • ^ (Raise to a power)
  • > (Is greater than)
  • < (Is less than)
  • >= (Is greater than or equal to)
  • <= (Is less than or equal to)

@VBAndCs
Copy link

VBAndCs commented Oct 31, 2020

@pricerc
I am covering my bases. This is an auto generated, for a general purpose use, so, someone can decide for some reason to use the Flag as an Integer and do some operation. These operators returns Integer , so, they doesn't alter the Flag logic.
In fact the utility needs to offer some options to allow the user to check the methods and Operator categories he needs. Bit-wise operators are a must, but comparison maybe less important (not that bigger value means that more flags are set), and finally the arithmetic operators which is the useful.
I need also to allow the user to change methods names.
But all this needs time that I don't have currently.
Note that I changed the underline Type from Integer to UInteger, to prevent negative values, and have a wider range.
This is the optimun form of the generated flag I have so far:

Class MyFlag
    Public Shared ReadOnly X As New MyFlag("X", 1)
    Public Shared ReadOnly Y As New MyFlag("Y", 2)
    Public Shared ReadOnly Z As New MyFlag("Z", 4)

    Const MaxValue As UInteger = 7
    Public Shared ReadOnly None As New MyFlag("None", 0)
    Public Shared ReadOnly All As New MyFlag("All", MaxValue)

    Private ReadOnly Value As UInteger

    Private Sub New(value As UInteger)
        Me.Value = If(value > MaxValue, value And MaxValue, value)
    End Sub

    Private Sub New(name As String, value As UInteger)
        _Name = name
        Me.Value = If(value > MaxValue, value And MaxValue, value)
    End Sub

    Public Shared ReadOnly Property Flags As MyFlag() = {X, Y, Z}
    Public Shared ReadOnly Property FlagNames As String() = {"X", "Y", "Z"}
    Public Shared ReadOnly Property FlagValues As UInteger() = {1, 2, 4}
    Public ReadOnly Property Name As String

    Public Property OnFlags As List(Of MyFlag)
        Get
            Dim lstFlags As New List(Of MyFlag)
            For Each flag In Flags
                If (Value And flag.Value) > 0 Then lstFlags.Add(flag)
            Next
            Return lstFlags
        End Get
        Set()
            SetFlags(Value.ToArray())
        End Set
    End Property

    Public Property OffFlags As List(Of MyFlag)
        Get
            Dim lstFlags As New List(Of MyFlag)
            For Each flag In Flags
                If (Value And flag.Value) = 0 Then lstFlags.Add(flag)
            Next
            Return lstFlags
        End Get
        Set()
            UnsetFlags(Value.ToArray)
        End Set
    End Property

    Default Public ReadOnly Property IsSet(flag As MyFlag) As Boolean
        Get
            Return (Value And flag.Value) > 0
        End Get
    End Property

    Public Overrides Function ToString() As String
        Return ToString("+")
    End Function

    Public Overloads Function ToString(Separator As String) As String
        If Value = 0 Then Return None.Name
        If Value = MaxValue Then Return All.Name
        Dim sb As New Text.StringBuilder
        For Each flag In Flags
            If (Value And flag.Value) > 0 Then
                If sb.Length > 0 Then sb.Append(Separator)
                sb.Append(flag.Name)
            End If
        Next
        Return sb.ToString
    End Function

    Public Function ToInteger() As UInteger
        Return Value
    End Function

    Public Function SetFlag(flag As MyFlag) As MyFlag
        Return New MyFlag(Value Or flag.Value)
    End Function

    Public Function SetFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return New MyFlag(Value)

        Dim v = Value
        For Each flag In flags
            v = v Or flag.Value
        Next
        Return New MyFlag(v)
    End Function

    Public Function SetAllExcxept(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return New MyFlag(Value)

        Dim v = MaxValue
        For Each flag In flags
            v -= flag.Value
        Next
        Return New MyFlag(v)
    End Function

    Public Function UnsetFlag(flag As MyFlag) As MyFlag
        Return New MyFlag(Value And Not flag.Value)
    End Function

    Public Function UnsetFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return New MyFlag(Value)

        Dim v = Value
        For Each flag In flags
            v = v And Not flag.Value
        Next
        Return New MyFlag(v)
    End Function

    Public Function UnsetAllExcxept(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return New MyFlag(Value)

        Dim v As UInteger = 0
        For Each flag In flags
            v += flag.Value
        Next
        Return New MyFlag(v)
    End Function

    Public Function ToggleFlag(flag As MyFlag) As MyFlag
        Return New MyFlag(Value Xor flag.Value)
    End Function

    Public Function ToggleFlags(ParamArray flags() As MyFlag) As MyFlag
        If flags Is Nothing OrElse flags.Length = 0 Then Return New MyFlag(Value)

        Dim v = Value
        For Each flag In flags
            v = v Xor flag.Value
        Next
        Return New MyFlag(v)
    End Function

    Public Function ToggleAllFlags() As MyFlag
        Return New MyFlag(Value Xor 7)
    End Function

    Public Function AreAllSet(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value = 7

        For Each flag In flags
            If (Value And flag.Value) = 0 Then Return False
        Next
        Return True
    End Function

    Public Function AreAllUnset(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value = 0

        For Each flag In flags
            If (Value And flag.Value) <> 0 Then Return False
        Next
        Return True
    End Function

    Public Function AreAnySet(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value > 0

        For Each flag In flags
            If (Value And flag.Value) <> 0 Then Return True
        Next
        Return False
    End Function

    Public Function AreAnyUnset(ParamArray flags() As MyFlag) As Boolean
        If flags Is Nothing OrElse flags.Length = 0 Then Return Value < 7

        For Each flag In flags
            If (Value And flag.Value) = 0 Then Return True
        Next
        Return False
    End Function

#Region "Operators"
    ' Contains all arithmetic and logical operators to work on Flags and Integers
    ' No need to publish them all here
#End Region
End Class

@rskar-git
Copy link

I am covering my bases. This is an auto generated, for a general purpose use, so, someone can decide for some reason to use the Flag as an Integer and do some operation.

Lovely. All of that to encourage the worst of "leaky abstractions".

https://en.wikipedia.org/wiki/Leaky_abstraction

https://stackoverflow.com/questions/3883006/meaning-of-leaky-abstraction

https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/

An Enum is simply a peculiar way to organize the namespace of presumably related integer constants. The <Flag> attribute changes the output of .ToString() and the functioning of .Parse/.TryParse. It also - to a point - communicates to humans the bit-field interpretation upon these specially named integer constants.

Even so, the main abstraction here is that of a bit-field. The fact that an integer is used as its basis doesn't mean we should make it too implicitly so.

@VBAndCs
Copy link

VBAndCs commented Oct 31, 2020

I changed the + to set the sent flag, and - to clear it. Thee flag can be Flag object or a number.
I also dropped the rest of the arithmetic operators, but kept the comparison ones, as it has a meaning regarding the flag as I described. So, the operator code size is not too much less. In fact if I used C#, it can drop significantly, as it is more cleaver in using operators than VB. This seems one of the reasons that made programmers leave VB to C#.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants