Implicit Operators in C# and How To Create a Multi Type

As a software engineer, you've likely encountered situations where you need to represent a value that can either be one thing or another, but not both at the same time. In C#, there isn't a built-in type that represents this scenario, and given that C# is a strongly-typed language, this seems difficult on the surface. However, we can create our own custom data type using implicit operators!

In this article, we'll explore how we can use implicit operators in C# to create an "either" data type that represents one value or another. We'll start by discussing what implicit operators are and how they work. Then, we'll dive into how we can use them to create our custom "either" data type. Finally, we'll wrap up with some examples of how we can use this custom data type in real-world scenarios.

Understanding Implicit Operators

Before diving into creating our custom data type, let's first take a moment to understand what implicit operators are and how they work. After all, even many C# developers can go years using the language without ever having to consider writing code to use implicit operators. Personally, I've had little use to even explore them after over a decade of coding in C#.

Implicit operators are special methods that allow us to define custom conversions between two data types. When we use an implicit operator, the conversion happens automatically without the need for explicit casting. In other words, we can use an implicit operator to define how a type can be converted to another type.

Here's an example of an implicit operator that converts a string to an integer:

public static implicit operator int(string str)
{
    return int.Parse(str);
}

With this implicit operator defined, we can now convert a string to an integer without the need for explicit casting:

string str = "42";
int num = str; // implicit conversion

Creating an Either Data Type

Now that we understand what implicit operators are and how they work, let's explore how we can use them to create our custom "either" data type. Keep in mind that we'll be looking at a class here, but you can also apply this to the record type in C#.

Our "either" data type will represent a value that can either be one thing or another, but not both at the same time. We can define this data type as a class with two properties: one for the first value and another for the second value. We'll also need to define two implicit operators to allow us to convert a value to our "either" data type and vice versa.

Here's what our "either" data type class might look like:

public class Either<T1, T2>
{
    private readonly T1 _value1;
    private readonly T2 _value2;
    private readonly bool _isValue1;

    public Either(T1 value)
    {
        _value1 = value;
        _isValue1 = true;
    }

    public Either(T2 value)
    {
        _value2 = value;
        _isValue1 = false;
    }

    public static implicit operator Either<T1, T2>(T1 value)
    {
        return new Either<T1, T2>(value);
    }

    public static implicit operator Either<T1, T2>(T2 value)
    {
        return new Either<T1, T2>(value);
    }

    public T Match<T>(Func<T1, T> f1, Func<T2, T> f2)
    {
        return _isValue1 ? f1(_value1) : f2(_value2);
    }
}

Let's review what the code above does. We define our class with two generic type parameters, T1 and T2, which represent the two possible values that our "either" data type can hold. We then define two constructors: one for each possible value. In each constructor, we store the value and set a boolean flag to indicate which value is currently being held. This pattern ensures that we can only create this object (through normal means) with either one of the two types passed in (not neither, and not both).

Next, we define two implicit operators that allow us to convert a value of type T1 or T2 to our "either" data type. The first operator takes a value of type T1 and returns a new instance of our "either" data type with the value set to the provided value. The second operator does the same thing but for values of type T2.

Finally, we define a Match method that takes two functions, f1 and f2, and returns a value of type T. This method is used to retrieve the value currently being held by our "either" data type. If the boolean flag is set to true, we call the f1 function with the value of T1 that is being held. If the boolean flag is set to false, we call the f2 function with the value of T2 that is being held. There are alternative mechanisms we could use here for retrieving the value but we will explore them in a future article.

With this class defined, we can now create an instance of our "either" data type by simply assigning a value of type T1 or T2 to it:

Either<int, string> either = 42; // creates an instance with an int value
Either<int, string> either = "hello"; // creates an instance with a string value

We can also retrieve the value currently being held by our "either" data type using the Match method:

Either<int, string> either = 42;
int num = either.Match(
    value1 => value1, // returns the int value
    value2 => int.Parse(value2) // parses the string value to an int (not called because we assigned an integer value of 42)
);

Real-World Examples

Now that we've seen how we can create our custom "either" data type using implicit operators, let's take a look at some real-world examples of how we can use it.

Parsing Input

Imagine we're building a command-line tool that accepts input from the user. The user can provide either a file path or a URL. We want to store this input as a string, but we also want to be able to easily determine whether the user provided a file path or a URL.

We can use our "either" data type to represent this scenario:

Either<string, Uri> input = "file.txt";
// or
Either<string, Uri> input = new Uri("https://example.com");

We can then retrieve the value currently being held by our "either" data type and act accordingly:

var input = // get user input
var parsedInput = Uri.TryCreate(input, UriKind.Absolute, out Uri uri) ?
    new Either<string, Uri>(uri) :
    new Either<string, Uri>(input);

Returning Results

Imagine we're building a function that performs a complex calculation and returns a result. This calculation might fail, in which case we want to return an error message instead of a result. This is actually the number one reason I started working with a custom type that could represent two different types, exclusively! And I will link that for you to follow up with shortly!

Conclusion

In this blog post, we've explored how we can use implicit operators in C# to create a custom "either" data type that allows us to represent values that can be one of two types, but not both at the same time. We saw how we can define implicit operators that enable us to create instances of our custom "either" data type by simply assigning values of the appropriate types to it.

We also saw how we can define a Match method that enables us to retrieve the value currently being held by our "either" data type and act accordingly. We discussed some real-world examples of how we can use this data type to represent scenarios where a value can be one of two types, but not both.

By using implicit operators in this way, we can create more expressive and concise code that clearly communicates our intentions. The "either" data type is just one example of how we can use these powerful language features to create more flexible and powerful code.

Stay tuned for the post on Tried, TriedEx, and TriedNullEx!

Implicit Operators - Clean Code Secrets Or Buggy Nightmare?

Implicit operators in C# are a feature that can make our code more readable and expressive. But beware! Misuse can backfire and cause a great deal of headaches!

Implicit Operators in C#: How To Simplify Type Conversions

Learn how to perform implicit conversions by using implicit operators in C#. This is a helpful feature that can enhance readability when done with care.

How to Automatically Cast Between Types in C#

Learn how to automatically cast between types in C#! Leveraging implicit operators in C#, we can do away with explicit casts to convert between types.

An error has occurred. This application may no longer respond until reloaded. Reload x