Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Custom Conversions: Operators for Type Conversion

Recommended resources

Hello again. In our previous lesson, we explored how to overload operators like + and == to make our custom types more intuitive. We established that this is a powerful tool for designing APIs that are expressive and readable, especially for types that behave like primitive values.

This lesson builds directly on that foundation. We will now focus on a special kind of operator that governs how one type can transform into another. The learning objective is to implement custom implicit and explicit conversion operators between related types. You'll learn not just the syntax for these conversions, but more importantly, the design principles that determine whether a conversion should happen automatically (implicitly) or require a deliberate user action (explicitly). This skill is key to creating fluent and bug-resistant APIs.

1. The Core Principle: Safe vs. Unsafe Conversions

As a developer, you are already familiar with C#'s built-in type conversions. For example, the compiler has no problem with this:

int myInt = 100;
long myLong = myInt; // Implicit conversion

This works automatically because any possible int value can fit into a long without losing information. It's a "widening" or safe conversion. However, the reverse is not true:

long myLong = 3000000000;
// int myInt = myLong; // Compile-time error!
int myInt = (int)myLong; // Explicit conversion (cast) required

Trying to assign a long to an int requires an explicit cast. By writing (int)myLong, you are telling the compiler, "I understand that this conversion might lose data or even throw an overflow exception, but I'm taking responsibility for it." This is a "narrowing" or potentially unsafe conversion.

This fundamental principle—safe conversions can be implicit, while potentially unsafe ones must be explicit—is the same one we will apply to our own custom types.


2. Implicit Conversions: Making Code Fluent

An implicit conversion allows the compiler to automatically convert one type to another without any special syntax. The key rule, as defined by Microsoft's design guidelines, is that an implicit conversion should never throw an exception and never lose information. It should be a completely safe and predictable operation.

The syntax for defining an implicit conversion operator is:
public static implicit operator <TargetType>(<SourceType> variable)

Let's see this in a modern, practical context. A common pattern in domain-driven design is to create "Value Objects" to wrap primitive types, adding type safety and domain meaning. For instance, instead of passing Guids around for user IDs, we can create a UserId struct.

Automatic type conversion with Implicit and Explicit operators in C#

Nick Chapsas, in his video "Automatic type conversion with Implicit and Explicit operators in C#", provides a concise demonstration of this exact scenario.

Watch the introduction from the start to understand the feature. Follow along as he creates a UserId value object and implements an implicit operator to convert a Guid into a UserId from the demonstration. Notice how the assignment UserId userId = id; becomes possible. He then shows how to implement the conversion in the opposite direction, from UserId back to Guid, from the reverse conversion.

As shown in the video, the implementation inside the UserId type would be:

public readonly struct UserId
{
    public Guid Value { get; }

    public UserId(Guid value)
    {
        Value = value;
    }

    // Implicit conversion FROM Guid TO UserId
    public static implicit operator UserId(Guid value)
    {
        return new UserId(value);
    }

    // Implicit conversion FROM UserId TO Guid
    public static implicit operator Guid(UserId id)
    {
        return id.Value;
    }
}

With these operators in place, your code becomes much more expressive:

// The compiler automatically calls our implicit operator
UserId userId = Guid.NewGuid(); 

// We can also pass our UserId to a method expecting a Guid
void LogUser(Guid id) { /* ... */ }
LogUser(userId); // Works seamlessly

This is a perfect use case for implicit conversion. A UserId is conceptually just a Guid with a specific purpose; there is no data loss or risk of exceptions when converting between them.


3. Explicit Conversions: Forcing a Deliberate Choice

An explicit conversion, as the name implies, must be invoked with a cast. You should define a conversion as explicit if it might fail or lose data. This forces the consumer of your API to acknowledge the potential risk.

The syntax is nearly identical to its implicit counterpart:
public static explicit operator <TargetType>(<SourceType> variable)

Automatic type conversion with Implicit and Explicit operators in C#

Let's return to the video by Nick Chapsas. He explains the difference using the built-in int/long example and then applies it to his custom type.

First, observe his explanation of the built-in int and long conversions from the built-in types example. This reinforces the core concept. Next, see what happens when he changes the conversion from UserId to Guid to be explicit from the explicit operator demo. Notice how the code that previously compiled now requires a cast (Guid)userId.

Why would you choose explicit for the UserId to Guid conversion? While it doesn't lose data, you could argue that by casting back to a raw Guid, you are losing the type safety that the UserId struct provides. Making it explicit forces the developer to consciously "unwrap" the value and acknowledge they are moving from a specific domain type back to a general-purpose primitive.

A more clear-cut case for an explicit operator involves potential data loss.

Implicit vs Explicit Conversion in C# | Grant Winney

Grant Winney's article provides a very clear rule of thumb and a good example involving a custom Birthday type.

First, read the guiding principle from Microsoft in the section "Implicit Conversion in Our Own Types". Then, examine "Example 2", where he defines an implicit conversion from DateTime to Birthday, and then an explicit operator to convert back.

In the article's example, converting from DateTime to a custom Birthday class is safe. But what if the Birthday class only stored the Date and discarded the Time? Converting back from Birthday to DateTime would lose the time information. This is a classic scenario for an explicit conversion.

public class Birthday
{
    public DateOnly Date { get; }
    
    public Birthday(DateOnly date) { Date = date; }

    // Implicit conversion from a full DateTime is okay, 
    // we just take what we need.
    public static implicit operator Birthday(DateTime dateTime)
    {
        return new Birthday(DateOnly.FromDateTime(dateTime));
    }

    // Explicit conversion back to DateTime, because we've lost
    // the time information. We must make a choice (e.g., use midnight).
    public static explicit operator DateTime(Birthday birthday)
    {
        return birthday.Date.ToDateTime(new TimeOnly(0, 0));
    }
}

// Usage:
Birthday bday = DateTime.Now; // Implicit, easy and safe.
DateTime dt = (DateTime)bday; // Explicit, requires a cast. The programmer
                              // must be aware they are getting a DateTime at midnight.

4. Practical Application: Mapping Between Layers

As a full-stack developer, you frequently work with objects that represent the same conceptual data but have different structures in different layers of your application (e.g., a database entity, a domain model, a DTO for a web API). Conversion operators can simplify the mapping between these layers.

C# User-defined explicit and implicit conversion operators

This article by Karen Payne demonstrates using conversion operators for this exact purpose.

First, review the definitions section to refresh the distinction between implicit and explicit, from the beginning. Now, examine the scenario in Example 3. The code defines a ProductContainer with a Process property for UI logic and a simpler ProductItem for serialization. Pay attention to the implicit operator implementation that maps from a Product to a ProductItem.

In that article's example, a ProductContainer (which might be used in a UI) is converted to a ProductItem (a clean DTO). The conversion is defined as implicit because it's a safe "narrowing" in terms of properties—no data is being misinterpreted or lost, you're just selecting a subset of properties. This allows for very clean mapping code using LINQ:

// Assuming 'products' is a list of ProductContainer
// The .Select automatically uses the implicit conversion operator
List<ProductItem> results = products
    .Select<Product, ProductItem>(container => container)
    .ToList();

Without the operator, you would need to write a manual mapping inside the Select statement (.Select(c => new ProductItem { ... })), which can be verbose and repetitive.


Conclusion

Today, we've extended our knowledge of operator overloading to include custom type conversions. By understanding the fundamental distinction between safe (implicit) and potentially unsafe (explicit) conversions, you can design types that are not only expressive but also guide other developers toward correct usage.

Key takeaways from this lesson are:

  • Syntax: Conversion operators are declared as public static methods with the implicit or explicit keyword: public static implicit operator TargetType(SourceType s).
  • Implicit for Safety: Use implicit for conversions that are guaranteed to succeed and will not lose any information. This promotes fluent, readable code.
  • Explicit for Risk: Use explicit when a conversion might fail (throw an exception) or lose data (e.g., converting a decimal to an int). This forces the programmer to use a cast, making the potential risk visible in the code.
  • API Design: Thoughtful use of conversion operators can significantly simplify code, especially when mapping between different data layers or creating strongly-typed value objects.

In our next lesson, we will move from how types interact to how they are structured. We will explore a core object-oriented design principle by learning to choose between composition over inheritance to create flexible and maintainable designs. This will further refine your ability to build robust and scalable C# applications.