Operator Overloading in Visual Basic 2005
Updated October 2005
Summary: Operator Overloading, new to Visual Basic 2005, simplifies the use and development of complex types by allowing you to specify your own implementation for standard operations such as addition and subtraction. (15 printed pages)
Operator overloading is a powerful tool that allows you to redefine many operators (addition, subtraction, and so on) within classes and structures, making them more useful and relevant to the class or structure in question. With the release of Microsoft Visual Studio 2005, you can now create and use overloaded operators in your code. The use of operator overloading allows for much cleaner, readable code, and additionally makes it much easier to interact with some types in the .NET Framework that already expose overloaded operators (for example, System.Data.SqlTypes or DateTime). This article discusses the scenarios in which operating overloading can be a very useful tool for you when you write applications using Visual Basic. It shows examples of how you can introduce operator overloading into your classes and structures in such a way as to make those objects act in statements as if they were intrinsic types (such as String, Integer, and so on).
Operator overloading is the ability for you to define procedures for a set of operators on a given type. This allows you to write more intuitive and more readable code. The operators that can be defined on your class or structure are as follows:
- Unary operators:
+ - Not IsTrue IsFalse CType
- Binary operators:
+ - * / \ & Like Mod And Or Xor ^ << >> = <> > < >= <=
There are some caveats. First, only the operators listed above can be overloaded. In particular, you can't overload member access, method invocation, or the AndAlso, OrElse, New, TypeOf... Is, Is, IsNot, AddressOf, GetType, and AsType operators. Note that assignment itself (=) is a statement, so you can't overload it either. Secondly, operator overloading cannot be used to change to order in which operators are executed on a given line. For example, multiplication will always happen before addition, unless parentheses are used appropriately.
You don't have to provide overloaded operators on a class or structure if you don't need them, and overloading any particular operator does not usually require you to overload any other operators. It is perfectly acceptable for you to overload only (for example) the addition operator without overloading the subtraction operator. There are, however, some exceptions. You do generally need to overload the logical operators in pairs. These paired cases include = and <>, > and <, >=, and <=, and IsTrue and IsFalse. For each pair, if you define one, you must define the other, so as to properly account for logical negation in expressions.
You declare an operator overload method in a similar fashion to the way you'd declare any other Visual Basic method. However, instead of "Function" or "Sub," you'll be using the "Operator" keyword in the declaration. The name of the method is simply (and always) the operator that is being overloaded. For example, the declaration in Code Block 1 declares an operator overloading for the "\" (integer division operator).
Code Block 1
Public Shared Operator \(P As MyType, Q As MyType) As MyType
Note that overloaded operators are always shared methods that always return a value, though the return value need not be of the same type as the types on which the operation is performed. Another thing to note is that you need to define the operator in such a way that either an argument to it or the return type from it (or both) is the same type as the class that defines it. Your overloaded operators must furthermore use the "Return" keyword to return their results, and they do not support an "Exit" statement.
There are many ways to use overloading operators and samples of them are covered in following sections.
Defining Complex Data Types
Suppose that you're creating a math library that involves the use complex numbers. You might use a class such as that shown in Code Block 2:
' Code Block 2 Class ComplexNumber Public Sub New(ByVal real As Double, ByVal imaginary As Double) x = real y = imaginary End Sub Public Overrides Function ToString() As String Dim s As String If y >= 0 Then s = x & " + " & y & "i" Else s = x & " - " & -1*y & "i" End If Return s End Function Public Property Real() As Double Get Return x End Get Set(ByVal Value As Double) x = Value End Set End Property Public Property Imaginary() As Double Get Return y End Get Set(ByVal Value As Double) y = Value End Set End Property Private x As Double = 0.0 Private y As Double = 0.0 End Class
So, you've got a way to store and retrieve the component parts, and a way to display them. However, there is no real way for you to easily manipulate the data. On the first pass, you might simply add the component parts as in Code Block 3:
' Code Block 3 Dim a As New ComplexNumber(5.2, 3.1) Dim b As New ComplexNumber(-7.6, 0.02) Dim c As New ComplexNumber(a.Real() + b.Real(), a.Imaginary() + b.Imaginary())
And an obvious next step would be simply to create an addition method for the ComplexNumber, such as that given in Code Block 4:
' Code Block 4 Public Function Add(cn As ComplexNumber) As ComplexNumber Dim Result As New ComplexNumber(cn.Real() + Me.Real(), cn.Imaginary() + Me.Imaginary()) Return Result End Function
So far, so good. But let's examine the usage of this function in Code Block 5, as compared to (for example) type "Double" addition:
' Code Block 5 Dim a As New ComplexNumber(5.2, 3.1) Dim b As New ComplexNumber(-7.6, 0.02) Dim c As ComplexNumber = a.Add( b) Dim i As Double = 2.0 Dim j As Double = 3.0 Dim k As Double = i + j
Clearly, the lines of code involving double addition are much clearer to read, easier to type, and overall more intuitive than the lines involving ComplexNumber addition. Unfortunately, in the absence of operator overloading—the ability to use operators such as "+" that work "correctly"—there is no good way for you to make this code cleaner. If you try to do the intuitive thing to add two complex numbers using the "+" sign, a syntax error will result. Furthermore, as you add more methods (Subtract, Multiply, and so on), your code becomes that much harder to read, consisting as it does of mostly references to the ComplexNumber class itself.
In Visual Basic 2005, however, you can overcome this problem by rewriting the Add routine as follows (Code Block 6):
' Code Block 6 Public Shared Operator +(cn1 As ComplexNumber, _ cn2 As ComplexNumber) As ComplexNumber Dim Result As New ComplexNumber( _ cn1.Real() + cn2.Real(), _ cn1.Imaginary() + cn2.Imaginary()) Return Result End Operator
Note the differences—we've replaced the keyword "function" with the new keyword "operator," and the name "Add" has been replaced with the symbol "+". Furthermore, we've specified that this is now a shared function, and so, since there's no longer any specific "me" instance involved, we've rewritten the method signature to take the other operand and have renamed those argument appropriately. These small changes, however, now allow you to rewrite the addition on the third line of Code Block 5 as follows (Code Block 7):
' Code Block 7 Dim c As ComplexNumber = a + b
This is much easier to read, and as you add more overloaded operator functions, your code still remains clear. Essentially, you can treat your defined types as any other intrinsic type if the proper operators are set up. It's another case of "fix it and forget it." You define common operations in one place, and then can pretty much take them for granted from that point on. Furthermore, since this is now a "shared" method, you'll even gain a small performance boost at runtime.
Operations between different types are also possible; for example, you might define the following code (Code Block 8) to allow an Integer to be added to a ComplexNumber:
' Code Block 8 Public Shared Operator +(cn1 As ComplexNumber, _ n2 As Integer) As ComplexNumber Dim Result As New ComplexNumber( _ cn1.Real() + CDbl(n2), cn1.Imaginary) Return Result End Operator
Note that, since the unary CType() operator can also be overloaded, you could make even the initial assignments to "a" and "b" in Code Block 5 cleaner in this example, with (for example) a cast from the strings "5.2 + 3.1i" and "-7.6 + 0.02". Doing this would involve parsing and validating the strings in the CType() operator and returning a new ComplexType with the appropriate values filled in. That exercise is beyond the scope of this document, however.
Using Complex Data Types
An additional problem that you may have encountered in the past, prior to the addition of operator overloading in Visual Basic 2005, is that there are data types out there already that not only expose overloaded operators, but require their use as well. For example, suppose you're coding against a SQL database, using the classes defined in System.Data.SQLTypes, so that values can be null (as opposed to defining arbitrary values like "1/1/1900" or "-1" as indicators to indicate that a given field does not have applicable data). You might write code to access a row in the database as given in Code Block 9:
' Code Block 9 Imports System.Data Imports System.Data.SQLTypes . . . Public Sub ProcessServiceListAddress(ByVal currentRow As Integer) Dim FirstName, LastName As SQLString Dim AddressLine1, AddressLine2 As SQLString Dim City, State, ZipCode As SQLString Dim ds As Dataset . . . AddressLine2 = ds("ServiceList").Rows(currentRow).Item("AddressLine2") . . . End Sub
However, not all rows will have the "AddressLine2" item filled in (it being generally an optional field), and when the code is run Visual Basic 2003, it will throw a runtime error ("Invalid Cast Exception") whenever it encounters such a row. Adding an explicit cast to SQLString will not help, either. This is because the casting operator is overloaded in the System.Data.SQLTypes to support returning the appropriate type regardless of whether or not the data is null, but Visual Basic 2003 does not support operator overloading and thus tries to retrieve a type which it is not expecting. Therefore, in Visual Basic 2003, the only option is to either use default indicators in all of the items (for example, require the database to have all items filled in, using something like "<not applicable>" as a value which should then be ignored in subsequent logic), or else implement logic to check for the problem beforehand (using code such as that given in Code Block 10):
' Code Block 10 If (ds("ServiceList").Rows(currentRow).IsNull("AddressLine2") ' No data -- do something Else AddressLine2 = ds("ServiceList").Rows(currentRow).Item("AddressLine2") End If
This is clumsy, however, and if there is many potentially null items in a row, the code becomes much longer and more complicated than need be. (Visual Basic 2005 fully supports operator overloading, however, and so such clumsy mechanisms are no longer required to work around the problem; the intuitive code in Code Block 8 will run fine.)
There are several issues that you need to keep in mind when using operator overloading, in order to avoid unanticipated behavior.
If the operand(s) of an overloaded operator do not match what is defined explicitly by the operator's declaration, the standard overload resolution rules apply in determining which overload to use, as well as how the result is applied. Consider the following example in Code Block 11:
' Code Block 11 Sub Main() Dim Q As New B Dim P As B = -Q ' This will fail End Sub Class A Public Shared Operator -(value As A) As A . . . End Operator End Class Class B Inherits A End Class
Since no unary negation operator is defined for class B, the unary negation operator for calls A (from which B derives) will be used as would be expected from a normal overloaded function. However, after that code is run, the assignment will return a value of type A, and then attempt to assign it to a reference to the derived type B, so the assignment will fail with an "Invalid cast" runtime error.
Narrowing and Widening
One important thing to note is that any overload of CType that you create must be defined as Narrowing or Widening. In general, Widening should be used when there is no possibility of error in the cast, whereas Narrowing should be used whenever there is a possibility of the cast failing.
Let's consider two structures—structure Position holds a two-degree/direction pairs (one for latitude, one for longitude), whereas structure GPSCoordinate contains not only a Position object, but an Elevation value as well. It's clear that GPSCoordinate can hold any information that Position can, but not the reverse. You'd lose the elevation information. Therefore, we can describe the relation from Position to GPSCoordinate as Widening, and that from GPSCoordinate to Position as Narrowing. When casting between the two, we can define these relationships in the overloaded operators so that they can be enforced correctly, as given in Code Block 12:
' Code Block 12 Option Strict On Module Module1 Sub Main() Dim pos As New Position(47.5, CompassPoint.North, _ 180.2, CompassPoint.West) Dim gps As GPSCoordinate gps = pos ' This is OK -- widening conversion gps.m_Elevation = 5.5 pos = gps ' Uh, oh -- narrowing conversion! Error when option strict is on. End Sub Enum CompassPoint North = 0 East = 0 South = 1 West = 1 End Enum Structure DegreeLine Public Property Degrees() As Double Get Return m_Degrees End Get Set(ByVal value As Double) m_Degrees = value End Set End Property Public Property Direction() As CompassPoint Get Return m_Direction End Get Set(ByVal value As CompassPoint) m_Direction = value End Set End Property Private m_Degrees As Double Private m_Direction As CompassPoint End Structure Structure Position Public Sub New(ByVal lat As DegreeLine, _ ByVal lon As DegreeLine) Latitude = lat Longitude = lon End Sub Public Sub New(ByVal latdeg As Double, _ ByVal latdir As CompassPoint, _ ByVal londeg As Double, _ ByVal londir As CompassPoint) m_Latitude.Degrees = latdeg m_Latitude.Direction = latdir m_Longitude.Degrees = londeg m_Longitude.Direction = londir End Sub Public Property Latitude() As DegreeLine Get Return m_Latitude End Get Set(ByVal value As DegreeLine) m_Latitude.Degrees = value.Degrees m_Latitude.Direction = value.Direction End Set End Property Public Property Longitude() As DegreeLine Get Return m_Longitude End Get Set(ByVal value As DegreeLine) m_Longitude.Degrees = value.Degrees m_Longitude.Direction = value.Direction End Set End Property Private m_Latitude As DegreeLine Private m_Longitude As DegreeLine End Structure Structure GPSCoordinate Public Sub New(ByVal positionValue As Position, _ ByVal elevationValue As Double) m_Elevation = elevationValue m_Position.Latitude = positionValue.Latitude m_Position.Longitude = positionValue.Longitude End Sub Public Overloads Shared _ Narrowing Operator CType( _ ByVal value As GPSCoordinate) As Position If value.m_Elevation <> 0.0 Then Throw New ArgumentException() _ ' Elevation information would be lost! End If Return New Position(value.m_Position.Latitude, value.m_Position.Longitude) End Operator Public Overloads Shared _ Widening Operator CType( _ ByVal value As Position) As GPSCoordinate Return New GPSCoordinate(value, 0.0) End Operator Public m_Position As Position Public m_Elevation As Double End Structure End Module
Casting from GPSCoordinate to Position will throw an exception at runtime if the elevation portion of the GPSCoordinate is non-zero. To make matters worse, when Option Strict is turned on (which is a recommended practice), the line of code that attempts the cast will be in error, due to the "Narrowing" qualifier in the relevant cast operator, unless CType() is explicitly used. Assigning a Position type to a GPSCoordinate type, on the other hand, will always work regardless of whether or not Option Strict is on, since the cast in that direction is defined as Widening. In this case, we just define the missing elevation to be zero.
Note that, in this case, we've defined the overloaded operators on the GPSCoordinate structure. We could just as easily have defined them on the Position structure. As long as Position was used in the overloaded operators either as a return value or an argument, this would be perfectly legal. However, the Position structure does not otherwise depend on the GPSCoordinate structure, whereas, even without the overloaded operators, GPSCoordinate depends on the Position structure. Therefore, it makes the most sense to have the operators defined in the GPSCoordinate structure. Otherwise, if we decided to move just the position structure to a library, we'd be in the awkward position of the Position code and the GPSCoordinate code having to reference each other, which is not a good coding practice and defeats the whole purpose of having a library.
The following pairs of operators are linked. if you define one of them, the other must be defined as well:
= and <> < and > <= and => IsTrue and IsFalse
Additionally, if the operator "\" (integral division) is defined, but not the operator "/" (normal division), Visual Basic will internally create a wrapper for "/" in the resulting code which simply calls into the integral division operator. This is required for proper cross-language support of operators.
Implicit Operator Overloading
There are certain instances where it is not necessary for you to overload operators, because the language already handles it for you:
- Delegates: It's not necessary to overload "=" and "<>" operators for delegates; these operations are implicit in Visual Basic. Two delegate instances are equal either if they are both Nothing, or if they both refer to precisely the same method(s).
- AndAlso/OrElse: These operators cannot be overloaded because they make use of whatever the user has defined for "And" and "Or." When used on a type that defines such overloading, both the return type and operand type must match, and the IsTrue/IsFalse pair must also be defined so that the Also/Else logic can be executed.
Debugging Overloaded Operators
You can place breakpoints within overloaded operators and debug them as if they were any other procedure. You can also use the operators in the immediate window, as if they were in code, such as in the following case that assumes the existence of the overloaded addition operator listed in Code Block 6:
? a + b 5.4 – 7.6i
Cross-Language Support for Operator Overloading
Other Visual Studio languages built on the .NET platform support operator overloading, so the overloaded operators that you create in your Visual Basic libraries are accessible from Visual C#, for example. Of course, operators are subject to normal accessibility rules, so if you don't want to expose the operators externally, you can always declare them (or the objects that define them) as protected or private, depending on the situation.
Abuse of Operator Overloading
Operator overloading is an extremely cool feature, and I use it myself quite a lot to make my code more readable and easier to type. However, you should be careful not to get carried away when writing operators. It's good practice to define operators in such a way that they behave intuitively, and try to avoid side effects as much as possible. For example, the following code in code block 13 has a number of problems:
' Code Block 13 Module Module1 Sub Main() Dim x As New SomeClass Dim y As Boolean = -x ' What the heck???? What does this mean??? End Sub Class SomeClass Dim member As Long = 2 Public Overloads Shared Operator -(ByVal value As SomeClass) As Boolean ' Shall we use unary negation here for convenience, since we have no other use for it? If value.member Mod 5 = 0 Then ' If divisible by 5, write out a message Console.WriteLine("This is a message.") Return True End If Return False End Operator End Class End Module
Although perfectly legal, this overloaded operator makes absolutely no sense. No one reading your code will expect unary negation of your class to return a Boolean and randomly log information to the console. While I will grant that this example is a bit far-fetched, the point is that the behavior of your overloaded operator should be intuitive, predictable, and consistent to readers not directly familiar with your code. They should be considered "commando" code—they do the important job, do it well, and then immediately return. (The last thing you want, for example, is a performance problem in your operators!) The "addition" operator should do something like normal addition (or perhaps concatenation in the case of strings), the "and" operator should perform appropriate logical operations, and so on. This is especially important if you are creating a library that will be used by other developers (perhaps even using other languages) who may not have access to your source code. Because operating overloading is supposed to work "intuitively," it just might not be appropriate for certain classes or structures. When in doubt, use the following rule of thumb: if your type doesn't generally act like a logical, numeric, or string type, you should think probably twice before using operator overloading.
I also advise that you should not use the CType operator as a replacement for a property. In the GPS example given above in code block 12, the narrowing conversion is coded so as to throw an exception if there is data loss. You might argue that the CType operator should simply return the Position value and not worry about the elevation at all. However, using casting to retrieve a portion of the data contained by a class or structure makes it harder to read and understand your code. The reader can't determine what's returned without inspecting your operator, which sort of defeats the purpose of overloading the operator in the first place. It especially becomes difficult when a type is used more than once in the class. Consider a class you've created which contains three strings, and let's say you want to define a CType() operator from that class to a string. Which string gets returned? The reader has absolutely no way of knowing with looking through your sources (which may or may not be accessible). In such a case, properties are definitely the way to go.
The use of operator overloading gives you an easy way to factor your code in such a way as to make usage of complex types more readable and maintainable. After you define the operators appropriately, their usage is intuitive, both while coding and debugging. Support of operator overloading also allows you to consume other complex data types already available in the .NET Framework. Overall, operator overloading is a great new Visual Basic tool that gives you increased power without sacrificing ease-of-use.
Matthew Gertz is the Developer Lead for the Microsoft Visual Basic Compiler, Editor, and Debugger team. Alan Carter and Amanda Silver, Visual Basic Program Managers, also contributed material to this document.