Welcome to the world of implicit operators in C#! Implicit operators are a powerful feature of the C# language that has the ability to make our code more readable and expressive. In the wrong hands, they can allow for some really tricky behavior that’s hard to notice unless you go investigating. Implicit operators can allow us to
Implicit operators are defined using the implicit
keyword and can be used to create a method that converts one type to another. Two key rules of implicit operators are that they should not throw exceptions and should not lose information, as the conversion is done automatically by the compiler. This is because the compiler needs to be able to guarantee that the conversion can always succeed without any potential for data loss or exceptions.
Implicit operators are special methods we can define in our classes to allow for automatic conversion to another type. They are defined using the implicit
keyword. This keyword is used to modify the operator keyword in the method signature, indicating that the operator will be applied implicitly.
Here’s a simple example:
public readonly record struct Meter
{
public double Length { get; init; }
public static implicit operator Meter(double length)
{
return new Meter { Length = length };
}
}
// Usage
Meter meter = 10.0;
Console.WriteLine(meter); // Meter { Length = 10 }
In this example, we’ve defined an implicit operator that allows us to convert a double
to a Meter
object. This is done without any explicit casting or conversion in the code, hence the term “implicit operator”. The compiler automatically applies the operator when it’s needed, and we don’t need to explicitly cast with (double)
or (meter)
in front of our variables.
Implicit operators can be incredibly powerful. They allow us to create code that is more expressive and easier to read. By defining implicit operators, we can create types that feel like a natural part of the language, rather than something that’s been bolted on. This can be a popular solution for looking into strongly typed identifiers as well when you want to create types that naturally fit into the domain you’re working in.
For example, consider a Money
type. Without implicit operators, working with a Money
type might look something like this:
Money m = new Money(10.0m);
decimal amount = m.Amount;
But with implicit operators, we can make this code much more natural and intuitive:
Money m = 10.0m;
decimal amount = m;
This might seem like a small change, but it can make a big difference in the readability of our code. And given that we, as software developers, spend much of our time reading code compared to writing it… Readability should be optimized for!
Implicit operators can be used to improve the readability of your code and to create more intuitive APIs. They are particularly useful when working with domain-specifics or when you want to provide a simple interface for working with complex types. I even used them for creating custom return types that make my exception handling much easier!
For example, you could define implicit operators for a Money
type that allows you to work with money values as if they were numbers:
public readonly record struct Money
{
public decimal Amount { get; init; }
public static implicit operator Money(decimal amount)
{
return new Money { Amount = amount };
}
public static implicit operator decimal(Money money)
{
return money.Amount;
}
}
// Usage
Money money = 10.0m;
decimal amount = money;
In this example, we can assign a decimal
to a Money
object and vice versa, making the Money
type feels more like a natural part of the domain you’re working in.
While implicit operators are powerful, they should be used judiciously. Overuse of implicit operators can lead to code that is difficult to understand and maintain. That sounds counter to what we said earlier, right? Well, the code may be easy to read and this allows us to make assumptions about what is happening. However, if the actual behavior doesn’t line up with our expectations about what we’re reading, suddenly we have a much worse problem than hard-to-read code! It’s also important to remember that implicit operators should not throw exceptions or lose information, which ties into this point exactly.
Here’s an example of a more advanced usage of implicit operators, where we define an implicit operator for a Complex
number type:
public readonly record struct Complex
{
public double Real { get; init; }
public double Imaginary { get; init; }
public static implicit operator Complex(double real)
{
return new Complex { Real = real };
}
public static implicit operator Complex((double real, double imaginary) tuple)
{
return new Complex { Real = tuple.real, Imaginary = tuple.imaginary };
}
}
// Usage
Complex complex1 = 10.0;
Complex complex2 = (10.0, 20.0);
In this example, we can assign a double
or a tuple to a Complex
object, providing a flexible interface for creating complex numbers. The options for this become seemingly endless when you think about all of the different domains that exist. What about something for working with quantities and doing unit conversions?!
While implicit operators can be incredibly useful, they can also be a source of confusion if not used carefully. One of the main things to avoid is creating implicit operators that can result in data loss or that can throw exceptions. Because implicit operators are applied automatically by the compiler, any exceptions they throw can be difficult to trace.
For example, if we were to have an invalid cast exception thrown because of an improper type conversion operation, we’d be used to seeing a line of code with an explicit cast operation. We’d see in parentheses the type we are trying to cast to being applied to a variable or value and know right away there was some type of incompatibility as suggested by the exception pointing to that line number. With implicit operators, the conversion happens “magically” behind the scenes for us so if there’s any sort of error as a result of the operator executing:
We not only have to pause to recognize there’s some type of implicit operator being applied (i.e. “Wait a second, how is a double value being assigned to this type that is not a double?!”)
We then have to go into that type’s code (let’s hope we have the source for it!) and then go diagnose what’s wrong with the logic.
Another thing to avoid is creating implicit operators that can result in unexpected behavior. For example, if you create an implicit operator that converts a string
to an int
, it might be unclear whether the conversion is parsing the string as a number or getting the length of the string. Remember from earlier we touched on readability being paramount? Well, this remains true. But if the behavior does not align with what the reader is understanding from the code in front of them, then this will create a lot of confusion.
Congratulations! You’ve just learned about implicit operators in C#, a powerful feature that can make your code more readable and expressive. Remember, while implicit operators are powerful, they should be used with care. Always ensure that your implicit operators do not throw exceptions or lose information. And most importantly, make sure the behavior of your operators is aligned with how the reader of the code would expect things to happen.
Thanks for reading, and consider
Also published here.
The lead image for this article was generated by HackerNoon's AI Image Generator via the prompt "display c# on a laptop’s screen"