Advanced Basics

Revisiting Operator Overloading

Ken Getz

Code download available at: AdvancedBasics0509.exe (369 KB)
Browse the Code Online

Contents

Introducing the RGB and HSV Classes
Conversion Operators
Overloading Comparison Operators
Performing Mathematical Operations

On the conference circuit recently, I was speaking about some of my favorite new features in the Microsoft® .NET Framework 2.0, using the content of three recent columns in this series as fodder. In my talk I sped through generics, operator overloading, and the BackgroundWorker component, all in the space of an hour (for the columns, see Advanced Basics: Being Generic Ain't So Bad, Advanced Basics: Calling All Operators, and Advanced Basics: Doing Async the Easy Way). This accomplishment requires speed talking, but over the years I've mastered that skill. Of course, a downloadable"fast conference speaker" codec would help listeners keep up, but alas, that may never happen.

After the session was over, one attendee came up to chat. He suggested that my example for operator overloading was a bit too academic. It showed how to create a class named Vector, which contained a one-dimensional array of integers. In that example, you can overload the +, =, <>, and CType operators in order to manipulate the contents of the vector instances. He then suggested an example that is also a bit esoteric, but it presented a perfect opportunity to revisit operator overloading: using the RGBA and HSV standards for handling colors.

.NET provides the System.Drawing.Color structure for dealing with colors. It's a wrapper around standard RGBA values. An RGBA value is a simple quadruple containing the color values representing the red, green, and blue components of a color, as well as an alpha value representing transparency. Although RGBA is convenient for developers, it's terrible for designers, so there's an alternative technique for describing colors, the HSL (slightly modified as HSV) standard, which stores hue, saturation, and brightness values instead. (A few years back, I delved into these concepts and you can read the results at GDI+: A Primer on Building a Color Picker User Control with GDI+ in Visual Basic .NET or C#.) The earlier article compared the RGB and HSV standards, and understanding their differences will help you here. Also, if you haven't investigated overloading operators in Visual Basic® 2005, please take a moment to review the previously mentioned column on the topic, so that you can understand the basics.

Figure 1 The Sample Form

Figure 1** The Sample Form  **

The sample application for this column includes a simple form, as shown in Figure 1. This form includes the same features as in my previous GDI article, and adds several new demonstrations that allow you to interact with the new overloaded operators.

Introducing the RGB and HSV Classes

The original GDI article's example included RGB and HSV structures, but here I've promoted them to classes. The RGB class contains three integer values (one each for the red, green, and blue components of the color), and each can take on values between 0 and 255. The HSV class contains three integer values (one each for hue, saturation, and value) and although the scale of these values is arbitrary, I chose to use values between 0 and 255 for consistency with the RGB values. Each class contains a shared method that converts an instance of the type into the other color representation. That is, the RGB class contains a ToHSV method, and the HSV class contains a ToRGB method. These methods (which aren't directly pertinent to this discussion) perform the mathematical conversion from one format to the other. You can investigate these methods in the sample code (both conversions are somewhat complex). The HSV class contains the simple declarations shown in Figure 2.

Figure 2 Portion of the HSV Class

Public H As Integer
Public S As Integer
Public V As Integer

Public Sub New(ByVal H As Integer, ByVal S As Integer, ByVal V As Integer)
    Me.H = H
    Me.S = S
    Me.V = V
End Sub

Public Overrides Function ToString() As String
    Return String.Format("({0}, {1}, {2})", H, S, V)
End Function

Because this class doesn't need to perform any operations on its component values, it seems reasonable for this example and for simplicity to expose them as public integer fields, even though public fields are typically frowned upon. The RGB class provides similar functionality, but in a slightly more complex manner, as you'll see later. The public constructors and ToString method, however, work as you might expect (see Figure 3).

Figure 3 Portion of the RGB Class

Public Sub New(ByVal R As Integer, ByVal G As Integer, ByVal B As Integer)
    Me.R = R
    Me.G = G
    Me.B = B
End Sub

Public Sub New(ByVal value As Color)
    R = value.R
    G = value.G
    B = value.B
End Sub

Public Overrides Function ToString() As String
    Return String.Format("({0}, {1}, {2})", R, G, B)
End Function

There's a point to this added complexity—once it's possible to add two RGB values together, or to add an integer to an RGB value (and both these operations are possible, once you've overloaded the + operator for use with this class), you may end up with values in the R, G, and/or B components that are outside the possible range of 0 to 255. For example, you may want to add 255 to a color to force it to be white (255, 255, 255). On the other hand, you may later want to be able to subtract 255 and retrieve the original color. By storing the actual R, G, and B values in private variables, and only converting these values within the 0 to 255 range when necessary for public consumption, it's possible to play tricks with the RGB contents. You'll see an example later.

Note also that the RGB class contains an overloaded constructor that allows you to pass in a System.Drawing.Color value. This addition makes perfect sense, because the RGB class is merely a wrapper around the three components that make up the System.Drawing.Color value. Creating this added constructor makes it easy to write code like this:

Dim myRGB as New RGB(colorLabel.BackColor)

This code copies the R, G, and B values from the System.Drawing.Color into the new RGB instance.

What might you want to do with these classes that would require operator overloading? If you investigate the previous GDI article, you'll find a lot of code that uses that article's ColorHandler class, which contains the HSV and RGB structures as well as methods to convert between the two. All conversions had to be performed calling methods of the ColorHandler class, like the code snippets in Figure 4, which compare the old and the new conversions.

Figure 4 Old and New Conversions

hsv = ColorHandler.RGBtoHSV(rgb)
' becomes this:
hsv = CType(rgb, HSV)

SetRGB(ColorHandler.HSVtoRGB(hsv))
' becomes this:
SetRGB(CType(hsv, RGB))

selectedColor = ColorHandler.HSVtoColor(hsv)
' becomes this, using explicit conversion:
selectedColor = CType(hsv, Color)

fullColor = ColorHandler.HSVtoColor(hsv.H, hsv.S, 255)
' becomes this, using explicit conversion:
fullColor = CType(New HSV(hsv.H, hsv.S, 255), Color)

In addition, using operator overloading, you can easily add new features that weren't needed in the original application, including:

  • Overloading the = and <> (equal and not equal) operators, allowing you to compare HSV, RGB, and System.Drawing.Color values.
  • Overloading the + and – (addition and subtraction) operators for the RGB class, allowing you to add two RGB values together or add a single integer to an RGB instance.
  • Overloading the / (division) operator, allowing you to divide all the components of an RGB instance by a particular integer—this allows you to average two RGB colors, for example.

The remainder of this column discusses the code that is required to provide these overloaded operators, using sample code from the downloadable application in order to demonstrate how you might use the new operators.

Conversion Operators

You can overload the CType operator, creating either a widening (implicit) or narrowing (explicit) conversion operator. Normally, you use a widening conversion if you're guaranteed that the conversion cannot lose data, and a narrowing conversion if it's possible that data loss might occur. You can call the widening conversion operator without explicitly including the call to CType, but for the narrowing conversion, you must explicitly call CType.

In this case, because converting between RGB and System.Drawing.Color values maintains the same R, G, and B values, it made sense to use widening conversions for these, so that you can perform the conversions implicitly in code. (One could argue against this case, because the System.Drawing.Color structure maintains four components of each color—A, R, G, and B. Therefore, the conversion from System.Drawing.Color to RGB is technically a narrowing conversion, but I'll let that slide.) On the other hand, conversions between RGB and HSV, and similarly between HSV and System.Drawing.Color, could lose some precision during the conversion, and are therefore included as narrowing conversions (requiring an explicit call conversion using CType).

In the RGB class, the conversion operators convert to and from RGB and System.Drawing.Color, and from RGB to HSV (the corresponding HSV to RGB conversion could have been in this class, but it appears in the HSV class in the example in Figure 5).

Figure 5 RGB Conversion Operators

' Allow implicit conversion from RGB to Color.
Public Overloads Shared Widening Operator _
        CType(ByVal value As RGB) As Drawing.Color
    If value Is Nothing Then
        Return Nothing
    Else
        Return Color.FromArgb(value.R, value.G, value.B)
    End If
End Operator

' Require explicit conversion (using CType) from RGB to HSV.
Public Overloads Shared Narrowing Operator _
        CType(ByVal value As RGB) As HSV
    If value Is Nothing Then 
        Return Nothing
    Else
        Return ToHSV(value)
    End If
End Operator

' Allow implicit conversion from color to RGB.
Public Overloads Shared Widening Operator _
        CType(ByVal value As Color) As RGB
    Return New RGB(value)
End Operator

Using these operators, you can convert to and from RGB, as shown in the following lines of code:

' Explicit conversion from RGB to HSV:
Dim myHSV As HSV = CType(myRGB, HSV)

' Implicit conversion from RGB to Color:
myLabel.BackColor = myRGB

Note that when converting from HSV to Color, it's easiest to convert the HSV value to RGB first, and from there to Color. The HSV class contains similar overloaded conversion operators, converting to and from System.Drawing.Color, and to RGB, as shown in the code in Figure 6.

Figure 6 HSV Conversion Operators

' Require explicit conversion from HSV to Color. Take advantage of 
' implicit conversion from RGB to Color.
Public Overloads Shared Narrowing Operator CType( _
         ByVal value As HSV) As Color
    If value Is Nothing Then
        Return Nothing
    Else
        Return ToRGB(value)
    End If
End Operator

' Require explicit conversion from Color to HSV.
Public Overloads Shared Widening Operator CType(ByVal value As Color) As HSV
    Dim rgb As New RGB(value)
    Return CType(rgb, HSV)
End Operator

' Require explicit conversion (using CType) from HSV to RGB.
Public Overloads Shared Narrowing Operator CType(ByVal value As HSV) As RGB
    If value Is Nothing Then
        Return Nothing
    Else
        Return ToRGB(value)
  End If
End Operator

Overloading Comparison Operators

Because RGB, HSV, and System.Drawing.Color instances all contain essentially the same information, it makes sense to overload the = and <> operators, so you can easily compare two values, no matter how they're stored. The Visual Basic compiler will complain if you overload = without overloading <>, so you'll need to provide overloads for both of these. Although the compiler doesn't warn you if you don't, it just makes sense to also overload the Equals method of each class, as well as GetHashCode.

There's one small difficulty in comparing RGB and HSV values—although the RGB format stores black as 0, 0, 0 (that is, the absence of any red, green, or blue component), HSV can store black in a large number of different ways. In the HSV format, if the value component is 0, it doesn't matter what the hue or saturation components contain—the represented color is black. The simplest solution is to convert the HSV value to RGB format, and then compare two RGB values, and that's what the sample code does.

When overloading the = operator, you'll need to take into account some logic for handling null values. For example, the procedure declaration for overloading the = operator in the RGB class, comparing to RGB instances, looks like this:

Public Overloads Shared Operator =( _
    ByVal value1 As RGB, ByVal value2 As RGB) As Boolean

End Operator

The two values passed to this method might both be null, they might both be non-null, or one or the other might be null. What does it mean to compare two null RGB values, or to compare one null value with an RGB instance? (The same issues apply when working with HSV values.) Although you could decide that any comparison that involves a null reference returns False, I decided to handle colors as if they were value types, much like the String class. In other words, if two colors are both null, the = operator returns True. If one is null and the other isn't, the operator returns False. Otherwise, the operator compares the colors and returns the result. The code in Figure 7 shows the results from the RGB class.

Figure 7 Comparing RGB Values

' Compare two RGB instances, either of which might be Nothing.
Public Overloads Shared Operator =( _
        ByVal value1 As RGB, ByVal value2 As RGB) As Boolean
    If value1 Is Nothing And value2 Is Nothing Then
        ' If both values are Nothing, return True—
        ' the colors are the same.
        Return True
    ElseIf value1 Is Nothing Then
        ' They're not both Nothing, but Value1 is Nothing.
        ' They can't be equal.
        Return False
    Else
        ' One or the other of the colors might be Nothing, 
        ' otherwise compare their values.
        Return value1.Equals(value2)
    End If
End Operator

' Compare an RGB and a Color. 
Public Overloads Shared Operator =( _
        ByVal value1 As RGB, ByVal value2 As Color) As Boolean
    If value1 Is Nothing Then
        ' A Color instance can't be null, so if the RGB 
        ' is Nothing, return False.
        Return False
    Else
        Return value1.Equals(value2)
    End If
End Operator

' Compare an RGB and an HSV, either of which can be null.
Public Overloads Shared Operator =( _
        ByVal value1 As RGB, ByVal value2 As HSV) As Boolean
    If value1 Is Nothing And value2 Is Nothing Then
        Return True
    ElseIf value1 Is Nothing Then
        Return False
    Else
        Return value1.Equals(value2)
    End If
End Operator

Public Overloads Shared Operator <>( _
        ByVal value1 As RGB, ByVal value2 As Color) As Boolean
    Return Not (value1 = value2)
End Operator

Public Overloads Shared Operator <>( _
       ByVal value1 As RGB, ByVal value2 As RGB) As Boolean
  Return Not (value1 = value2)
End Operator

Public Overloads Shared Operator <>( _
        ByVal value1 As RGB, ByVal value2 As HSV) As Boolean
    Return Not (value1 = value2)
End Operator

Note that the overloaded versions of the <> operator simply return the opposite of the corresponding = overloads. This is standard behavior, but it's required by the compiler. The HSV class included the same set of comparison operators, so I'll let you investigate those in the sample project.

The overloaded = operator methods rely on the overloaded Equals method, which comes in three varieties in the RGB class (see Figure 8). If you overload the Equals method, you should also overload the GetHashCode method. The sample includes a simple implementation of this method in both the RGB and HSV classes—in each case, the code simply returns the hash code of the corresponding System.Drawing.Color value.

Figure 8 RGB Equals Overloads

Public Overloads Function Equals(ByVal value As RGB) As Boolean
    If value Is Nothing Then
        ' If value is Nothing, the comparison must fail—the current
        ' instance cannot be null, so the two can't be equal.
        Return False
    Else
        Return (Me.R = value.R AndAlso _
            Me.G = value.G AndAlso Me.B = value.B)
    End If
End Function

Public Overloads Function Equals(ByVal value As Color) As Boolean
    ' The current instance can't be Nothing, and the Color
    ' can't be Nothing, so there's no need to check for
    ' null references. Use the Equals(RGB) overload, taking advantage
    ' of the implicit conversion from Color to RGB:
    Return Me.Equals(New RGB(value))
End Function

Public Overloads Function Equals(ByVal value As HSV) As Boolean
    If value Is Nothing Then
        ' If the HSV value is Nothing, then this comparison must fail.
        ' The current instance can't be Nothing.
        Return False
    Else
        Return Me.Equals(New RGB(value))
  End If
End Function

Public Overloads Function GetHashCode() As Integer
    ' Need to provide an arbitrary integer that represents this color. 
    Return CType(Me, System.Drawing.Color).GetHashCode
End Function

The main form in the sample project includes a method named DisplayInfo that displays its parameters in the Listbox on the form:

Private Sub DisplayInfo(ByVal caption As String)
    lstResults.Items.Add(caption)
End Sub

Private Sub DisplayInfo(ByVal caption As String, _
        ByVal value As Object)
    DisplayInfo(String.Format("{0}: {1}", caption, value))
End Sub

Clicking the button labeled "Test Equality Operators" runs the code in Figure 9. Although these examples don't represent every possible combination of types in every possible combination of equality, they do demonstrate that colors that are equivalent compare correctly. (Note that rgb5 and hsv1 represent the same color, and that rgb1 contains the same color as Color.Red.) Without the use of overloaded operators for =, <>, and the overloaded Equals methods, it wouldn't be possible to write simple code like this that compares instances of the RGB and HSV classes.

Figure 9 Testing Equality Operators

Dim rgb1 As New RGB(255, 0, 0)
Dim rgb2 As New RGB(255, 0, 0)
Dim rgb3 As New RGB(0, 255, 255)
Dim rgb4 As RGB
Dim rgb5 As New RGB(255, 109, 126)
Dim hsv1 As New HSV(250, 146, 255)
Dim hsv2 As New HSV(250, 146, 255)

' Display-related code removed here...

DisplayInfo("rgb1.Equals(rgb2)", rgb1.Equals(rgb2))
DisplayInfo("rgb1.Equals(rgb3)", rgb1.Equals(rgb3))
DisplayInfo("rgb1 = rgb2", rgb1 = rgb2)
DisplayInfo("rgb1 = Color.Red", rgb1 = Color.Red)
DisplayInfo("rgb1 = rgb4", rgb1 = rgb4)
DisplayInfo("rgb4 = Color.Red", rgb4 = Color.Red)
DisplayInfo("rgb5 = hsv1", rgb5 = hsv1)
DisplayInfo("hsv1 = hsv2", hsv1 = hsv2)
DisplayInfo("hsv1 = rgb5", hsv1 = rgb5)

Performing Mathematical Operations

Although you won't usually perform mathematical operations on colors, for RGB values it's easy enough to assign reasonable semantics to these operations:

  • Adding or subtracting an integer to an RGB increases or decreases each of the R, G, and B values by the same amount. (This action effectively changes the brightness of the color, so you would get a similar effect by adding or subtracting a value to the V component of an HSV value.)
  • Adding or subtracting two RGB values performs the appropriate operation on each of the properties.
  • Dividing an RGB value by an integer divides each of the members by the integer value.

Of course, you might find uses for other mathematical operations, but as you'll see, it's easy to create your own mathematical operator overloads so feel free to add your own.

As mentioned earlier, because legal RGB component values can only be between 0 and 255, you can get into trouble at the endpoints. For example, if you had an RGB value containing a particular color, and then added 255 to the color (or added Color.White to it), you couldn't simply subtract 255 to get back to the original color—that information would have been lost by adding 255. The sample uses one possible solution: it maintains private Integer values that represent the actual values of the R, G, and B components, but the R, G, and B property Get procedures ensure that the values are within the standard byte range (0 through 255) before returning the values.

For example, the B property procedure looks like this:

Private BValue As Integer

Public Property B() As Integer
    Get
        Return GetValueWithinRange(BValue)
    End Get
    Set(ByVal value As Integer)
        BValue = value
    End Set
End Property

The R and G property procedures work in a similar fashion. The GetValueWithinRange procedure guarantees that no matter what value is stored in the private backing field, the property returns a reasonable value:

Private Function GetValueWithinRange(value As Integer) As Integer
    Return Math.Max(0, Math.Min(255, value))
End Function

The RGB class contains the overloaded operator procedures shown in Figure 10, allowing you to add two RGB values together, or to add an integer to an RGB value. Note that each of these procedures returns Nothing if an RGB parameter is Nothing, and that each of the methods uses the private variables, rather than the public properties, to perform operations.

Figure 10 Addition with RGB Values

' Add two RGB values together.
Public Overloads Shared Operator +( _
        ByVal value1 As RGB, ByVal value2 As RGB) As RGB
    If value1 Is Nothing OrElse value2 Is Nothing Then
        Return Nothing
    Else
        Dim newValue As New RGB( _
        value1.RValue + value2.RValue, _
        value1.GValue + value2.GValue, _
        value1.BValue + value2.BValue)
        Return newValue
    End If
End Operator

' Add a single value to an RGB value.
Public Overloads Shared Operator +( _
        ByVal RGBvalue As RGB, ByVal integerValue As Integer) As RGB
    If RGBvalue Is Nothing Then
        Return Nothing
    Else
        Return New RGB( _
            RGBvalue.RValue + integerValue, _
            RGBvalue.GValue + integerValue, _
            RGBvalue.BValue + integerValue)
    End If
End Operator

The RGB class also provides the overloaded \ (integer division) and / (division) operators shown in Figure 11.

Figure 11 Division with RGB Values

' Divide an RGB value by an integer. The results will always be integers.
Public Overloads Shared Operator \( _
        ByVal value As RGB, ByVal divisor As Integer) As RGB
    If value Is Nothing Then
        Return Nothing
    ElseIf divisor = 0 Then
        Throw New DivideByZeroException()
    Else
        Return New RGB(value.RValue \ divisor, _
            value.GValue \ divisor, _
            value.BValue \ divisor)
    End If
End Operator

' Perform integer division.
Public Overloads Shared Operator /( _
        ByVal value As RGB, ByVal divisor As Integer) As RGB
    Return value \ divisor
End Operator

The sample form provides three demonstrations of these methods. The first, which you can demonstrate by sliding the track bar within the Add/Subtract Selected Color group box, allows you to add and subtract integers to an existing RGB value. When you click Select Color, the code allows you to select a color, and stores it into both the BackColor and ForeColor properties of the label. The button's click Event handler calls the procedure in Figure 12.

Figure 12 SetLabelColor

Private Sub SetLabelColor(ByVal lbl As Label)
    Using frm As New ColorChooser2()
        frm.Color = lbl.BackColor
        If frm.ShowDialog(Me) = DialogResult.OK Then
            SetColor(lbl, frm.Color)
        End If
    End Using
End Sub

Private Sub SetColor(ByVal lbl As Label, ByVal clr As Color)
    lbl.BackColor = clr
    ' Store the color, so you can get it back later. A better solution 
    ' would be to inherit from the Label control and add an OriginalColor
    ' property, but that's overkill for this demonstration.
    lbl.ForeColor = clr
End Sub

The track bar's Scroll event handler calls the following code, which simply adds the value of the trackbar, which contains values between -255 and +255, to the selected color:

Dim value As New RGB(colorLabelAddSubtract.ForeColor)

' Add in the value of the trackbar:
value += CInt(TrackBar1.Value)

' Set the label color and display the text:
colorLabelAddSubtract.BackColor = value
colorStringLabelAddSubtract.Text = value.ToString()

Clicking the button in the Blend Two Colors group box performs an even simpler operation.It averages the two colors you've selected, using the following code:

Dim rgb1 As New RGB(colorLabel1.BackColor)
Dim rgb2 As New RGB(colorLabel2.BackColor)
colorLabel3.BackColor = (rgb1 + rgb2) / 2

Try selecting some different pairs of colors to see how they average out. Red and blue average to violet; red and yellow average to orange, just as you might expect from your grade school experimentation with finger paints.

The final demonstration, in the group box labeled "Make a Color Black/White", shows what happens when you add 255 to a color and then subtract it back; or subtract 255 from a color and then add it back. Because the RGB class maintains the real values of its components rather than truncating them internally, it's possible to perform tricks like this. For the color red, the original R value is 255. Adding 255 to this value results in the private RValue member containing 510, but retrieving the R property returns only 255, as it should. Subtract 255 and the value returns to 255, as it should (rather than dropping to 0, as it would if the internal value matched the external value). The code isn't terribly interesting—it simply stores the selected color into a class-level RGB instance, and adds and subtracts 255 on demand. The interesting part is that the code correctly maintains the "out-of-bounds" values, so that you can undo changes that pushed the values out of bounds.

I'll end with a suggestion—don't use operator overloading as a solution in search of a problem. Make sure you're making the code simpler and easier to work with. I removed a lot of code from the original color chooser application, and I'm convinced that adding overloaded operators makes this sample application work better, and provides some new functionality that would otherwise have been more difficult to write.

Send your questions and comments for Ken to  basics@microsoft.com.

Ken Getz is a senior consultant with MCW Technologies, and a courseware author for AppDev. He is coauthor of ASP .NET Developers Jumpstart (Addison-Wesley, 2002), Access Developer's Handbook (Sybex, 2002), and VBA Developer's Handbook, 2nd Edition (Sybex, 2001). Reach him at keng@mcwtech.com.