explicit vs implicit: Designing Conversions that Don't Bite

When Conversions Go Wrong

If you've ever written int x = (int)someDouble and lost fractional data silently, you've experienced the trade-off between convenience and safety. C# lets you define custom conversions between your types, but choosing between implicit and explicit operators determines whether callers notice data loss.

Implicit conversions happen automatically and should never surprise users. Explicit conversions require casts and signal "I know what I'm doing." Getting this choice wrong leads to silent bugs or APIs that require unnecessary ceremony.

You'll learn the rules for safe conversion design, when each operator type fits, and how to prevent data loss. By the end, you'll write conversions that match user expectations and protect data integrity.

Implicit Conversions: Safe and Automatic

Implicit conversions work without casts. The compiler applies them automatically when you assign or pass a value to a compatible type. Use implicit operators only when conversion always succeeds and preserves all information.

The standard example is numeric widening: converting int to long never fails or loses data. Your custom types should follow this same principle. If there's any chance of information loss or failure, use an explicit operator instead.

Temperature.cs - Safe implicit conversion
namespace Units;

public readonly struct Celsius
{
    public double Value { get; }

    public Celsius(double value) => Value = value;

    // Implicit to double - always safe, no data loss
    public static implicit operator double(Celsius c) => c.Value;

    // Implicit from double - always succeeds
    public static implicit operator Celsius(double value) => new(value);
}

public readonly struct Kelvin
{
    public double Value { get; }

    public Kelvin(double value) => Value = value;

    // Implicit Celsius to Kelvin - always valid
    public static implicit operator Kelvin(Celsius c) =>
        new(c.Value + 273.15);

    // Kelvin to Celsius
    public static implicit operator Celsius(Kelvin k) =>
        new(k.Value - 273.15);
}

These conversions preserve all data and never fail. Converting between temperature scales is mathematically straightforward, and extracting the numeric value is information-preserving. Users can write double temp = myCelsius without casts.

Explicit Conversions: When Precision Matters

Explicit conversions require casts and signal potential data loss or failure. Use them when rounding occurs, when conversion might throw, or when the conversion changes meaning significantly enough that users should acknowledge it.

Explicit operators let you provide convenient conversion syntax while forcing callers to be deliberate. The cast syntax makes data transformation visible in code reviews and prevents accidental precision loss.

Currency.cs - Explicit for precision loss
namespace Finance;

public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // Explicit to int - loses decimal places
    public static explicit operator int(Money m) => (int)m.Amount;

    // Explicit to double - loses precision
    public static explicit operator double(Money m) => (double)m.Amount;

    // Explicit from string - might fail
    public static explicit operator Money(string s)
    {
        var parts = s.Split(' ');
        if (parts.Length != 2)
            throw new FormatException("Format must be 'amount currency'");

        if (!decimal.TryParse(parts[0], out var amount))
            throw new FormatException($"Invalid amount: {parts[0]}");

        return new Money(amount, parts[1]);
    }
}

Converting Money to int loses cents. Converting from string might throw if the format is wrong. Both require explicit casts like (int)money or (Money)"100.50 USD", making the potential issues visible to anyone reading the code.

Designing Bidirectional Conversions

When providing conversions in both directions, consider whether each direction should be implicit or explicit independently. One direction might be safe while the reverse loses information or could fail.

Common patterns include implicit from your type to a simpler representation, but explicit back if reconstruction might fail or lose data. This asymmetry protects users while still providing convenience for safe operations.

Identifier.cs - Asymmetric conversions
namespace Domain;

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

    public UserId(int value)
    {
        if (value <= 0)
            throw new ArgumentException("User ID must be positive");
        Value = value;
    }

    // Implicit to int - always safe
    public static implicit operator int(UserId id) => id.Value;

    // Explicit from int - validation required
    public static explicit operator UserId(int value) => new(value);

    // Implicit to string - always succeeds
    public static implicit operator string(UserId id) => id.Value.ToString();

    // Explicit from string - parsing might fail
    public static explicit operator UserId(string s)
    {
        if (!int.TryParse(s, out var value))
            throw new FormatException($"Cannot parse '{s}' as UserId");
        return new UserId(value);
    }
}

Extracting the int or string is safe and implicit. Creating a UserId requires validation, so those conversions are explicit. This design lets users write int id = userId naturally while forcing var userId = (UserId)42 to acknowledge validation.

Working with Nullable Value Types

The compiler automatically lifts your conversion operators to nullable versions. If you define implicit operator int(MyStruct), you also get implicit operator int?(MyStruct?) that returns null when the input is null.

You can define nullable-specific operators if you need custom null handling. This lets you provide different behavior for null inputs than what the automatic lifting provides.

Score.cs - Nullable conversions
namespace Gaming;

public readonly struct Score
{
    public int Points { get; }

    public Score(int points)
    {
        if (points < 0)
            throw new ArgumentException("Score cannot be negative");
        Points = points;
    }

    // Implicit to int
    public static implicit operator int(Score score) => score.Points;

    // Compiler provides: implicit operator int?(Score? score)

    // Explicit nullable override - treat null as zero
    public static explicit operator int(Score? score) => score?.Points ?? 0;

    // Optional: convert null to minimum score instead
    public static Score FromNullable(int? points) =>
        points.HasValue ? new Score(points.Value) : new Score(0);
}

The automatic nullable lifting handles null propagation naturally. If you need different null semantics, define explicit overloads or provide helper methods. Most types work fine with the default lifted behavior.

Choosing the Right Approach

Use implicit conversions when the operation is information-preserving and always succeeds. Think of built-in conversions like int to long or derived class to base class. If users wouldn't want to see an explicit cast in their code, make it implicit.

Use explicit conversions when data might be lost, when validation could fail, or when the semantic meaning changes enough that callers should acknowledge it. Casting from double to int loses decimals—that deserves visibility. Parsing strings might throw—that deserves a cast.

Avoid conversion operators entirely when the operation needs configuration or context. Converting a User to a UserDTO might require decisions about which fields to include. Use a method like ToDTO() instead, where parameters make options explicit.

Never throw from implicit operators unless the type's invariants are fundamentally violated. Users expect implicit conversions to "just work" like built-in conversions. Reserve exceptions for explicit operators where casts signal "this might fail."

Try It Yourself

Build types with both implicit and explicit conversions to see how they behave differently. You'll experience how implicit conversions provide convenience while explicit ones force acknowledgment of data loss.

Steps

  1. Set up the project: dotnet new console -n ConversionDemo
  2. Navigate in: cd ConversionDemo
  3. Replace Program.cs with the code below
  4. Update the .csproj file
  5. Run: dotnet run
ConversionDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs
Console.WriteLine("=== Conversion Operators Demo ===\n");

var distance = new Meters(1500);

// Implicit conversion to double
double meters = distance;
Console.WriteLine($"Distance: {meters}m");

// Explicit conversion to int (loses precision)
int rounded = (int)distance;
Console.WriteLine($"Rounded: {rounded}m");

// Explicit from string (might throw)
try
{
    var parsed = (Meters)"2500";
    Console.WriteLine($"Parsed: {parsed.Value}m");
}
catch (FormatException ex)
{
    Console.WriteLine($"Parse failed: {ex.Message}");
}

readonly struct Meters
{
    public double Value { get; }

    public Meters(double value) => Value = value;

    // Implicit to double - safe
    public static implicit operator double(Meters m) => m.Value;

    // Explicit to int - loses decimals
    public static explicit operator int(Meters m) => (int)m.Value;

    // Explicit from string - validation required
    public static explicit operator Meters(string s)
    {
        if (!double.TryParse(s, out var value))
            throw new FormatException($"Invalid meters value: {s}");
        return new Meters(value);
    }
}

Console

=== Conversion Operators Demo ===

Distance: 1500m
Rounded: 1500m
Parsed: 2500m

Notice how implicit to double needs no cast, while explicit to int and from string both require casts. This makes potential data loss and parsing failures visible in the code.

How do I...?

How do I decide between explicit and implicit conversions?

Use implicit when the conversion always succeeds and never loses data, like int to long. Use explicit when conversion might fail or lose precision, like double to int. If users need to think about the conversion, make it explicit.

Can conversion operators throw exceptions?

Yes, especially explicit operators. Throw ArgumentException or InvalidOperationException when conversion is impossible. For implicit operators, avoid throwing—users don't expect safe-looking conversions to fail unexpectedly.

Should I provide both directions of conversion?

Only if both make semantic sense. Converting Celsius to Fahrenheit and back works. But converting Order to OrderDTO might not warrant a reverse conversion if DTOs lack full order data. Consider each direction independently.

Do conversion operators work with nullable types?

The compiler lifts conversion operators to nullable versions automatically. If you define implicit operator int(Temperature t), you also get implicit operator int?(Temperature? t) for free. Handle null cases explicitly if needed.

Can I chain multiple conversion operators?

The compiler doesn't chain user-defined conversions automatically. If A converts to B and B to C, A won't automatically convert to C. You must define A to C directly or explicitly call both conversions.

Back to Articles