public static Point operator +(Point a, Point b) { ... }
.public float this[int index] { get { ... } set { ... }}
.public static explicit operator Point3(Point input) { ... }
.You may have noticed, by now, that some of the types you use seem to have a few more capabilities than what we’ve been able to do on our own so far. For example:
int
type lets you do addition: int a = b + 17;
. The string
type has it as well: "Hello, " + name + "!"
.numbers[index] = -2000;
.float f = (float)someInteger;
.Are those powers our own types can learn? Or is it reserved for special types?
Turns out, we can do similar things on our own types! That is the focus of this tutorial: operator overloading, indexers, and custom conversions.
C# defines a whole lot of operators, from =
(assignment) to +
, to &&
and so on.
For some (but not all) of these operators, you can define how the operator should work for your own type.
I know we’ve overused the example of a Point
type in these tutorials, but we’re going to use it here, once again, because it is such a natural fit.
So let’s assume we’ve already got a basic Point
class:
class Point
{
public float X { get; }
public float Y { get; }
public Point(float x, float y)
{
X = x; Y = y;
}
}
There’s a very clearly defined way to interpret adding points together (though mathematically, perhaps you can also consider it as adding a vector to a point, if that makes more sense to you).
For a point at (2, 3), if you add (4, 4) to it, the resulting point is (2+4, 3+4) or (6, 7).
We could, of course, make an Add
method that does this:
public static Point Add(Point a, Point b)
{
return new Point(a.X + b.X, a.Y + b.Y);
}
Then use it like this:
Point p1 = new Point(2, 3);
Point p2 = new Point(4, 4);
Point result = Point.Add(p1, p2);
note
We could have also made this a normal instance method (not a static
method) and not passed in two parameters, but one.
public Point Add(Point other)
{
return new Point(X + other.X, Y + other.Y);
}
Which would get used like this:
p1.Add(p2);
You may feel like that’s a more natural usage style, but I chose to make it static to mirror the operator overload we will make in a moment.
However, in this case, using the +
operator would just plain feel simpler.
To define how the +
operator should work for your custom-made type, you define an operator overload
like this:
public static Point operator +(Point a, Point b)
{
return new Point(a.X + b.X, a.Y + b.Y);
}
The only difference between this and the original method is that this uses the operator
keyword, and then uses +
where the name would typically go.
Operator overloads must be both public
and static
, and they also must be placed inside of the type indicated by one of the parameters.
In this case, that means that this operator must live inside the Point
class, because both parameters use that type.
But that tips us off to something useful: you can mix types in your operator overloads.
For example, we could imagine being able to use *
to “scale” a point or set of points up or down.
Even in the math world, taking a point or vector and multiplying it by a single number (what the math people call a scalar) has a clear definition: (2, 3) * 2
becomes (2*2, 3*2)
or (4, 6)
.
We can overload the *
operator to include this:
public static Point operator *(Point p, float scalar)
{
return new Point(p.X * scalar, p.Y * scalar);
}
Which allows us to do this:
Point p = new Point(2, 3);
p = p * 2;
Notably, it does not allow this:
Point q = 2 * p;
That is because the order is reversed! For some math operations, the order matters. For others, it does not. (This last one is called the commutative property in math.)
If you want to be able to do both 2 * p
and p * 2
, you need to define the *
operator with both orderings.
I usually do that by picking one order to be the “real” implementation, and having the other just call it by swapping the order:
public static Point operator *(float scalar, Point p)
{
return p * scalar; // Swap the order to call the other operator overload.
}
Not every operator can be overloaded.
The typical math operators can be (+
, -
, *
, /
, %
, as well as unary +
and -
).
You can also overload the relational operators (>
, <
, >=
, <=
, ==
, and !=
) but they must be done in pairs.
If you overload <
, you must also overload >
, for example.
Also, you cannot directly overload the compound assignment operators like +=
and *=
, but by overloading +
and *
, those will be made to work with your own overloads automatically.
You can’t overload the indexing operator ([]
) in this way, but we’ll see how to make that happen in the next section.
You cannot use operator overloads to define new operators.
We don’t overload the indexing operator ([]
) in the traditional way, but we can still define how indexing works with our type.
Let’s say we make a class like the following:
class NumberTriplet
{
public float First { get; set; }
public float Second { get; set; }
public float Third { get; set; }
public NumberTriplet(float first, float second, float third)
{
First = first;
Second = second;
Third = third;
}
}
It might be nice to be able to look these up not just by name but by index.
This can be done by defining an indexer
in the NumberTriplet
type:
public float this[int index]
{
get
{
if (index == 0) return First;
else if (index == 1) return Second;
else return Third;
}
set
{
if (index == 0) First = value;
else if (index == 1) Second = value;
else Third = value;
}
}
Perhaps you are looking at that and saying, “Wow, that looks a lot like a property!”
Indeed, some people call indexers parameterful properties, because they effectively are properties, just with extra parameters (like the index
variable).
Just like a property, you can include the get or set, as you have a need.
If we had wanted to make First
, Second
, and Third
be read-only, we could have left of the set
part of this indexer.
Note that value
is available in the setter, and the parameter index
is available in both getter and setter.
There is nothing special about the name index
, nor are we limited to the int
type.
We could have had a parameter that is a string name
instead, had that been desired.
Also, note that you can put many parameters in that list, not just one.
If you were doing something with a 2D grid of values, you could make an operator that looks like public float this[int row, int column]
, and had access to both of those parameters in the getter and setter.
The last thing we’ll cover is defining custom conversions.
We see conversions happen both automatically and on-demand with other types, like between int
and short
, for example:
int a = 200;
short b = 200;
a = b; // Converted "implicitly" or automatically.
b = (short)a; // Converted "explicitly"--the programmer has to call it out.
Let’s suppose we added in the following Point3
to the mix with our original 2D Point
class:
class Point3
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public Point3(float x, float y, float z)
{
X = x; Y = y; Z = z;
}
}
It would make sense to be able to turn a Point
, which has X
and Y
, into a Point3
automatically.
We can do that by adding the following custom conversion to either the Point
or Point3
class (it must be in one or the other, since those are the types involved in the conversion):
public static implicit operator Point3(Point p)
{
return new Point3(p.X, p.Y, 0);
}
This looks similar to a normal operator overload (it even includes that operator
keyword) but includes the implicit
keyword, and does not include a return type, though the “name” of the custom conversion will be the expected return type.
So this conversion must return a Point3
, given the parameter Point p
.
With this added to either the Point
or Point3
class, we can do the following:
Point p = new Point(2, 3);
Point3 p3 = p; // Automatically calls our custom conversion.
The conversion happens automatically, without needing to do (Point3)p
, because of that implicit
keyword we used when defining the operator.
This conversion happening automatically makes sense.
We don’t lose any data, and just fill in a z-value of 0
, which is a great default.
Going the other way, we would actually lose any z-value that is in there. The conversion is more dangerous, because it loses important information.
When we might lose important data, it is better for the conversion to be explicit.
So we define our Point3
to Point
conversion like this:
public static explicit operator Point(Point3 p)
{
return new Point(p.X, p.Y);
}
Mechanically, it is the same, it just uses the explicit
keyword instead of the implicit
keyword.
When we go to do the conversion, we have to write out a cast:
Point3 p3 = new Point3(2, 3, 4);
Point p = (Point3)p3;