Obviously C# is not C++, and so it is important to not simply assume that things true in C++ are also true in C#. A good question is the cost of the is operator compared to a virtual (or abstract) function. Is a visitor better than casting? This is pretty easy to test, but I couldn’t find any good post about this, so I wrote it myself.

A few things should affect the result. Most importantly, the speed of the cast approach depends on the number of if tests you have to do, or equivalently, the number of failed tests. The more classes you have to test, the slower things should be. The result might also depend on class hierarchy depth. To test this out, I designed the following class hierarchy.

I then wrote to code to either cast via is or call a visitor function. Everything except the Circle class can be entirely removed from the code by conditional compiles. This way, I can test the effects of adding more classes or increating the class hierarchy depth and this is detailed in the table below.

ClassesVisitor Time (ms)Visitor AverageCast Time (ms)Cast AverageFastest
Circle

1218
1233
1176

0.12

798
802
807

0.08

Cast

Circle
Square

2259
2242
2520

0.12

1890
1898
1903

0.09

Cast

Circle
Square
RoundedSquare

3339
3337
3305

0.11

3769
3780
3766

0.13

Visitor

Circle
Square
Hexagon

3286
3581
3301

0.11

3693
3718
3681

0.12

Visitor

Circle
Square
RoundedSquare
Hexagon

4501
4592
4649

0.11

5703
5672
5662

0.14

Visitor

Circle
Square
RoundedSquare
Hexagon
Octagon

5541
5649
5534

0.11

7920
7950
7958

0.16

Visitor

Times increase with each addition because the total number of objects increases, so the important number is the average. The loop is also run 10000 times so that the times are much larger than the timer resolution of 1 ms.

Overall, the results are not that unexpected. As you add more types, the number of failed if’s increases, and the performance degredates. The visitor stays the same. Interestingly, at the lowest end, the cast is faster than the visitor, and so for simple checks, the better choice might be the cast.

You can see the full source below.

// Comment the conditional compile lines below to
// remove particular types from the object hierarchy
#define SQUARE
#define ROUNDEDSQUARE
#define HEXAGON
#define OCTAGON

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace VisitorVsCast
{
/// <summary>
/// The visitor interface
/// </summary>
public interface IVisitor
{
void VisitCircle(Circle circle);
#if SQUARE
void VisitSquare(Square square);
#endif
#if ROUNDEDSQUARE
void VisitRoundedSquare(RoundedSquare square);
#endif
#if HEXAGON
void VisitHexagon(Hexagon hexagon);
#endif
#if OCTAGON
void VisitOctagon(Octagon octagon);
#endif
}

/// <summary>
/// Base class for all shapes
/// </summary>
public abstract class Shape
{
public abstract void AcceptVisitor(IVisitor visitor);
}

/// <summary>
/// A circle
/// </summary>
public class Circle : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitCircle(this);
}
}

#if SQUARE
/// <summary>
/// A square shape
/// </summary>
public class Square : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitSquare(this);
}
}
#endif

#if ROUNDEDSQUARE
/// <summary>
/// A rounded square (a square with round corners)
/// </summary>
public class RoundedSquare : Square
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitRoundedSquare(this);
}
}
#endif

#if HEXAGON
public class Hexagon : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitHexagon(this);
}
}
#endif

#if OCTAGON
public class Octagon : Shape
{
public override void AcceptVisitor(IVisitor visitor)
{
visitor.VisitOctagon(this);
}
}
#endif

/// <summary>
/// Counting visitor
/// </summary>
public class CountingVisitor : IVisitor
{

public void VisitCircle(Circle circle)
{
}

#if SQUARE
public void VisitSquare(Square square)
{
}
#endif

#if ROUNDEDSQUARE
public void VisitRoundedSquare(RoundedSquare square)
{
}
#endif

#if HEXAGON
public void VisitHexagon(Hexagon hexagon)
{
}
#endif

#if OCTAGON
public void VisitOctagon(Octagon octagon)
{
}
#endif
}

class Program
{
static int NumObjectsPerCategory = 10000;
static int NumIterations = 10000;

static void Main(string[] args)
{
long visitorTime = 0;
long castTime = 0;

// Create a list with lots of shapes. It doesn't matter the order
// of the shapes, but that we have a bunch
List<Shape> shapes = new List<Shape>();
shapes.Capacity += 5 * NumObjectsPerCategory;
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
}
#if SQUARE
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
}
#endif
#if ROUNDEDSQUARE
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
}
#endif
#if HEXAGON
for (int i = 0; i < NumObjectsPerCategory; ++i)
{
}
#endif
#if OCTAGON
for (int i = 0; i < 10000; ++i)
{
}
#endif

visitorTime = CountByVisitor(shapes);
castTime = CountByCast(shapes);

Console.WriteLine("Visitor: {0}", visitorTime);
Console.WriteLine("Cast: {0}", castTime);
}

/// <summary>
/// Iterate them by the visitor
/// </summary>
/// <param name="shapes"></param>
/// <returns></returns>
static long CountByVisitor(List<Shape> shapes)
{
CountingVisitor visitor = new CountingVisitor();

Stopwatch sw = Stopwatch.StartNew();

for (int i = 0; i < NumIterations; ++i)
{
foreach (Shape shape in shapes)
{
shape.AcceptVisitor(visitor);
}
}

return sw.ElapsedMilliseconds;
}

/// <summary>
/// Iterate them by cast
/// </summary>
/// <param name="shapes"></param>
/// <returns></returns>
static long CountByCast(List<Shape> shapes)
{
Stopwatch sw = Stopwatch.StartNew();

for (int i = 0; i < NumIterations; ++i)
{
foreach (Shape shape in shapes)
{
if (shape is Circle)
{
}
#if ROUNDEDSQUARE
else if (shape is RoundedSquare)
{
}
#endif
#if SQUARE
else if (shape is Square)
{
}
#endif
#if HEXAGON
else if (shape is Hexagon)
{
}
#endif
#if OCTAGON
else if (shape is Octagon)
{
}
#endif
}
}

return sw.ElapsedMilliseconds;
}

}
}