Operator Overloading and Equality
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is
a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in
the .NET 10 SDK container (Linux, invariant culture); the output shown is real output. The projects use the root namespace
ShelfDocs, which is why a few messages and type names below carry that prefix. The examples that are meant to fail say so
and show the compiler messages that the SDK printed. Where an example compares with C# it was built as a real C# project
that references the Visual Basic one.
A type decides for itself what +, =, < or a conversion means, and how it behaves in a HashSet, a Dictionary or a
sort. Visual Basic gives you two separate mechanisms for that, and most of the confusion about equality comes from mixing
them up:
-
Operator procedures (
Operator =,Operator +,Widening Operator CTypeand so on) change what an expression such asa = bmeans in source code. They are resolved by the compiler from the declared types of the operands. -
The equality and ordering contract (
Object.Equals,GetHashCode,IEquatable(Of T),IComparable(Of T)and comparers) is what the .NET class library calls at run time:List(Of T).Contains,Dictionary(Of K, V),Distinct,SortandSortedSet(Of T)never look at your operators.
A well-behaved type defines both, with the same meaning. The running example is the shelf library catalogue: the Isbn
structure, a few catalogue entries and a Money structure for late fees. The Operator statement and operator
overloading itself have existed since Visual Basic 2005 (VB 8.0, .NET Framework 2.0), together with the Widening and
Narrowing conversion operators, IsTrue/IsFalse and the lifting of operators to nullable value types. What
changed around it since is the class library (IEquatable(Of T), HashCode, generic math), and one of those changes
Visual Basic cannot use, as shown near the end.
Choosing what to implement
| You want | Implement | Because |
|---|---|---|
|
|
the compiler resolves them from operand types; the library never calls them |
|
|
|
|
|
|
|
the four relational operators, in pairs, usually written with |
same reason as |
|
the matching |
arithmetic has no library hook |
|
a |
conversions have their own operator kind |
|
|
short-circuit evaluation is built from them |
A different ordering or "equal ignoring case" without touching the type |
|
one type can have many orderings and many equalities |
Operator procedures
An operator procedure is a Public Shared procedure named by an operator symbol. The general shape is:
[<attributes>] Public [Overloads] Shared [Shadows] [Widening | Narrowing]
Operator operatorsymbol(operand1 [, operand2]) [As type]
...
Return value
End Operator
The rules, from the Language Reference and checked against the compiler:
-
The declaration context is a class or a structure. An operator inside a
Moduleis rejected (BC33018), and an interface cannot declare one (BC30603: a statement cannot appear within an interface body). -
The modifiers are exactly
PublicandShared: omitSharedand you get BC33012, usePrivateorFriendand you get BC33011. -
Operands are always passed
ByVal;ByRef,OptionalandParamArrayare rejected (BC30651, BC33010). -
At least one operand must be of the containing type (for a conversion, the operand or the return type). Nullable forms of the containing type count. Otherwise: BC33021.
-
A unary operator has one operand, a binary operator two (BC33014, BC33015). Unary
+and-and binary+and-are separate declarations. -
The return type is free:
Money / Moneycan return aDecimal, and=can return something that is not aBoolean(although aBooleanis what every caller expects). -
The operands may have different types, so
Money * DecimalandDecimal * Moneyare two operators, and a type that should multiply commutatively declares both. -
Overloadsis optional on the type that declares the operator, but it is needed to overload (rather than shadow) the operator of a base class, as shown further down. -
Nothing is inherited in the usual sense: the operator is a static method, and the compiler picks it from the declared types of the operands, never from the run-time types.
A first example: money
Option Strict On
Imports System.Globalization
Public Structure Money
Implements IEquatable(Of Money), IComparable(Of Money)
Private ReadOnly _cents As Long
Public Sub New(cents As Long)
_cents = cents
End Sub
Public Shared ReadOnly Property Zero As Money = New Money(0)
Public Shared Function FromEuros(euros As Decimal) As Money
Return New Money(CLng(Math.Round(euros * 100D, MidpointRounding.AwayFromZero)))
End Function
Public ReadOnly Property Cents As Long
Get
Return _cents
End Get
End Property
' Arithmetic: binary and unary forms are separate operator procedures.
Public Shared Operator +(left As Money, right As Money) As Money
Return New Money(left._cents + right._cents)
End Operator
Public Shared Operator -(left As Money, right As Money) As Money
Return New Money(left._cents - right._cents)
End Operator
Public Shared Operator -(value As Money) As Money
Return New Money(-value._cents)
End Operator
Public Shared Operator +(value As Money) As Money
Return value
End Operator
' Mixed operand types: both orders are needed for commutativity.
Public Shared Operator *(value As Money, factor As Decimal) As Money
Return New Money(CLng(Math.Round(value._cents * factor, MidpointRounding.AwayFromZero)))
End Operator
Public Shared Operator *(factor As Decimal, value As Money) As Money
Return value * factor
End Operator
Public Shared Operator /(value As Money, divisor As Decimal) As Money
Return New Money(CLng(Math.Round(value._cents / divisor, MidpointRounding.AwayFromZero)))
End Operator
' The result type need not be the operand type: Money / Money is a ratio.
Public Shared Operator /(left As Money, right As Money) As Decimal
Return CDec(left._cents) / CDec(right._cents)
End Operator
' Comparison operators must be declared in pairs.
Public Shared Operator =(left As Money, right As Money) As Boolean
Return left._cents = right._cents
End Operator
Public Shared Operator <>(left As Money, right As Money) As Boolean
Return left._cents <> right._cents
End Operator
Public Shared Operator <(left As Money, right As Money) As Boolean
Return left._cents < right._cents
End Operator
Public Shared Operator >(left As Money, right As Money) As Boolean
Return left._cents > right._cents
End Operator
Public Shared Operator <=(left As Money, right As Money) As Boolean
Return left._cents <= right._cents
End Operator
Public Shared Operator >=(left As Money, right As Money) As Boolean
Return left._cents >= right._cents
End Operator
Public Overloads Function Equals(other As Money) As Boolean Implements IEquatable(Of Money).Equals
Return _cents = other._cents
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is Money AndAlso Equals(DirectCast(obj, Money))
End Function
Public Overrides Function GetHashCode() As Integer
Return _cents.GetHashCode()
End Function
Public Function CompareTo(other As Money) As Integer Implements IComparable(Of Money).CompareTo
Return _cents.CompareTo(other._cents)
End Function
Public Overrides Function ToString() As String
Return (CDec(_cents) / 100D).ToString("0.00", CultureInfo.InvariantCulture) & " EUR"
End Function
End Structure
Module Program
Sub Main()
Dim perDay = Money.FromEuros(0.25D)
Dim fee = perDay * 12
Dim waived = -Money.FromEuros(1.5D)
Dim total As Money = Money.Zero
total += fee
total += waived
Console.WriteLine("fee = " & fee.ToString())
Console.WriteLine("waived = " & waived.ToString())
Console.WriteLine("total = " & total.ToString())
Console.WriteLine("3 * perDay = " & (3 * perDay).ToString())
Console.WriteLine("fee / 5 = " & (fee / 5).ToString())
Console.WriteLine("fee / perDay = " & (fee / perDay).ToString(CultureInfo.InvariantCulture))
Console.WriteLine("fee > total: " & (fee > total))
Console.WriteLine("fee = perDay * 12: " & (fee = perDay * 12))
Console.WriteLine("fee <> total: " & (fee <> total))
Dim fees As New List(Of Money) From {fee, waived, perDay, Money.Zero}
fees.Sort()
Console.WriteLine("sorted: " & String.Join(", ", fees))
End Sub
End Module
fee = 3.00 EUR
waived = -1.50 EUR
total = 1.50 EUR
3 * perDay = 0.75 EUR
fee / 5 = 0.60 EUR
fee / perDay = 12
fee > total: True
fee = perDay * 12: True
fee <> total: True
sorted: -1.50 EUR, 0.00 EUR, 0.25 EUR, 3.00 EUR
Source: Operator Statement
Points worth noticing in this example:
-
total += feeis a compound assignment: the compiler rewrites it astotal = total + fee, so it works for any type that has the binary operator. The compound forms that exist are+=,-=,*=,/=,\=,^=,&=,<<=and>>=. There is noAnd=,Or=,Xor=orMod=(the compiler rejects them with parse errors such as BC30201); writex = x Or y. -
3 * perDayandperDay * 12both compile because both orders were declared; the integer literal widens toDecimalfor the parameter. Without the second declaration the first form would not compile (BC30311, "Value of type 'Integer' cannot be converted to 'Money'"). -
fee / perDayreturns aDecimal: the result type of an operator is whatever you declare. -
fees.Sort()does not use<; it usesIComparable(Of Money). The relational operators and the interface were written separately here and both delegate to the same field comparison. Keep them consistent. -
Money is stored as a whole number of cents. Arithmetic on money in a
Doubleis a classic source of rounding bugs; see Numbers and Math forDecimaland rounding modes.
Which operators can be overloaded
The Language Reference lists the operators that can appear in an Operator statement. The compiler enforces the list:
anything else, such as AndAlso, is rejected with BC33002.
| Kind | Visual Basic operator | .NET method name (op_) |
C# counterpart |
|---|---|---|---|
Unary |
|
|
unary |
Unary |
|
|
|
Unary |
|
|
|
Binary arithmetic |
|
|
|
Binary arithmetic |
|
|
none |
Binary arithmetic |
|
|
|
Binary arithmetic |
|
|
none |
Concatenation, pattern |
|
|
none |
Logical and bitwise |
|
|
|
Shifts |
|
|
|
Comparison (matched pairs) |
|
|
|
Conversion |
|
|
|
Not overloadable, and why: AndAlso and OrElse are derived from And/IsFalse and Or/IsTrue; Is, IsNot,
TypeOf ... Is, If(...), GetType, AddressOf and Await are not operations on your type; compound assignment is derived
from the binary operator. The right operand of ^ may have any type you declare, but the shift operators must take an
Integer (or Integer?) count (BC33041).
The exact op_ names matter in two situations: when you call Visual Basic operators from another language, and when you use
reflection. This program declares one of everything and prints what the compiler emitted:
Option Strict On
Imports System.Reflection
' One of every overloadable operator; the program prints what the compiler emitted.
Public Structure Bits
Public ReadOnly Value As Integer
Public Sub New(value As Integer)
Me.Value = value
End Sub
Public Shared Operator +(a As Bits, b As Bits) As Bits
Return New Bits(a.Value + b.Value)
End Operator
Public Shared Operator -(a As Bits, b As Bits) As Bits
Return New Bits(a.Value - b.Value)
End Operator
Public Shared Operator -(a As Bits) As Bits
Return New Bits(-a.Value)
End Operator
Public Shared Operator +(a As Bits) As Bits
Return a
End Operator
Public Shared Operator *(a As Bits, b As Bits) As Bits
Return New Bits(a.Value * b.Value)
End Operator
Public Shared Operator /(a As Bits, b As Bits) As Bits
Return New Bits(a.Value \ b.Value)
End Operator
Public Shared Operator \(a As Bits, b As Bits) As Bits
Return New Bits(a.Value \ b.Value)
End Operator
Public Shared Operator Mod(a As Bits, b As Bits) As Bits
Return New Bits(a.Value Mod b.Value)
End Operator
Public Shared Operator ^(a As Bits, b As Integer) As Bits
Return New Bits(CInt(Math.Pow(a.Value, b)))
End Operator
Public Shared Operator &(a As Bits, b As Bits) As Bits
Return New Bits(CInt(a.Value.ToString() & b.Value.ToString()))
End Operator
Public Shared Operator Like(a As Bits, pattern As String) As Boolean
Return a.Value.ToString() Like pattern
End Operator
Public Shared Operator And(a As Bits, b As Bits) As Bits
Return New Bits(a.Value And b.Value)
End Operator
Public Shared Operator Or(a As Bits, b As Bits) As Bits
Return New Bits(a.Value Or b.Value)
End Operator
Public Shared Operator Xor(a As Bits, b As Bits) As Bits
Return New Bits(a.Value Xor b.Value)
End Operator
Public Shared Operator Not(a As Bits) As Bits
Return New Bits(Not a.Value)
End Operator
Public Shared Operator <<(a As Bits, count As Integer) As Bits
Return New Bits(a.Value << count)
End Operator
Public Shared Operator >>(a As Bits, count As Integer) As Bits
Return New Bits(a.Value >> count)
End Operator
Public Shared Operator IsTrue(a As Bits) As Boolean
Return a.Value <> 0
End Operator
Public Shared Operator IsFalse(a As Bits) As Boolean
Return a.Value = 0
End Operator
Public Shared Operator =(a As Bits, b As Bits) As Boolean
Return a.Value = b.Value
End Operator
Public Shared Operator <>(a As Bits, b As Bits) As Boolean
Return a.Value <> b.Value
End Operator
Public Shared Operator <(a As Bits, b As Bits) As Boolean
Return a.Value < b.Value
End Operator
Public Shared Operator >(a As Bits, b As Bits) As Boolean
Return a.Value > b.Value
End Operator
Public Shared Operator <=(a As Bits, b As Bits) As Boolean
Return a.Value <= b.Value
End Operator
Public Shared Operator >=(a As Bits, b As Bits) As Boolean
Return a.Value >= b.Value
End Operator
Public Shared Widening Operator CType(value As Integer) As Bits
Return New Bits(value)
End Operator
Public Shared Narrowing Operator CType(value As Bits) As Integer
Return value.Value
End Operator
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is Bits AndAlso DirectCast(obj, Bits).Value = Value
End Function
Public Overrides Function GetHashCode() As Integer
Return Value
End Function
End Structure
Module Program
Sub Main()
Dim names = GetType(Bits).GetMethods(BindingFlags.Public Or BindingFlags.Static Or BindingFlags.DeclaredOnly) _
.Where(Function(m) m.IsSpecialName AndAlso m.Name.StartsWith("op_", StringComparison.Ordinal)) _
.Select(Function(m) m.Name & "(" & String.Join(",", m.GetParameters().Select(Function(p) p.ParameterType.Name)) & ")->" & m.ReturnType.Name) _
.OrderBy(Function(n) n, StringComparer.Ordinal)
For Each n In names
Console.WriteLine(n)
Next
End Sub
End Module
op_Addition(Bits,Bits)->Bits
op_BitwiseAnd(Bits,Bits)->Bits
op_BitwiseOr(Bits,Bits)->Bits
op_Concatenate(Bits,Bits)->Bits
op_Division(Bits,Bits)->Bits
op_Equality(Bits,Bits)->Boolean
op_ExclusiveOr(Bits,Bits)->Bits
op_Explicit(Bits)->Int32
op_Exponent(Bits,Int32)->Bits
op_False(Bits)->Boolean
op_GreaterThan(Bits,Bits)->Boolean
op_GreaterThanOrEqual(Bits,Bits)->Boolean
op_Implicit(Int32)->Bits
op_Inequality(Bits,Bits)->Boolean
op_IntegerDivision(Bits,Bits)->Bits
op_LeftShift(Bits,Int32)->Bits
op_LessThan(Bits,Bits)->Boolean
op_LessThanOrEqual(Bits,Bits)->Boolean
op_Like(Bits,String)->Boolean
op_Modulus(Bits,Bits)->Bits
op_Multiply(Bits,Bits)->Bits
op_OnesComplement(Bits)->Bits
op_RightShift(Bits,Int32)->Bits
op_Subtraction(Bits,Bits)->Bits
op_True(Bits)->Boolean
op_UnaryNegation(Bits)->Bits
op_UnaryPlus(Bits)->Bits
Source: Operator Procedures
A widening conversion becomes op_Implicit and a narrowing one op_Explicit, which is what lets C# and Visual Basic use each
other’s conversions. A C# project that references this one can use most of the operators with C# syntax. The operators that
C# has no syntax for are ordinary static methods to C#, and can be called by name:
using ShelfDocs;
var a = new Bits(6);
var b = new Bits(3);
Console.WriteLine("a + b = " + (a + b).Value);
Console.WriteLine("a - b = " + (a - b).Value);
Console.WriteLine("a * b = " + (a * b).Value);
Console.WriteLine("a / b = " + (a / b).Value);
Console.WriteLine("a % b = " + (a % b).Value); // Visual Basic Mod
Console.WriteLine("a & b = " + (a & b).Value); // Visual Basic And
Console.WriteLine("a | b = " + (a | b).Value); // Visual Basic Or
Console.WriteLine("a ^ b = " + (a ^ b).Value); // Visual Basic Xor
Console.WriteLine("~a = " + (~a).Value); // Visual Basic Not
Console.WriteLine("a << 1 = " + (a << 1).Value);
Console.WriteLine("a == b = " + (a == b));
Console.WriteLine("a < b = " + (a < b));
int i = (int)a; // Visual Basic Narrowing CType
Bits c = 5; // Visual Basic Widening CType
Console.WriteLine("i, c = " + i + ", " + c.Value);
if (a) Console.WriteLine("a is true (IsTrue)");
Console.WriteLine("op_Exponent = " + Bits.op_Exponent(a, 2).Value); // Visual Basic ^
Console.WriteLine("op_IntegerDivision = " + Bits.op_IntegerDivision(a, b).Value); // Visual Basic \
a + b = 9
a - b = 3
a * b = 18
a / b = 2
a % b = 0
a & b = 2
a | b = 7
a ^ b = 5
~a = -7
a << 1 = 12
a == b = False
a < b = False
i, c = 6, 5
a is true (IsTrue)
op_Exponent = 36
op_IntegerDivision = 2
Bits.op_Exponent(a, 2) works only because C# recognises no ^ exponent operator; the same trick for op_Addition would
be an error. For the C# side of the topic see
Equality and Operator Overloading in C#.
The error messages, collected
Every rule above has a compile-time error. This file breaks them one by one (each declaration is wrong in the way its comment says), and the compiler reports them all:
Option Strict On
Public Structure Meters
Public ReadOnly Value As Double
Public Sub New(value As Double)
Me.Value = value
End Sub
Public Shared Operator <(a As Meters, b As Meters) As Boolean ' no matching '>'
Return a.Value < b.Value
End Operator
Public Shared Operator +(a As Integer, b As Integer) As Integer ' no parameter of type Meters
Return a + b
End Operator
Public Shared Operator -(ByRef a As Meters, b As Meters) As Meters ' ByRef parameter
Return New Meters(a.Value - b.Value)
End Operator
Public Shared Operator *(a As Meters, Optional b As Meters = Nothing) As Meters ' Optional parameter
Return a
End Operator
Public Operator /(a As Meters, b As Meters) As Meters ' not Shared
Return a
End Operator
Public Shared Operator Mod(a As Meters, b As Meters, c As Meters) As Meters ' three operands
Return a
End Operator
Public Shared Operator Not(a As Meters, b As Meters) As Meters ' Not is unary
Return a
End Operator
Public Shared Operator CType(value As Double) As Meters ' neither Widening nor Narrowing
Return New Meters(value)
End Operator
Public Shared Widening Operator CType(value As Meters) As Meters ' converts to itself
Return value
End Operator
Public Shared Widening Operator CType(value As Meters) As Object ' to a base type
Return value
End Operator
Public Shared Operator <<(a As Meters, count As Long) As Meters ' shift count must be Integer
Return a
End Operator
End Structure
Public Class Tag
End Class
Public Class Derived
Inherits Tag
Public Shared Widening Operator CType(value As Derived) As Tag ' to a base class
Return value
End Operator
End Class
Module Program
Sub Main()
Dim a As New Meters(1)
Dim text As String = "x"
Console.WriteLine(text & a) ' no widening to String declared
Dim m As Meters = 3.5 ' no conversion declared
Console.WriteLine(a = a) ' '=' not defined
Dim o As Object = a
Console.WriteLine(o = o) ' Object operands under Option Strict On
End Sub
End Module
Program.vb(10,28): error BC33033: Matching '>' operator is required for 'Public Shared Operator <(a As Meters, b As Meters) As Boolean'.
Program.vb(14,28): error BC33021: At least one parameter of this binary operator must be of the containing type 'Meters'.
Program.vb(18,30): error BC30651: operator parameters cannot be declared 'ByRef'.
Program.vb(22,43): error BC33010: 'operator' parameters cannot be declared 'Optional'.
Program.vb(26,21): error BC33012: Operators must be declared 'Shared'.
Program.vb(30,28): error BC33015: Operator 'Mod' must have two parameters.
Program.vb(34,28): error BC33014: Operator 'Not' must have one parameter.
Program.vb(38,28): error BC33017: Conversion operators must be declared either 'Widening' or 'Narrowing'.
Program.vb(42,37): error BC33024: Conversion operators cannot convert from a type to the same type.
Program.vb(46,37): error BC33028: Conversion operators cannot convert to Object.
Program.vb(50,28): error BC33041: Operator '<<' must have a second parameter of type 'Integer' or 'Integer?'.
Program.vb(61,37): error BC33026: Conversion operators cannot convert from a type to its base type.
Source: Operator Statement
Two of the errors stop the compiler before it gets that far, so they are shown separately: an operator in a module and a
declaration of AndAlso.
Option Strict On
Public Module Helpers
Public Operator +(a As Integer, b As Integer) As Integer ' operator in a Module
Return a + b
End Operator
End Module
Public Structure Meters
Public ReadOnly Value As Double
Public Shared Operator AndAlso(a As Meters, b As Meters) As Meters ' not overloadable
Return a
End Operator
End Structure
Module Program
Sub Main()
End Sub
End Module
Program.vb(4,5): error BC33018: Operators cannot be declared in modules.
Program.vb(12,28): error BC33002: Operator is not overloadable. Operator declaration must be one of: +, -, *, \, /, ^, &, Like, Mod, And, Or, Xor, Not, <<, >>, =, <>, <, <=, >, >=, CType, IsTrue, IsFalse.
Source: Operator Statement
The messages at the use of operators are different, and they are the ones you meet first. The next file declares no
operators at all and uses =, <, Is, &, a conversion and a generic +:
Option Strict On
Public Structure Meters
Public ReadOnly Value As Double
Public Sub New(value As Double)
Me.Value = value
End Sub
End Structure
Public Class Tag
Public ReadOnly Name As String
Public Sub New(name As String)
Me.Name = name
End Sub
End Class
Module Program
Function Total(Of T)(a As T, b As T) As T
Return a + b
End Function
Sub Main()
Dim a As New Meters(1)
Dim b As New Meters(2)
Console.WriteLine(a = b) ' structure without Operator =
Console.WriteLine(a < b) ' structure without Operator <
Console.WriteLine(a Is b) ' Is on a value type
Console.WriteLine("x" & a) ' & needs strings or a defined operator
Dim m As Meters = 3.5 ' no conversion declared
Dim t1 As New Tag("a")
Dim t2 As New Tag("a")
Console.WriteLine(t1 = t2) ' class without Operator =
Dim pair1 = (1, "x")
Dim pair2 = (1, "x")
Console.WriteLine(pair1 = pair2) ' tuples have no = operator in VB
Dim o1 As Object = t1
Dim o2 As Object = t2
Console.WriteLine(o1 = o2) ' Object operands under Option Strict On
End Sub
End Module
Program.vb(21,16): error BC30452: Operator '+' is not defined for types 'T' and 'T'.
Program.vb(27,27): error BC30452: Operator '=' is not defined for types 'Meters' and 'Meters'.
Program.vb(28,27): error BC30452: Operator '<' is not defined for types 'Meters' and 'Meters'.
Program.vb(29,27): error BC30020: 'Is' operator does not accept operands of type 'Meters'. Operands must be reference or nullable types.
Program.vb(29,32): error BC30020: 'Is' operator does not accept operands of type 'Meters'. Operands must be reference or nullable types.
Program.vb(30,27): error BC30452: Operator '&' is not defined for types 'String' and 'Meters'.
Program.vb(31,27): error BC30311: Value of type 'Double' cannot be converted to 'Meters'.
Program.vb(34,27): error BC30452: Operator '=' is not defined for types 'Tag' and 'Tag'.
Program.vb(37,27): error BC30452: Operator '=' is not defined for types '(Integer, String)' and '(Integer, String)'.
Program.vb(40,27): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Program.vb(40,32): error BC32013: Option Strict On disallows operands of type Object for operator '='. Use the 'Is' operator to test for object identity.
Source: Comparison Operators
Reading the list from the top:
-
A structure has no
=unless you define it (BC30452), even thoughValueType.Equalsexists. A class hasIsand nothing else;t1 = t2on twoTagobjects is an error, not a reference comparison. -
Isaccepts only reference types and nullable value types (BC30020). To compare two structures use=(once defined) orEquals. -
"x" & ais BC30452 even whenMetershas a widening conversion toString: the&operator does not consider user-defined conversions, so writea.ToString()orCStr(a). -
A literal
3.5does not convert toMeters(BC30311) until a conversion operator is declared (see below). -
Two tuples cannot be compared with
=: Visual Basic 17.13 has no tuple equality operator (useEquals). -
Under
Option Strict Onthe=operator is rejected forObjectoperands (BC32013). UseIsto test identity. -
A generic method cannot use
+onT(BC30452); there is more on this under generic math.
Further diagnostics that are not worth an example, with the message the SDK printed:
| Code | Message (shortened) | Cause |
|---|---|---|
BC33011 |
Operators must be declared 'Public'. |
|
BC33033 |
Matching 'IsFalse' operator is required for 'Public Shared Operator IsTrue(…)' |
|
BC40003 |
operator 'op_Equality' shadows an overloadable member declared in the base class 'CatalogItem'. … must be declared 'Overloads'. |
derived class declares |
BC31409 |
function 'Equals' must be declared 'Overloads' because another 'Equals' is declared 'Overloads' or 'Overrides'. |
|
BC40005 |
function 'Equals' shadows an overridable method in the base class 'ValueType'. … must be declared 'Overrides'. |
the same mistake as a warning |
BC30311 |
Value of type 'Odd' cannot be converted to 'Integer'. |
|
BC37315 |
Structure 'Meters' cannot implement interface 'IAdditionOperators(Of Meters, Meters, Meters)' because it contains shared abstract or virtual … |
implementing a static-abstract interface (see generic math) |
Operators are chosen at compile time
Operator procedures are static methods, so they are never virtual. A derived class that declares its own = has a second
operator, not an override. Which one runs depends on the declared types of the expression, which is the opposite of what
Equals does:
Option Strict On
Public Class CatalogItem
Public ReadOnly Property Title As String
Public Sub New(title As String)
Me.Title = title
End Sub
Public Shared Operator =(left As CatalogItem, right As CatalogItem) As Boolean
If left Is Nothing OrElse right Is Nothing Then
Return left Is right
End If
Return left.Title = right.Title
End Operator
Public Shared Operator <>(left As CatalogItem, right As CatalogItem) As Boolean
Return Not (left = right)
End Operator
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, CatalogItem)
Return other IsNot Nothing AndAlso Title = other.Title
End Function
Public Overrides Function GetHashCode() As Integer
Return Title.GetHashCode()
End Function
End Class
Public Class Book
Inherits CatalogItem
Public ReadOnly Property Isbn As String
Public Sub New(title As String, isbn As String)
MyBase.New(title)
Me.Isbn = isbn
End Sub
Public Shared Overloads Operator =(left As Book, right As Book) As Boolean
If left Is Nothing OrElse right Is Nothing Then
Return left Is right
End If
Return left.Isbn = right.Isbn
End Operator
Public Shared Overloads Operator <>(left As Book, right As Book) As Boolean
Return Not (left = right)
End Operator
Public Overrides Function Equals(obj As Object) As Boolean
Dim other = TryCast(obj, Book)
Return other IsNot Nothing AndAlso Isbn = other.Isbn
End Function
Public Overrides Function GetHashCode() As Integer
Return Isbn.GetHashCode()
End Function
End Class
Module Program
Sub Main()
Dim first As New Book("Refactoring", "9780134757599")
Dim second As New Book("Refactoring", "9780321127426") ' same title, other edition
Console.WriteLine("Book = Book: " & (first = second))
Dim itemA As CatalogItem = first
Dim itemB As CatalogItem = second
Console.WriteLine("CatalogItem = CatalogItem: " & (itemA = itemB))
Console.WriteLine("Equals (virtual): " & itemA.Equals(itemB))
Console.WriteLine("Book.Equals(Book): " & first.Equals(second))
Console.WriteLine("same hash code: " & (itemA.GetHashCode() = itemB.GetHashCode()))
End Sub
End Module
Book = Book: False
CatalogItem = CatalogItem: True
Equals (virtual): False
Book.Equals(Book): False
same hash code: False
Source: Overloads
first = second is False because both operands are Book and the operator declared in Book compares the ISBN. The same two objects
seen through CatalogItem variables compare by title and are True. The virtual Equals, called through either type,
gives one answer (here the ISBN rule, because Book overrides it). The lesson is general: if a class hierarchy needs
equality, put the logic in the virtual Equals and keep operators as thin wrappers over it, preferably declared once at
the root of the hierarchy, or on a NotInheritable class. The Overloads keyword on the derived operators is required; the
declaration without it is the BC40003 listed above, and the warning is the only hint that you meant to overload.
Operators and nullable value types
Operators declared for a structure are lifted to its nullable form (Money?, written Nullable(Of Money)): the compiler
emits a call that unwraps both operands and, if either is Nothing, produces Nothing instead of calling your code. The
rule is the same as for the built-in operators (see
Nothing and Nullable Value Types):
Option Strict On
Public Structure Money
Public ReadOnly Cents As Long
Public Sub New(cents As Long)
Me.Cents = cents
End Sub
Public Shared Operator +(left As Money, right As Money) As Money
Console.WriteLine(" Money + Money called")
Return New Money(left.Cents + right.Cents)
End Operator
Public Shared Operator =(left As Money, right As Money) As Boolean
Console.WriteLine(" Money = Money called")
Return left.Cents = right.Cents
End Operator
Public Shared Operator <>(left As Money, right As Money) As Boolean
Return left.Cents <> right.Cents
End Operator
Public Shared Operator <(left As Money, right As Money) As Boolean
Return left.Cents < right.Cents
End Operator
Public Shared Operator >(left As Money, right As Money) As Boolean
Return left.Cents > right.Cents
End Operator
Public Overrides Function ToString() As String
Return Cents.ToString() & "c"
End Function
End Structure
Module Program
Sub Main()
Dim a As Money? = New Money(100)
Dim b As Money? = New Money(250)
Dim none As Money? = Nothing
Console.WriteLine("a + b:")
Dim sum = a + b
Console.WriteLine(" = " & sum.ToString() & " (HasValue " & sum.HasValue & ")")
Console.WriteLine("a + none:")
Dim lifted = a + none
Console.WriteLine(" HasValue: " & lifted.HasValue)
Console.WriteLine("a = b:")
Console.WriteLine(" " & (a = b).ToString())
Console.WriteLine("a = none (the result is Nothing, not False):")
Dim eq = (a = none)
Console.WriteLine(" HasValue: " & eq.HasValue)
Console.WriteLine(" as a condition: " & If(a = none, "true", "false"))
Console.WriteLine(" a <> none as a condition: " & If(a <> none, "true", "false"))
Console.WriteLine("a < b: " & (a < b).ToString())
Console.WriteLine("a Is Nothing: " & (a Is Nothing))
Console.WriteLine("none Is Nothing: " & (none Is Nothing))
End Sub
End Module
a + b:
Money + Money called
= 350c (HasValue True)
a + none:
HasValue: False
a = b:
Money = Money called
False
a = none (the result is Nothing, not False):
HasValue: False
as a condition: false
a <> none as a condition: false
a < b: True
a Is Nothing: False
none Is Nothing: True
Source: Nullable Value Types
For comparison operators the lifted result is a Boolean? that is Nothing when either side is Nothing, and a Nothing
condition is treated as False. So a = none and a <> none are both false in an If, which is why a null test on a
nullable structure should be Is Nothing or HasValue, and why the operator’s side effect line is printed only when both
operands have values. This is also the reason to keep operators free of side effects.
Operators inside statements
Several statements are defined in terms of operators, and they accept your types without further work if the operators exist:
Option Strict On
Public Structure Day
Public ReadOnly Number As Integer
Public Sub New(number As Integer)
Me.Number = number
End Sub
Public Shared Operator +(left As Day, right As Day) As Day
Return New Day(left.Number + right.Number)
End Operator
Public Shared Operator -(left As Day, right As Day) As Day
Return New Day(left.Number - right.Number)
End Operator
Public Shared Operator <=(left As Day, right As Day) As Boolean
Return left.Number <= right.Number
End Operator
Public Shared Operator >=(left As Day, right As Day) As Boolean
Return left.Number >= right.Number
End Operator
Public Shared Operator =(left As Day, right As Day) As Boolean
Console.WriteLine(" (= called)")
Return left.Number = right.Number
End Operator
Public Shared Operator <>(left As Day, right As Day) As Boolean
Return left.Number <> right.Number
End Operator
Public Shared Operator <(left As Day, right As Day) As Boolean
Return left.Number < right.Number
End Operator
Public Shared Operator >(left As Day, right As Day) As Boolean
Return left.Number > right.Number
End Operator
Public Overrides Function ToString() As String
Return "day " & Number
End Function
End Structure
Module Program
Sub Main()
' For ... Next over a user-defined type needs +, -, <= and >=.
For d As Day = New Day(1) To New Day(7) Step New Day(3)
Console.WriteLine("For: " & d.ToString())
Next
' Select Case uses = for Case x, and the relational operators for Is and To.
Dim today As New Day(5)
Select Case today
Case New Day(1)
Console.WriteLine("first day")
Case Is >= New Day(5)
Console.WriteLine("day 5 or later")
End Select
Select Case today
Case New Day(1) To New Day(10)
Console.WriteLine("within the first ten days")
End Select
End Sub
End Module
For: day 1
For: day 4
For: day 7
(= called)
day 5 or later
within the first ten days
Source: For…Next Statement
-
For d As Day = a To b Step cneeds+,-,<=and>=on the type (the example declares no conversion fromInteger, so none is needed). It runs at every step through your operators. -
Select Case xfollowed byCase yevaluatesx = y, hence the(= called)line;Case Is >= yuses the relational operator, andCase y To zuses>=and<=. See Control Flow. -
Aggregatewith a lambda,Function(a, b) a + b, works for any type that has the operator, because the lambda contains the+expression itself (see the generic-math section).
IsTrue, IsFalse and the short-circuit operators
IsTrue and IsFalse are the conversion of an object to a condition, and they are the building blocks of AndAlso and
OrElse. They are matched, both must return Boolean, and you cannot declare AndAlso or OrElse yourself. To use them
on your own type you must declare:
-
AndAlso:Operator And(left As T, right As T) As TandOperator IsFalse(x As T) As Boolean. -
OrElse:Operator Or(left As T, right As T) As TandOperator IsTrue(x As T) As Boolean.
a AndAlso b then means: if IsFalse(a) then a, otherwise a And b; b is evaluated only in the second case:
Option Strict On
' A three-state check result whose AndAlso / OrElse short-circuit through IsFalse / IsTrue.
Public Structure Check
Public ReadOnly Name As String
Public ReadOnly Passed As Boolean
Public Sub New(name As String, passed As Boolean)
Me.Name = name
Me.Passed = passed
End Sub
Public Shared Operator IsTrue(c As Check) As Boolean
Return c.Passed
End Operator
Public Shared Operator IsFalse(c As Check) As Boolean
Return Not c.Passed
End Operator
' AndAlso needs And with this exact shape; OrElse needs Or.
Public Shared Operator And(left As Check, right As Check) As Check
Return New Check(left.Name & " and " & right.Name, left.Passed AndAlso right.Passed)
End Operator
Public Shared Operator Or(left As Check, right As Check) As Check
Return New Check(left.Name & " or " & right.Name, left.Passed OrElse right.Passed)
End Operator
Public Shared Operator Not(c As Check) As Check
Return New Check("not " & c.Name, Not c.Passed)
End Operator
Public Overrides Function ToString() As String
Return Name & "=" & Passed
End Function
End Structure
Module Program
Function Probe(name As String, passed As Boolean) As Check
Console.WriteLine(" evaluating " & name)
Return New Check(name, passed)
End Function
Sub Main()
Console.WriteLine("And (both sides always run):")
Dim a = Probe("isbn-valid", False) And Probe("not-on-loan", True)
Console.WriteLine(" -> " & a.ToString())
Console.WriteLine("AndAlso (right side skipped when the left IsFalse):")
Dim b = Probe("isbn-valid", False) AndAlso Probe("not-on-loan", True)
Console.WriteLine(" -> " & b.ToString())
Console.WriteLine("OrElse (right side skipped when the left IsTrue):")
Dim c = Probe("member", True) OrElse Probe("staff", False)
Console.WriteLine(" -> " & c.ToString())
Console.WriteLine("If condition uses IsTrue:")
If Probe("fine-paid", True) Then
Console.WriteLine(" can borrow")
End If
Console.WriteLine("Not: " & (Not Probe("overdue", False)).ToString())
End Sub
End Module
And (both sides always run):
evaluating isbn-valid
evaluating not-on-loan
-> isbn-valid and not-on-loan=False
AndAlso (right side skipped when the left IsFalse):
evaluating isbn-valid
-> isbn-valid=False
OrElse (right side skipped when the left IsTrue):
evaluating member
-> member=True
If condition uses IsTrue:
evaluating fine-paid
can borrow
evaluating overdue
Not: not overdue=True
Source: IsTrue Operator
If Probe("fine-paid", True) Then also works: a type with IsTrue is usable as a condition without comparing it with
anything. Overloading And and Or forces the bitwise meaning: if you define them on a type, you cannot also give the
logical meaning. If And returns a different type, or its operands differ, AndAlso fails at the use site (the BC30311 in the
table above). Three-state logic types are the traditional reason to implement this; most application types should not.
Bitwise and shift operators
Types that model sets of flags can define And, Or, Xor, Not and the shifts. Unlike an Enum with <Flags>, a
structure can enforce a mask (the Not below keeps only the three defined rights) and carry methods:
Option Strict On
' A set of up to 8 permissions as a value type with logical and shift operators.
Public Structure RightSet
Private ReadOnly _bits As Byte
Public Sub New(bits As Byte)
_bits = bits
End Sub
Public Shared ReadOnly Property Borrow As New RightSet(1)
Public Shared ReadOnly Property Renew As New RightSet(2)
Public Shared ReadOnly Property Reserve As New RightSet(4)
Public Shared Operator Or(left As RightSet, right As RightSet) As RightSet
Return New RightSet(left._bits Or right._bits)
End Operator
Public Shared Operator And(left As RightSet, right As RightSet) As RightSet
Return New RightSet(left._bits And right._bits)
End Operator
Public Shared Operator Xor(left As RightSet, right As RightSet) As RightSet
Return New RightSet(left._bits Xor right._bits)
End Operator
Public Shared Operator Not(value As RightSet) As RightSet
Return New RightSet(CByte((Not value._bits) And &H7)) ' only the 3 defined rights
End Operator
Public Shared Operator <<(value As RightSet, count As Integer) As RightSet
Return New RightSet(CByte((CInt(value._bits) << count) And &H7))
End Operator
Public Shared Operator >>(value As RightSet, count As Integer) As RightSet
Return New RightSet(CByte(CInt(value._bits) >> count))
End Operator
' With IsTrue/IsFalse, "If rights Then" means "at least one right".
Public Shared Operator IsTrue(value As RightSet) As Boolean
Return value._bits <> 0
End Operator
Public Shared Operator IsFalse(value As RightSet) As Boolean
Return value._bits = 0
End Operator
Public Overrides Function ToString() As String
Dim names As New List(Of String)
If (_bits And 1) <> 0 Then names.Add("Borrow")
If (_bits And 2) <> 0 Then names.Add("Renew")
If (_bits And 4) <> 0 Then names.Add("Reserve")
Return If(names.Count = 0, "(none)", String.Join("|", names))
End Function
End Structure
Module Program
Sub Main()
Dim member = RightSet.Borrow Or RightSet.Renew
Console.WriteLine("member: " & member.ToString())
Dim student = member
student = student And Not RightSet.Renew
Console.WriteLine("student: " & student.ToString())
Dim toggled = member Xor RightSet.Reserve
Console.WriteLine("member Xor Reserve: " & toggled.ToString())
Dim shifted = RightSet.Borrow << 1
Console.WriteLine("Borrow << 1: " & shifted.ToString())
Console.WriteLine("Reserve >> 2: " & (RightSet.Reserve >> 2).ToString())
member = member Or RightSet.Reserve ' there is no Or= in Visual Basic
Console.WriteLine("member Or Reserve: " & member.ToString())
Dim grown = RightSet.Borrow
grown <<= 1 ' <<= and >>= exist and use << and >>
Console.WriteLine("grown <<= 1: " & grown.ToString())
Dim none = RightSet.Borrow And RightSet.Renew
If none Then
Console.WriteLine("none is true")
Else
Console.WriteLine("none is false (IsFalse)")
End If
' AndAlso / OrElse need And / Or plus IsFalse / IsTrue, all declared above.
Dim allowed = member AndAlso RightSet.Borrow
Console.WriteLine("member AndAlso Borrow: " & allowed.ToString())
End Sub
End Module
member: Borrow|Renew
student: Borrow
member Xor Reserve: Borrow|Renew|Reserve
Borrow << 1: Renew
Reserve >> 2: Borrow
member Or Reserve: Borrow|Renew|Reserve
grown <<= 1: Renew
none is false (IsFalse)
member AndAlso Borrow: Borrow
Source: Logical and Bitwise Operators
student = student And Not RightSet.Renew shows the precedence: Not binds tighter than And. For plain flag sets, an
Enum with the Flags attribute is simpler, see
Enumerations. Note that there is no Or=; the shift compound
assignments <<= and >>= do exist and call the shift operators.
Concatenation and Like
Concatenation (&) and pattern matching (Like) are Visual Basic operators with no C# counterpart, and a type can take them
over like any other. Their op_ methods are op_Concatenate and op_Like:
Option Strict On
' A shelf mark such as "QA76.73" is a class letter pair plus a number.
Public Structure ShelfMark
Public ReadOnly Section As String
Public ReadOnly Number As String
Public Sub New(section As String, number As String)
Me.Section = section
Me.Number = number
End Sub
' Concatenation: the operator & is VB-only and has its own operator name (op_Concatenate).
Public Shared Operator &(left As ShelfMark, suffix As String) As ShelfMark
Return New ShelfMark(left.Section, left.Number & suffix)
End Operator
Public Shared Operator &(prefix As String, right As ShelfMark) As ShelfMark
Return New ShelfMark(prefix & right.Section, right.Number)
End Operator
' Like: the right operand is whatever the operator declares, here a pattern over the text form.
Public Shared Operator Like(mark As ShelfMark, pattern As String) As Boolean
Return mark.ToString() Like pattern
End Operator
Public Overrides Function ToString() As String
Return Section & Number
End Function
End Structure
Module Program
Sub Main()
Dim mark As New ShelfMark("QA", "76.73")
Dim revised = mark & ".A"
Dim moved = "Z-" & mark
Console.WriteLine("mark & "".A"" = " & revised.ToString())
Console.WriteLine("""Z-"" & mark = " & moved.ToString())
Dim tail = mark
tail &= ".B" ' compound assignment uses the & operator
Console.WriteLine("mark &= "".B"" = " & tail.ToString())
Console.WriteLine("mark Like ""QA*"" = " & (mark Like "QA*"))
Console.WriteLine("mark Like ""Q[A-C]##.##"" = " & (mark Like "Q[A-C]##.##"))
Console.WriteLine("mark Like ""PS*"" = " & (mark Like "PS*"))
' The built-in Like on strings follows Option Compare (Binary here).
Console.WriteLine("""qa76"" Like ""QA*"" = " & ("qa76" Like "QA*"))
End Sub
End Module
mark & ".A" = QA76.73.A
"Z-" & mark = Z-QA76.73
mark &= ".B" = QA76.73.B
mark Like "QA*" = True
mark Like "Q[A-C]##.##" = True
mark Like "PS*" = False
"qa76" Like "QA*" = False
Source: Like Operator
The right operand of an overloaded Like is whatever you declare, a String pattern here. The built-in Like on two strings
is covered on Operators and Expressions, including
how Option Compare changes it. Overloading & is rare; a method such as Append is clearer, and ToString plus
interpolation is what most code needs.
Conversion operators: Widening and Narrowing CType
A conversion operator is always called CType, has exactly one operand, and converts to or from the containing type. The
Widening or Narrowing keyword is mandatory (BC33017 otherwise) and expresses what you promise:
-
Wideningconversions must never fail and lose no information; the compiler applies them implicitly, even withOption Strict On. -
Narrowingconversions may fail or lose information; withOption Strict Onthey require an explicitCType(or a conversion function). ChooseNarrowingwhenever the operator can throw.
A conversion operator cannot convert a type to itself (BC33024), to a base type (BC33026), to an interface (BC33025) or to
Object (BC33028): those conversions already exist through inheritance. A widening and a narrowing operator with the same
source and target types are duplicates (BC30269). The operator names seen from other languages are op_Implicit (widening) and
op_Explicit (narrowing).
Option Strict On
Public Structure Isbn
Implements IEquatable(Of Isbn)
Public ReadOnly Property Value As String
Private Sub New(value As String)
Me.Value = value
End Sub
Public Shared Function TryParse(text As String, ByRef result As Isbn) As Boolean
Dim digits = New String(If(text, "").Where(AddressOf Char.IsLetterOrDigit).ToArray())
If digits.Length = 13 AndAlso digits.All(AddressOf Char.IsDigit) AndAlso HasValidCheckDigit(digits) Then
result = New Isbn(digits)
Return True
End If
Return False
End Function
Public Shared Function Parse(text As String) As Isbn
Dim result As Isbn = Nothing
If Not TryParse(text, result) Then
Throw New FormatException($"{text} is not a valid ISBN-13.")
End If
Return result
End Function
Private Shared Function HasValidCheckDigit(digits As String) As Boolean
Dim sum = 0
For i = 0 To 12
Dim d = AscW(digits(i)) - AscW("0"c)
sum += If(i Mod 2 = 0, d, d * 3)
Next
Return sum Mod 10 = 0
End Function
' Every ISBN converts to String without loss, so the conversion is Widening (implicit).
Public Shared Widening Operator CType(value As Isbn) As String
Return value.Value
End Operator
' A String may not be a valid ISBN, so the conversion is Narrowing (explicit, may throw).
Public Shared Narrowing Operator CType(text As String) As Isbn
Return Parse(text)
End Operator
Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
Return Value = other.Value
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is Isbn AndAlso Equals(DirectCast(obj, Isbn))
End Function
Public Overrides Function GetHashCode() As Integer
Return If(Value, "").GetHashCode()
End Function
End Structure
Module Program
Sub Main()
Dim isbn As Isbn = CType("978-0-13-468599-1", Isbn)
' Widening: no cast needed, not even under Option Strict On.
Dim text As String = isbn
Console.WriteLine("widened: " & text)
' Overload resolution can pick the user-defined widening conversion.
Console.Write("Console.WriteLine(isbn): ")
Console.WriteLine(isbn)
Console.WriteLine("explicit CStr: " & CStr(isbn))
' Narrowing needs CType (the operator runs, so it may throw).
Try
Dim bad = CType("not an isbn", Isbn)
Console.WriteLine(bad)
Catch ex As FormatException
Console.WriteLine("CType threw: " & ex.Message)
End Try
' DirectCast and TryCast never call user-defined conversions.
Dim boxed As Object = text
Try
Dim viaDirectCast = DirectCast(boxed, Isbn)
Console.WriteLine(viaDirectCast)
Catch ex As InvalidCastException
Console.WriteLine("DirectCast threw: " & ex.GetType().Name)
End Try
Dim viaTryCast = TryCast(boxed, String)
Console.WriteLine("TryCast to String: " & viaTryCast)
' CType on an Object that holds the right type just unboxes.
Dim fromObject = CType(CObj(isbn), Isbn)
Console.WriteLine("unboxed equals: " & fromObject.Equals(isbn))
End Sub
End Module
widened: 9780134685991
Console.WriteLine(isbn): 9780134685991
explicit CStr: 9780134685991
CType threw: not an isbn is not a valid ISBN-13.
DirectCast threw: InvalidCastException
TryCast to String: 9780134685991
unboxed equals: True
What the output shows:
-
Dim text As String = isbnneeds no cast: the widening operator applies implicitly.Console.WriteLine(isbn)also printed the digits, not the type name, because overload resolution accepted the user-defined conversion toStringfor theWriteLine(String)overload, which beatsWriteLine(Object). An implicit conversion toStringis therefore not harmless: it silently changes which overload an existing call binds to. Prefer overridingToStringand, if you want conversions at all, aNarrowingdirection fromStringplus an explicit method (Parse) for the other. -
CType("not an isbn", Isbn)runs the operator, which parsed and threwFormatException. That is the reason it isNarrowing. -
DirectCastandTryCastnever run user-defined conversions: they test the run-time type only.DirectCast(boxed, Isbn)whereboxedholds aStringthrowsInvalidCastException. Of the cast forms onlyCTypeand theC...functions (CStr,CInt, …) use conversion operators. See Type Conversions for the full conversion tables. -
&and the other operators do not apply user-defined conversions to their operands (the"x" & aerror above).
The Narrowing operator to String-like targets should have a counterpart TryParse: exceptions are a poor way to report
bad input, and the shelf Isbn offers both.
Equality: what the operators and methods compare
Before implementing equality, be clear about what each form asks:
| Form | Compares | Notes |
|---|---|---|
|
references |
only for reference types and nullable value types; never calls user code |
|
references |
boxes value types: two boxes are never the same object |
|
whatever the declared types define |
built-in for primitives, |
|
the virtual |
a |
|
|
what generic code uses when it cannot assume non-null |
|
|
what |
|
a named string comparison |
independent of |
Reference and value equality
Option Strict On
Public Class Reader
Public ReadOnly Property Name As String
Public Sub New(name As String)
Me.Name = name
End Sub
End Class
Public Structure Point
Public ReadOnly X As Integer
Public ReadOnly Y As Integer
Public Sub New(x As Integer, y As Integer)
Me.X = x
Me.Y = y
End Sub
End Structure
Module Program
Sub Main()
' A class with no overrides: Equals and Is both mean "the same object".
Dim r1 As New Reader("Ana")
Dim r2 As New Reader("Ana")
Dim r3 = r1
Console.WriteLine("class: r1 Is r2 = " & (r1 Is r2) & ", r1.Equals(r2) = " & r1.Equals(r2))
Console.WriteLine("class: r1 Is r3 = " & (r1 Is r3) & ", r1.Equals(r3) = " & r1.Equals(r3))
' A structure with no overrides: ValueType.Equals compares the fields.
Dim p1 As New Point(1, 2)
Dim p2 As New Point(1, 2)
Console.WriteLine("structure: p1.Equals(p2) = " & p1.Equals(p2))
' Boxing creates a new object each time, and Visual Basic also copies a boxed structure
' whenever an Object variable is passed or assigned (RuntimeHelpers.GetObjectValue).
Dim box1 As Object = p1
Dim box2 As Object = box1
Console.WriteLine("structure: box1 Is box1 = " & (box1 Is box1))
Console.WriteLine("structure: box1 Is box2 = " & (box1 Is box2))
Console.WriteLine("structure: ReferenceEquals(box1, box1) = " & Object.ReferenceEquals(box1, box1))
' Strings are reference types with value semantics for = and Equals.
Dim literal1 = "shelf"
Dim literal2 = "shelf"
Dim built = New String("shelf".ToCharArray())
Console.WriteLine("string: literals Is = " & (literal1 Is literal2) & " (interned)")
Console.WriteLine("string: built Is literal = " & (built Is literal1))
Console.WriteLine("string: built = literal = " & (built = literal1))
Console.WriteLine("string: built.Equals(literal) = " & built.Equals(literal1))
Console.WriteLine("string: Intern(built) Is literal = " & (String.Intern(built) Is literal1))
' Object.Equals(a, b) handles Nothing and then calls a.Equals(b).
Dim missing As Reader = Nothing
Console.WriteLine("Object.Equals(Nothing, Nothing) = " & Object.Equals(missing, Nothing))
Console.WriteLine("Object.Equals(r1, Nothing) = " & Object.Equals(r1, missing))
Console.WriteLine("missing Is Nothing = " & (missing Is Nothing) & ", r1 IsNot Nothing = " & (r1 IsNot Nothing))
Try
Console.WriteLine(missing.Equals(r1))
Catch ex As NullReferenceException
Console.WriteLine("missing.Equals(r1) threw " & ex.GetType().Name)
End Try
End Sub
End Module
class: r1 Is r2 = False, r1.Equals(r2) = False
class: r1 Is r3 = True, r1.Equals(r3) = True
structure: p1.Equals(p2) = True
structure: box1 Is box1 = True
structure: box1 Is box2 = False
structure: ReferenceEquals(box1, box1) = False
string: literals Is = True (interned)
string: built Is literal = False
string: built = literal = True
string: built.Equals(literal) = True
string: Intern(built) Is literal = True
Object.Equals(Nothing, Nothing) = True
Object.Equals(r1, Nothing) = False
missing Is Nothing = True, r1 IsNot Nothing = True
missing.Equals(r1) threw NullReferenceException
Source: Object.Equals Method
-
A class without overrides has reference equality in both
IsandEquals; twoReaderobjects with the same name are different. -
A structure without overrides gets
ValueType.Equals, which compares field by field (for structures with reference fields this is typically slow and allocates, see the boxing example below). It works, but the structure still has no=operator. The analyzer CA2013 flagsObject.ReferenceEqualscalled with a value type: it is on by default, and it was an error in these projects because warnings are treated as errors. -
Literals of the same value are interned, so
literal1 Is literal2isTrue, while a string built at run time is a different object with the same content. Use=orString.Equals, neverIs, for text. -
The last pair of lines is a Visual Basic quirk. When an
Objectvariable holding a boxed structure is assigned or passed, the compiler wraps it inRuntimeHelpers.GetObjectValue, which returns a copy of a mutable boxed value so that value semantics survive.box1 Is box2is thereforeFalseeven thoughbox2was assigned frombox1, andObject.ReferenceEquals(box1, box1)isFalsefor the same reason;Isbetween a variable and itself does not copy, sobox1 Is box1isTrue. Never use reference equality on boxed value types. -
Object.Equals(Nothing, Nothing)isTrueandObject.Equals(r1, Nothing)isFalse, butmissing.Equals(r1)on aNothingreference throws, exactly as in C#. Prefer the static form or theIsNot Nothingguard.
Strings and Option Compare
The comparison operators on String follow Option Compare, and nothing else does. A file with Option Compare Text makes
=, <, >, Like, Select Case and InStr case-insensitive, while every String method keeps its own rules:
Option Compare Text
Option Strict On
Module Program
Sub Main()
Dim a = "Shelf"
Dim b = "SHELF"
' With Option Compare Text, = < > and Like are case-insensitive;
' the String methods are not affected.
Console.WriteLine("a = b : " & (a = b))
Console.WriteLine("a.Equals(b) : " & a.Equals(b))
Console.WriteLine("String.Equals(a, b) : " & String.Equals(a, b))
Console.WriteLine("a Like ""sh*"" : " & (a Like "sh*"))
Console.WriteLine("a < b : " & (a < b))
Console.WriteLine("InStr(a, ""HEL"") : " & InStr(a, "HEL"))
Console.WriteLine("a.IndexOf(""HEL"") : " & a.IndexOf("HEL"))
Console.WriteLine("Select Case on a : " & Classify(a))
End Sub
Function Classify(text As String) As String
Select Case text
Case "shelf"
Return "matched ""shelf"""
Case Else
Return "no match"
End Select
End Function
End Module
a = b : True
a.Equals(b) : False
String.Equals(a, b) : False
a Like "sh*" : True
a < b : False
InStr(a, "HEL") : 2
a.IndexOf("HEL") : -1
Select Case on a : matched "shelf"
Source: Option Compare Statement
This is why equality code that must not depend on the file’s setting (hash keys, protocol keywords, file names) states its
comparison explicitly with StringComparer or StringComparison, and why an Equals implementation must not use = on
strings if the project could change Option Compare (the project property OptionCompare sets it for all files).
See Option Statements.
Numbers: where =, Equals and CompareTo disagree
Option Strict On
Module Program
Sub Main()
' Floating point: = follows IEEE 754, Equals and CompareTo give NaN a place in a total order.
Dim nan = Double.NaN
Console.WriteLine("NaN = NaN : " & (nan = nan))
Console.WriteLine("NaN.Equals(NaN) : " & nan.Equals(nan))
Console.WriteLine("NaN.CompareTo(NaN) : " & nan.CompareTo(nan))
Console.WriteLine("NaN < 1.0 : " & (nan < 1.0))
Console.WriteLine("NaN.CompareTo(1.0) : " & nan.CompareTo(1.0))
Console.WriteLine("0.0 = -0.0 : " & (0.0 = -0.0))
Console.WriteLine("0.1 + 0.2 = 0.3 : " & (0.1 + 0.2 = 0.3))
Console.WriteLine("within 1E-9 : " & (Math.Abs(0.1 + 0.2 - 0.3) < 1.0E-9))
' Decimal ignores trailing zeros in comparisons.
Dim one = 1.0D
Dim oneAgain = 1.00D
Console.WriteLine("1.0D = 1.00D : " & (one = oneAgain) & " but prints " & one & " and " & oneAgain)
Console.WriteLine("hash codes equal : " & (one.GetHashCode() = oneAgain.GetHashCode()))
' = widens its operands; Equals does not.
Dim i As Integer = 5
Dim l As Long = 5
Console.WriteLine("i = l : " & (i = l))
Console.WriteLine("i.Equals(l) : " & i.Equals(l))
Console.WriteLine("l.Equals(i) : " & l.Equals(i))
Console.WriteLine("Object.Equals(i, l) : " & Object.Equals(i, l))
End Sub
End Module
NaN = NaN : False
NaN.Equals(NaN) : True
NaN.CompareTo(NaN) : 0
NaN < 1.0 : False
NaN.CompareTo(1.0) : -1
0.0 = -0.0 : True
0.1 + 0.2 = 0.3 : False
within 1E-9 : True
1.0D = 1.00D : True but prints 1.0 and 1.00
hash codes equal : True
i = l : True
i.Equals(l) : False
l.Equals(i) : True
Object.Equals(i, l) : False
Source: Double.Equals Method
-
=on floating point numbers follows IEEE 754:NaN = NaNisFalse,0.0 = -0.0isTrue.EqualsandCompareToform a consistent total order instead, soNaN.Equals(NaN)isTrueandNaNsorts first. A hash set of doubles therefore holds oneNaN. -
0.1 + 0.2 = 0.3isFalse; compare with a tolerance.Decimalignores trailing zeros when comparing and hashing, butToStringkeeps them. -
=widens its operands (i = lcompares asLong).i.Equals(l)goes toEquals(Object)with a boxedLong, which is never anInteger, so it isFalse;l.Equals(i)binds toLong.Equals(Long)through a widening conversion and isTrue. Equality across numeric types is therefore not symmetric in method form, which is one more reason to compare with=or to convert explicitly. See Numbers and Math.
Implementing value equality
The contract that every collection relies on:
-
Reflexive, symmetric, transitive:
x.Equals(x),x.Equals(y) = y.Equals(x), and equality chains. -
Consistent: the same answer while the compared state does not change.
-
Never throws for a
Nothingargument, andx.Equals(Nothing)isFalse. -
Hash consistent: equal objects return equal
GetHashCode()values; unequal objects may collide. -
IEquatable(Of T).Equals,Equals(Object),=/<>andCompareTo = 0agree.
For a structure, the complete set is IEquatable(Of T), both Equals methods, GetHashCode, and the =/<> pair, as in the
shelf Isbn:
Option Strict On
Public Structure Isbn
Implements IEquatable(Of Isbn)
Public ReadOnly Property Value As String
Private Sub New(value As String)
Me.Value = value
End Sub
Public Shared Function TryParse(text As String, ByRef result As Isbn) As Boolean
Dim digits = New String(If(text, "").Where(AddressOf Char.IsLetterOrDigit).ToArray())
If digits.Length = 13 AndAlso digits.All(AddressOf Char.IsDigit) AndAlso HasValidCheckDigit(digits) Then
result = New Isbn(digits)
Return True
End If
Return False
End Function
Public Shared Function Parse(text As String) As Isbn
Dim result As Isbn = Nothing
If Not TryParse(text, result) Then
Throw New FormatException($"{text} is not a valid ISBN-13.")
End If
Return result
End Function
Private Shared Function HasValidCheckDigit(digits As String) As Boolean
Dim sum = 0
For i = 0 To 12
Dim d = AscW(digits(i)) - AscW("0"c)
sum += If(i Mod 2 = 0, d, d * 3)
Next
Return sum Mod 10 = 0
End Function
' IEquatable(Of Isbn): the strongly typed Equals, no boxing in generic collections.
Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
Return String.Equals(Value, other.Value, StringComparison.Ordinal)
End Function
' Object.Equals: the override that non-generic code and old collections call.
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is Isbn AndAlso Equals(DirectCast(obj, Isbn))
End Function
' Equal values must give equal hash codes; use only the fields that Equals uses.
Public Overrides Function GetHashCode() As Integer
Return If(Value, "").GetHashCode()
End Function
Public Overrides Function ToString() As String
Return If(Value, "(no ISBN)")
End Function
Public Shared Operator =(left As Isbn, right As Isbn) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As Isbn, right As Isbn) As Boolean
Return Not left.Equals(right)
End Operator
End Structure
Module Program
Sub Main()
Dim a = Isbn.Parse("978-0-13-468599-1")
Dim b = Isbn.Parse("9780134685991")
Dim c = Isbn.Parse("978-0-13-475759-9")
Dim unset As Isbn = Nothing
Console.WriteLine("a = b: " & (a = b) & ", a <> c: " & (a <> c))
Console.WriteLine("a.Equals(b): " & a.Equals(b))
Console.WriteLine("a.Equals(CObj(b)): " & a.Equals(CObj(b)))
Console.WriteLine("a.Equals(""9780134685991""): " & a.Equals("9780134685991"))
Console.WriteLine("a.Equals(Nothing): " & a.Equals(Nothing))
Console.WriteLine("unset = unset: " & (unset = unset) & " (prints " & unset.ToString() & ")")
Console.WriteLine("a hash = b hash: " & (a.GetHashCode() = b.GetHashCode()))
Dim seen As New HashSet(Of Isbn) From {a, b, c}
Console.WriteLine("HashSet count: " & seen.Count)
Dim titles As New Dictionary(Of Isbn, String)
titles(a) = "first entry"
titles(b) = "second entry"
Console.WriteLine("Dictionary count: " & titles.Count & ", value: " & titles(a))
Dim list As New List(Of Isbn) From {a, c}
Console.WriteLine("List.Contains(b): " & list.Contains(b))
Console.WriteLine("List.IndexOf(c): " & list.IndexOf(c))
Console.WriteLine("Distinct count: " & New Isbn() {a, b, c, a}.Distinct().Count())
End Sub
End Module
a = b: True, a <> c: True
a.Equals(b): True
a.Equals(CObj(b)): True
a.Equals("9780134685991"): False
a.Equals(Nothing): False
unset = unset: True (prints (no ISBN))
a hash = b hash: True
HashSet count: 2
Dictionary count: 1, value: second entry
List.Contains(b): True
List.IndexOf(c): 1
Distinct count: 2
Source: IEquatable<T> Interface
Details that the output exercises:
-
Equals(other As Isbn)needs theOverloadskeyword because the type also overridesEquals(Object): without it the compiler reports BC31409, plus warning BC40005 for the shadowedValueType.Equals. -
Equals(Object)must accept anything and returnFalsefor a foreign type:a.Equals("9780134685991")isFalseeven though the text is the ISBN’s. -
The default value of the structure (
unset) hasValue = Nothing, so every member must be null-safe.ToStringandGetHashCodeuseIf(Value, "")here. -
HashSet,Dictionary,List.Contains,IndexOf,DistinctandContainsall found the equal ISBN, because they useEqualityComparer(Of Isbn).Default, which callsIEquatable(Of Isbn).Equals. None of them called the=operator. -
GetHashCodehashes the same fieldEqualscompares. In .NET Core and laterHashCode.Combine(a, b, ...)is the recommended way to hash several fields;String.GetHashCode()is randomised per process, so hash codes must never be stored or compared across runs.
The compiler does not help you here. Overriding Equals without GetHashCode, defining = without overriding Equals, or
the reverse, produces no diagnostic with the default analyzer settings of the SDK (checked: none of the three raises a
warning), unlike C# (CS0659, CS0660 and CS0661). The .NET code analysis rules do cover part of this: CA1066 (implement IEquatable(Of T) when overriding Equals), CA1067
(override Equals when implementing IEquatable(Of T)) and CA2231 (overload the equality operator when overriding
ValueType.Equals). Switch on the ones you want for equality-heavy code. See
XML Documentation, Analyzers and Code Style.
Equality for a class
A class has the same members, with three additions: the Nothing checks, NotInheritable (or a carefully designed
GetType() comparison) to avoid asymmetric equality with subclasses, and the decision whether equality is identity (default)
or entity/value equality. A catalogue Book is naturally identified by its ISBN:
Option Strict On
' Entity-style equality: two Book objects are the same book when their ISBN text is equal.
Public NotInheritable Class Book
Implements IEquatable(Of Book)
Public ReadOnly Property Isbn As String
Public Property Title As String
Public Sub New(isbn As String, title As String)
Me.Isbn = isbn
Me.Title = title
End Sub
Public Overloads Function Equals(other As Book) As Boolean Implements IEquatable(Of Book).Equals
If other Is Nothing Then Return False
If Object.ReferenceEquals(Me, other) Then Return True
Return String.Equals(Isbn, other.Isbn, StringComparison.Ordinal)
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return Equals(TryCast(obj, Book))
End Function
Public Overrides Function GetHashCode() As Integer
Return Isbn.GetHashCode()
End Function
' Operators on a class are called with Nothing too: never dereference blindly.
Public Shared Operator =(left As Book, right As Book) As Boolean
If left Is Nothing Then Return right Is Nothing
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As Book, right As Book) As Boolean
Return Not (left = right)
End Operator
End Class
Module Program
Sub Main()
Dim a As New Book("9780134685991", "Effective Java")
Dim b As New Book("9780134685991", "Effective Java, third edition")
Dim none As Book = Nothing
Console.WriteLine("a = b: " & (a = b) & " (different Title, same ISBN)")
Console.WriteLine("a Is b: " & (a Is b))
Console.WriteLine("a = Nothing: " & (a = Nothing))
Console.WriteLine("none = Nothing: " & (none = Nothing))
Console.WriteLine("Nothing = a: " & (Nothing = a))
Console.WriteLine("a.Equals(Nothing): " & a.Equals(Nothing))
Console.WriteLine("a.Equals(""9780134685991""): " & a.Equals("9780134685991"))
' Is Nothing never calls the operator, so it stays the safest null test.
Console.WriteLine("none Is Nothing: " & (none Is Nothing))
Dim shelf As New HashSet(Of Book) From {a, b}
Console.WriteLine("HashSet count: " & shelf.Count)
Console.WriteLine("Contains a copy: " & shelf.Contains(New Book("9780134685991", "x")))
End Sub
End Module
a = b: True (different Title, same ISBN)
a Is b: False
a = Nothing: False
none = Nothing: True
Nothing = a: False
a.Equals(Nothing): False
a.Equals("9780134685991"): False
none Is Nothing: True
HashSet count: 1
Contains a copy: True
Operator procedures on a class are called with Nothing operands, because a = Nothing binds to your operator, so the
first line of each comparison tests Nothing. Write x Is Nothing for null tests, never x = Nothing: with an overloaded =
the latter runs your code, and it is True only if your code says so. For a deep-dive on Nothing see
Nothing and Nullable Value Types.
Mutable members and hash codes
An object that is inside a hash-based collection must not change the state GetHashCode is computed from:
Option Strict On
' A deliberately flawed class: the hash code depends on a property that can change.
Public NotInheritable Class Borrower
Implements IEquatable(Of Borrower)
Public Property Name As String
Public Sub New(name As String)
Me.Name = name
End Sub
Public Overloads Function Equals(other As Borrower) As Boolean Implements IEquatable(Of Borrower).Equals
Return other IsNot Nothing AndAlso Name = other.Name
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return Equals(TryCast(obj, Borrower))
End Function
Public Overrides Function GetHashCode() As Integer
Return Name.GetHashCode()
End Function
End Class
Module Program
Sub Main()
Dim ana As New Borrower("Ana")
Dim members As New HashSet(Of Borrower) From {ana}
Dim byBorrower As New Dictionary(Of Borrower, Integer) From {{ana, 3}}
Console.WriteLine("before: Contains = " & members.Contains(ana))
ana.Name = "Ana Maria" ' the key changed while it was inside the collections
Console.WriteLine("after: Contains = " & members.Contains(ana))
Console.WriteLine("after: Remove = " & members.Remove(ana))
Console.WriteLine("count is still " & members.Count)
Console.WriteLine("dictionary lookup: " & byBorrower.ContainsKey(ana))
Console.WriteLine("but enumeration finds it: " & members.Any(Function(m) m Is ana))
Console.WriteLine("and a linear scan agrees: " & members.ToList().Contains(ana))
End Sub
End Module
before: Contains = True
after: Contains = False
after: Remove = False
count is still 1
dictionary lookup: False
but enumeration finds it: True
and a linear scan agrees: True
Source: Object.GetHashCode Method
The set still has the object (count 1, enumeration finds it) but cannot find it by lookup, so Contains and Remove both
fail, and the object can no longer be removed except by clearing. The hash must be computed from immutable state: ReadOnly
properties, set in the constructor. If the identity can legitimately change, do not use the type as a key: key by an
immutable ID instead. Collection types are on
Collections.
Structures and boxing
A structure that does not implement IEquatable(Of T) pays for boxing on every comparison made through
EqualityComparer(Of T).Default. The program counts allocations, not time, so the result is deterministic:
Option Strict On
' No overrides: equality comes from ValueType.Equals and boxes the argument.
Public Structure PlainCopy
Public Isbn As String
Public Copy As Integer
End Structure
' IEquatable(Of T) + overrides: EqualityComparer(Of T).Default compares without boxing.
Public Structure FastCopy
Implements IEquatable(Of FastCopy)
Public Isbn As String
Public Copy As Integer
Public Overloads Function Equals(other As FastCopy) As Boolean Implements IEquatable(Of FastCopy).Equals
Return Isbn = other.Isbn AndAlso Copy = other.Copy
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return TypeOf obj Is FastCopy AndAlso Equals(DirectCast(obj, FastCopy))
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(Isbn, Copy)
End Function
End Structure
Module Program
Function Allocated(Of T)(items As List(Of T), probe As T) As Long
items.Contains(probe) ' warm up: JIT, caches
Dim before = GC.GetAllocatedBytesForCurrentThread()
For i = 1 To 1000
items.Contains(probe)
Next
Return GC.GetAllocatedBytesForCurrentThread() - before
End Function
Sub Main()
Dim plain As New List(Of PlainCopy)
Dim fast As New List(Of FastCopy)
For i = 1 To 20
plain.Add(New PlainCopy With {.Isbn = "9780134685991", .Copy = i})
fast.Add(New FastCopy With {.Isbn = "9780134685991", .Copy = i})
Next
Console.WriteLine("PlainCopy.Contains finds copy 20: " & plain.Contains(New PlainCopy With {.Isbn = "9780134685991", .Copy = 20}))
Console.WriteLine("FastCopy.Contains finds copy 20: " & fast.Contains(New FastCopy With {.Isbn = "9780134685991", .Copy = 20}))
Dim plainBytes = Allocated(plain, New PlainCopy With {.Isbn = "9780134685991", .Copy = 99})
Dim fastBytes = Allocated(fast, New FastCopy With {.Isbn = "9780134685991", .Copy = 99})
Console.WriteLine("PlainCopy allocates while comparing: " & (plainBytes > 0))
Console.WriteLine("FastCopy allocates while comparing: " & (fastBytes > 0))
End Sub
End Module
PlainCopy.Contains finds copy 20: True
FastCopy.Contains finds copy 20: True
PlainCopy allocates while comparing: True
FastCopy allocates while comparing: False
Source: ValueType.Equals Method
The same applies to enumerations of structures in Dictionary and HashSet keys. A structure with only one or two
fields, used as a collection key, should always implement IEquatable(Of T).
Tuples and anonymous types as shortcuts
Visual Basic has no record declaration to generate equality for you, but two built-in forms provide it. ValueTuple
compares element by element and hashes accordingly, and an anonymous type compares only its Key properties:
Option Strict On
Public NotInheritable Class Edition
Implements IEquatable(Of Edition)
Public ReadOnly Property Isbn As String
Public ReadOnly Property Number As Integer
Public Property Note As String = ""
Public Sub New(isbn As String, number As Integer)
Me.Isbn = isbn
Me.Number = number
End Sub
' ValueTuple does the field-by-field comparison and the hashing for us.
Public Overloads Function Equals(other As Edition) As Boolean Implements IEquatable(Of Edition).Equals
Return other IsNot Nothing AndAlso (Isbn, Number).Equals((other.Isbn, other.Number))
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return Equals(TryCast(obj, Edition))
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(Isbn, Number)
End Function
End Class
Module Program
Sub Main()
Dim a As New Edition("9780134685991", 3) With {.Note = "reprint"}
Dim b As New Edition("9780134685991", 3)
Console.WriteLine("a.Equals(b): " & a.Equals(b) & ", same hash: " & (a.GetHashCode() = b.GetHashCode()))
' Tuples compare element by element with Equals.
Dim t1 = (Id:=1, Title:="Refactoring")
Dim t2 = (Id:=1, Title:="Refactoring")
Console.WriteLine("t1.Equals(t2): " & t1.Equals(t2))
Console.WriteLine("t1.Equals((1, ""Refactoring"")): " & t1.Equals((1, "Refactoring")))
Console.WriteLine("t1.CompareTo(t2): " & t1.CompareTo(t2))
' Anonymous types: only Key properties take part in Equals and GetHashCode.
Dim k1 = New With {Key .Isbn = "9780134685991", .Note = "a"}
Dim k2 = New With {Key .Isbn = "9780134685991", .Note = "b"}
Console.WriteLine("anonymous Key equality: " & k1.Equals(k2) & ", " & (k1.GetHashCode() = k2.GetHashCode()))
Dim n1 = New With {.Isbn = "9780134685991"}
Dim n2 = New With {.Isbn = "9780134685991"}
Console.WriteLine("without Key: " & n1.Equals(n2))
End Sub
End Module
a.Equals(b): True, same hash: True
t1.Equals(t2): True
t1.Equals((1, "Refactoring")): True
t1.CompareTo(t2): 0
anonymous Key equality: True, True
without Key: False
Source: Anonymous Types
Using a tuple inside Equals is a compact, correct implementation as long as the elements are themselves comparable. There
is no tuple = operator, so t1 = t2 is rejected (see the error list). For these two features see
Tuples and Anonymous Types.
Ordering with IComparable and comparers
IComparable(Of T) gives a type its natural order, returning a negative number, zero or a positive number. It is what
List.Sort(), SortedSet, Array.Sort and LINQ OrderBy/Max/Min with no comparer use. The conventions:
-
Any instance sorts after
Nothing;CompareTo(Nothing)returns a positive number rather than throwing. -
CompareTo(other) = 0should agree withEquals; where it does not (case-insensitive ordering, say) document it, becauseSortedSetwould then collapse values thatHashSetkeeps apart. -
Implement the non-generic
IComparableas well only when old code (ArrayList,Array.Sort(Array)) must be supported, and make it throwArgumentExceptionfor other types. -
The four relational operators are declared as two pairs and written with one shared
Comparehelper, so thatNothinghandling cannot drift.
Option Strict On
Public NotInheritable Class Issue
Implements IComparable(Of Issue), IComparable, IEquatable(Of Issue)
Public ReadOnly Property Year As Integer
Public ReadOnly Property Number As Integer
Public Sub New(year As Integer, number As Integer)
Me.Year = year
Me.Number = number
End Sub
' Convention: any instance sorts after Nothing; two Nothings are equal.
Public Function CompareTo(other As Issue) As Integer Implements IComparable(Of Issue).CompareTo
If other Is Nothing Then Return 1
Dim byYear = Year.CompareTo(other.Year)
Return If(byYear <> 0, byYear, Number.CompareTo(other.Number))
End Function
' The non-generic interface is used by ArrayList, Array.Sort(Array) and old APIs.
Private Function CompareToObject(obj As Object) As Integer Implements IComparable.CompareTo
If obj Is Nothing Then Return 1
Dim other = TryCast(obj, Issue)
If other Is Nothing Then
Throw New ArgumentException("Object must be of type Issue.", NameOf(obj))
End If
Return CompareTo(other)
End Function
Public Overloads Function Equals(other As Issue) As Boolean Implements IEquatable(Of Issue).Equals
Return other IsNot Nothing AndAlso CompareTo(other) = 0
End Function
Public Overrides Function Equals(obj As Object) As Boolean
Return Equals(TryCast(obj, Issue))
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(Year, Number)
End Function
Public Overrides Function ToString() As String
Return Year & "/" & Number
End Function
' The operators handle Nothing through a shared helper so that the rules cannot drift.
Private Shared Function Compare(left As Issue, right As Issue) As Integer
If left Is Nothing Then Return If(right Is Nothing, 0, -1)
Return left.CompareTo(right)
End Function
Public Shared Operator <(left As Issue, right As Issue) As Boolean
Return Compare(left, right) < 0
End Operator
Public Shared Operator >(left As Issue, right As Issue) As Boolean
Return Compare(left, right) > 0
End Operator
Public Shared Operator <=(left As Issue, right As Issue) As Boolean
Return Compare(left, right) <= 0
End Operator
Public Shared Operator >=(left As Issue, right As Issue) As Boolean
Return Compare(left, right) >= 0
End Operator
Public Shared Operator =(left As Issue, right As Issue) As Boolean
Return Compare(left, right) = 0
End Operator
Public Shared Operator <>(left As Issue, right As Issue) As Boolean
Return Compare(left, right) <> 0
End Operator
End Class
Module Program
Sub Main()
Dim issues As New List(Of Issue) From {
New Issue(2025, 11), New Issue(2024, 3), Nothing, New Issue(2025, 2), New Issue(2024, 3)
}
issues.Sort()
Console.WriteLine("List.Sort: " & String.Join(" ", issues.Select(Function(i) If(i Is Nothing, "(none)", i.ToString()))))
Dim distinct As New SortedSet(Of Issue)(issues.Where(Function(i) i IsNot Nothing))
Console.WriteLine("SortedSet count: " & distinct.Count & " (duplicates collapse via CompareTo)")
Console.WriteLine("Min / Max: " & distinct.Min.ToString() & " / " & distinct.Max.ToString())
Dim a As New Issue(2024, 3)
Dim b As New Issue(2025, 2)
Console.WriteLine("a < b: " & (a < b) & ", a >= b: " & (a >= b) & ", a = New Issue(2024, 3): " & (a = New Issue(2024, 3)))
Console.WriteLine("Nothing < a: " & (Nothing < a))
' The non-generic interface rejects other types.
Dim legacy As IComparable = a
Try
legacy.CompareTo("2024/3")
Catch ex As ArgumentException
Console.WriteLine("IComparable.CompareTo(String): " & ex.Message)
End Try
' Enumerable.OrderBy, Max and Min use Comparer(Of Issue).Default, i.e. IComparable(Of Issue).
Dim newest = New Issue() {a, b}.Max()
Console.WriteLine("Enumerable.Max: " & newest.ToString())
End Sub
End Module
List.Sort: (none) 2024/3 2024/3 2025/2 2025/11
SortedSet count: 3 (duplicates collapse via CompareTo)
Min / Max: 2024/3 / 2025/11
a < b: True, a >= b: False, a = New Issue(2024, 3): True
Nothing < a: True
IComparable.CompareTo(String): Object must be of type Issue. (Parameter 'obj')
Enumerable.Max: 2025/2
Source: IComparable<T> Interface
For several orderings use comparer objects, IComparer(Of T) for order and IEqualityComparer(Of T) for equality and
hashing. They are passed to List.Sort, OrderBy, SortedSet, Distinct, Dictionary and HashSet; Comparer(Of T).Create
and a plain lambda are enough for one-off orderings:
Option Strict On
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
Public Overrides Function ToString() As String
Return Title & " (" & Year & ")"
End Function
End Class
' A comparer is a separate object: one type can have many orderings.
Public NotInheritable Class ByTitleComparer
Implements IComparer(Of Book)
Public Function Compare(x As Book, y As Book) As Integer Implements IComparer(Of Book).Compare
If x Is y Then Return 0
If x Is Nothing Then Return -1
If y Is Nothing Then Return 1
Return StringComparer.OrdinalIgnoreCase.Compare(x.Title, y.Title)
End Function
End Class
Public NotInheritable Class ByTitleEquality
Implements IEqualityComparer(Of Book)
Public Overloads Function Equals(x As Book, y As Book) As Boolean Implements IEqualityComparer(Of Book).Equals
If x Is y Then Return True
If x Is Nothing OrElse y Is Nothing Then Return False
Return StringComparer.OrdinalIgnoreCase.Equals(x.Title, y.Title)
End Function
Public Overloads Function GetHashCode(obj As Book) As Integer Implements IEqualityComparer(Of Book).GetHashCode
Return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Title)
End Function
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("refactoring", 1999), New Book("Domain-Driven Design", 2003),
New Book("Refactoring", 2018), New Book("Clean Code", 2008)
}
books.Sort(New ByTitleComparer())
Console.WriteLine("IComparer: " & String.Join("; ", books))
books.Sort(Function(x, y) y.Year.CompareTo(x.Year))
Console.WriteLine("Comparison: " & String.Join("; ", books))
Dim byYear = Comparer(Of Book).Create(Function(x, y) x.Year.CompareTo(y.Year))
Console.WriteLine("Comparer.Create: " & String.Join("; ", books.OrderBy(Function(b) b, byYear)))
Console.WriteLine("OrderBy/ThenBy: " & String.Join("; ", books.OrderBy(Function(b) b.Title, StringComparer.OrdinalIgnoreCase).ThenByDescending(Function(b) b.Year)))
Console.WriteLine("Distinct by title: " & books.Distinct(New ByTitleEquality()).Count() & " of " & books.Count)
Dim index As New Dictionary(Of Book, Integer)(New ByTitleEquality())
For Each b In books
index(b) = b.Year
Next
Console.WriteLine("Dictionary keys: " & index.Count)
' Default equality for a class without overrides is reference equality.
Console.WriteLine("default Distinct: " & books.Distinct().Count())
Dim names As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) From {"Ana", "ANA", "Luis"}
Console.WriteLine("HashSet(Of String) with a comparer: " & names.Count)
End Sub
End Module
IComparer: Clean Code (2008); Domain-Driven Design (2003); refactoring (1999); Refactoring (2018)
Comparison: Refactoring (2018); Clean Code (2008); Domain-Driven Design (2003); refactoring (1999)
Comparer.Create: refactoring (1999); Domain-Driven Design (2003); Clean Code (2008); Refactoring (2018)
OrderBy/ThenBy: Clean Code (2008); Domain-Driven Design (2003); Refactoring (2018); refactoring (1999)
Distinct by title: 3 of 4
Dictionary keys: 3
default Distinct: 4
HashSet(Of String) with a comparer: 2
Source: IEqualityComparer<T> Interface
List.Sort is not stable (the two titles that compare equal came out in an arbitrary order in the first line), while
OrderBy is: use OrderBy/ThenBy when equal elements must keep their order. An IEqualityComparer(Of T) must keep the
same contract as Equals: whatever Equals says is equal must hash equally.
Generic math: what Visual Basic cannot do
INumber(Of T), IAdditionOperators(Of TSelf, TOther, TResult) and friends, so that a generic method can write a + b. C# 11
consumes and implements them. Visual Basic 17.13 does neither:
Option Strict On
Imports System.Numerics
Module Program
' Compiles in C# 11 and later; Visual Basic cannot use the operator through the constraint.
Function Total(Of T As IAdditionOperators(Of T, T, T))(a As T, b As T) As T
Return a + b
End Function
Sub Main()
Console.WriteLine(Total(1, 2))
End Sub
End Module
Program.vb(8,16): error BC30452: Operator '+' is not defined for types 'T' and 'T'.
Source: Generic math
and a type cannot opt in either:
Option Strict On
Imports System.Numerics
Public Structure Meters
Implements IAdditionOperators(Of Meters, Meters, Meters)
Public ReadOnly Value As Double
Public Sub New(value As Double)
Me.Value = value
End Sub
Public Shared Operator +(left As Meters, right As Meters) As Meters
Return New Meters(left.Value + right.Value)
End Operator
End Structure
Module Program
Sub Main()
Console.WriteLine((New Meters(1) + New Meters(2)).Value)
End Sub
End Module
Program.vb(6,16): error BC37315: Structure 'Meters' cannot implement interface 'IAdditionOperators(Of Meters, Meters, Meters)' because it contains shared abstract or virtual 'Function op_Addition(left As Meters, right As Meters) As Meters'.
Program.vb(6,16): error BC37315: Structure 'Meters' cannot implement interface 'IAdditionOperators(Of Meters, Meters, Meters)' because it contains shared abstract or virtual 'Function op_CheckedAddition(left As Meters, right As Meters) As Meters'.
Calling library APIs that use generic math internally is fine, for example Enumerable.Sum on built-in numeric types; what
fails is declaring a generic method over the operator interfaces and implementing those interfaces. The VB-side workaround
is to pass the operation in as a delegate (a Func(Of T, T, T)), or to use Aggregate with a lambda:
Option Strict On
Public Structure Meters
Public ReadOnly Value As Double
Public Sub New(value As Double)
Me.Value = value
End Sub
Public Shared Operator +(left As Meters, right As Meters) As Meters
Return New Meters(left.Value + right.Value)
End Operator
End Structure
Module Program
' Visual Basic cannot call "a + b" through a generic-math constraint,
' so pass the operation in as a delegate.
Function Total(Of T)(items As IEnumerable(Of T), zero As T, add As Func(Of T, T, T)) As T
Dim result = zero
For Each item In items
result = add(result, item)
Next
Return result
End Function
Sub Main()
' The BCL's own generic-math based Sum works fine with the built-in numeric types.
Console.WriteLine(Enumerable.Sum(New Integer() {1, 2, 3}))
Console.WriteLine(New Double() {1.5, 2}.Sum())
Dim lengths = {New Meters(1), New Meters(2.5), New Meters(3)}
Dim sum = Total(lengths, New Meters(0), Function(a, b) a + b)
Console.WriteLine(sum.Value)
Console.WriteLine(lengths.Aggregate(Function(a, b) a + b).Value)
End Sub
End Module
6
3.5
6.5
6.5
Source: Aggregate Clause
A C# library that offers generic math over its own types is usable from Visual Basic only through the concrete operators
those types also declare (+, =, …); the interface constraint itself cannot be satisfied or called from Visual Basic.
Turning Option Strict off and late binding (Operators.AddObject) can add two Object values, but give up compile-time checking
and are not used in this section (see
Early and Late Binding).
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Declaration |
|
|
Equality operator |
|
|
Missing |
no diagnostic by default |
warnings CS0659, CS0660, CS0661 |
Null test that skips the overload |
|
|
|
not declarable; use classes with |
value equality, |
Operators with no counterpart |
|
|
|
derived from |
|
Conversions |
|
|
|
never use conversion operators |
the |
Static abstract interface members (generic math) |
cannot declare, implement or call (BC37315, BC30452) |
supported since C# 11 |
Operator in an interface |
not possible (BC30603) |
static abstract members since C# 11 |
|
case-sensitive unless |
always ordinal |
|
rewritten as |
same; since C# 14 a user-defined |
Older material you may meet
Tutorials and the books this section was compared with often predate several of these points:
-
Examples that sort with the non-generic
IComparableandArrayList, or implement onlyEquals(Object): they still work, but useIComparable(Of T),IEquatable(Of T)and generic collections today, and make the structure’sEquals(Object)call the typed overload to avoid boxing. -
"Operator overloading" chapters that stop at
+and=. A type that defines=must also define<>, and should defineEquals,GetHashCodeand theIEquatable(Of T)implementation to match; the compiler will not remind you. -
VB6 code compared objects with
Isand relied on default properties andVariantcomparison for=. In .NET,=on two class-typed operands with no operator is a compile error (BC30452), andObject = Objectis BC32013 underOption Strict On. -
Hash-code advice that XORs fields (
a.GetHashCode() Xor b.GetHashCode()), which gives equal hashes for swapped values, predatesHashCode.Combine. -
Samples that compare Strings with
=and assume the project is case-sensitive, ignoringOption Compare.
Common pitfalls
| Pitfall | What to do |
|---|---|
|
override both, from the same immutable fields; there is no compiler warning |
|
declare all pairs; implement |
Operator on a class dereferences |
first line: |
Using |
use |
Derived class declares |
put the logic in virtual |
Implicit |
prefer |
|
use |
|
compare values; Visual Basic copies boxes on assignment |
Mutating a key inside a |
make the hashed state |
|
not in the language; write the expression out |
Generic |
pass a |
Relying on |
use |
Comparing |
use |
References
-
How to: Define an Operator, How to: Define a Conversion Operator, How to: Call an Operator Procedure and How to: Use a Class that Defines Operators
-
IsTrue Operator, Is Operator, Like Operator, Comparison Operators, CType Function and DirectCast Operator
-
For…Next Statement, Select…Case Statement, Option Compare Statement and Overloads
-
Object.Equals, Object.GetHashCode, ValueType.Equals, HashCode and RuntimeHelpers.GetObjectValue
-
IEquatable<T>, IComparable<T>, IComparable, IEqualityComparer<T>, IComparer<T>, EqualityComparer<T> and Comparer<T>
-
Equality Operators and Operator Overloads (.NET Framework Design Guidelines)