C# User-defined explicit and implicit conversion operators

Karen Payne - Jan 1 - - Dev Community

Learn about explicit and implicit conversion operators with practical examples for C#12 and higher.

Source code

Clone the following GitHub repository.

Definitions

A user-defined type can define a custom implicit or explicit conversion from or to another type.

Implicit conversions don't require special syntax to be invoked and can occur in various situations, for example, in assignments and methods invocations. Predefined C# implicit conversions always succeed and never throw an exception. User-defined implicit conversions should behave in that way as well. If a custom conversion can throw an exception or lose information, define it as an explicit conversion.

(above is from Microsoft)

Important

In several cases, classes are aliased in the project file so that, in this case ProductItem class can have different definitions without the need for a long path to the specific model for a specific code sample.



<ItemGroup>
   <Using Include="OperatorSamples.Models1" Alias="M1" />
   <Using Include="OperatorSamples.Models2" Alias="M2" />
   <Using Include="OperatorSamples.Models3" Alias="M3" />
</ItemGroup>


Enter fullscreen mode Exit fullscreen mode

INotifyPropertyChanged has nothing to do with the conversions, just good practice for change notification.

💡 Check out System.Convert for conversion methods in the Framework.

Console and Windows Forms projects are used to reach a wide audience rather than web or MAUI.

Example 1

We want to convert from Celsius to Fahrenheit using a conversion between the two types implicitly.

Base class which has a single property other classes inherit.



public class Temperature
{
    public float Degrees { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

Below Celsius utilizes an implicit operator to convert to Fahrenheit by passing in itself and a conversion is done. The opposite is done for the Fahrenheit class to Celsius.



public class Celsius : Temperature
{
    public Celsius(float sender)
    {
        Degrees = sender;
    }

    public static implicit operator Fahrenheit(Celsius sender) 
        => new((9.0f / 5.0f) * sender.Degrees + 32);
}

public class Fahrenheit : Temperature
{
    public Fahrenheit(float sender)
    {
        Degrees = sender;
    }

    public static implicit operator Celsius(Fahrenheit sender) 
        => new((5.0f / 9.0f) * (sender.Degrees - 32));
}


Enter fullscreen mode Exit fullscreen mode

Example usage



private static void TemperatureExample()
{
    Celsius celsius1 = new(10);
    Fahrenheit fahrenheit = celsius1;
    Celsius celsius2 = fahrenheit;
}


Enter fullscreen mode Exit fullscreen mode

Shows results from Visual Studio local window

💡 A similar conversion might be currency conversion.

Example 2

Working with paths for files, here we define to implicit operators which encapsulates commonly used methods, does the path exists and to list, in this case all .csproj files in the current solution.

Base class



public class FilePath(string path)
{
    public static implicit operator string(FilePath self) => self?.ToString();
    public static implicit operator FilePath(string value) => new(value);
    public override string ToString() => path;
}


Enter fullscreen mode Exit fullscreen mode

Wrapper class



public class PathOperations
{
    public void PrintFolderContents(string path)
    {
        GlobbingOperations.TraverseFileMatch += GlobbingOperations_TraverseFileMatch;
        GlobbingOperations.GetProjectFilesAsync(path);
    }

    private void GlobbingOperations_TraverseFileMatch(FileMatchItem sender)
    {
        Console.WriteLine(Path.GetFileNameWithoutExtension(sender.FileName));
    }

    public void PathExists(FilePath path)
    {
        AnsiConsole.MarkupLine(Path.Exists(path)
            ? $"[cyan]{nameof(PathExists)}[/] [yellow]{path}[/] [cyan]exists[/]"
            : $"[cyan]{nameof(PathExists)}[/] [yellow]{path}[/] [cyan]does not exists[/]");
    }
}


Enter fullscreen mode Exit fullscreen mode

Usage

There is a lot going on under the covers that is not important to the conversions, for example, using globbing.



private static void FilePathExample()
{

    PathOperations pathOps = new();
    pathOps.PathExists(DirectoryOperations.GetSolutionInfo().FullName);
    pathOps.PathExists(new FilePath(DirectoryOperations.GetSolutionInfo().FullName));
    pathOps.PrintFolderContents(DirectoryOperations.GetSolutionInfo().FullName);
    pathOps.PrintFolderContents(new FilePath(DirectoryOperations.GetSolutionInfo().FullName));

}


Enter fullscreen mode Exit fullscreen mode

Result of the above operation

Example 3

In this example, a DataGridView is populated with a list with property change notification as follows with a DataGridViewCheckBoxColumn which is not and should not be part of the model.

Shows the DataGridView with a checkbox column



public class Product : INotifyPropertyChanged
{
    private int _productId;
    private string _productName;
    private decimal _unitPrice;
    private short _unitsInStock;

    public int ProductId
    {
        get => _productId;
        set
        {
            if (value == _productId) return;
            _productId = value;
            OnPropertyChanged();
        }
    }

    public string ProductName
    {
        get => _productName;
        set
        {
            if (value == _productName) return;
            _productName = value;
            OnPropertyChanged();
        }
    }

    public decimal UnitPrice
    {
        get => _unitPrice;
        set
        {
            if (value == _unitPrice) return;
            _unitPrice = value;
            OnPropertyChanged();
        }
    }

    public short UnitsInStock
    {
        get => _unitsInStock;
        set
        {
            if (value == _unitsInStock) return;
            _unitsInStock = value;
            OnPropertyChanged();
        }
    }

    public Product(int id)
    {
        ProductId = id;
    }

    public Product()
    {

    }

    public override string ToString() => ProductName;

    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}


Enter fullscreen mode Exit fullscreen mode

Since there is a need to remember checked products we use the following class so not to taint the base product class by having a Process property.



public class ProductContainer : Product
{
    private bool _process;

    public ProductContainer(int id)
    {
        ProductId = id;
    }

    public ProductContainer()
    {

    }

    public bool Process
    {
        get => _process;
        set
        {
            if (value == _process) return;
            _process = value;
            OnPropertyChanged();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

A list is populated with NuGet package Bogus.



public static List<ProductContainer> Products1(int productCount = 50)
{
    int identifier = 1;
    Faker<ProductContainer> fake = new Faker<ProductContainer>()
        .CustomInstantiator(f => new ProductContainer(identifier++))
        .RuleFor(p => p.ProductName, f => f.Commerce.ProductName())
        .RuleFor(p => p.UnitPrice, f => f.Random.Decimal(10, 2))
        .RuleFor(p => p.UnitsInStock, f => f.Random.Short(1, 5));

    return fake.Generate(productCount).OrderBy(x => x.ProductName).ToList();
}


Enter fullscreen mode Exit fullscreen mode

implicit operator

We do not want the Process property so a class shown below uses an implicit operator to only get the other properties, in short the implicit operator is allowing for a simple mapping.



public class ProductItem
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public decimal UnitPrice { get; set; }
    public short UnitsInStock { get; set; }

    public static implicit operator ProductItem(Product product) =>
        new()
        {
            ProductId = product.ProductId,
            ProductName = product.ProductName,
            UnitPrice = product.UnitPrice,
            UnitsInStock = product.UnitsInStock,
        };
}


Enter fullscreen mode Exit fullscreen mode

Usage

Since the DataGridView has a BindingList as the DataSource we can get checked items directly from the BindingList as follows.



List<ProductContainer> products = 
    _bindingList.Where(pc => pc.Process).ToList();


Enter fullscreen mode Exit fullscreen mode

Then using the following to get checked items without the Process property which without the implicit operator would not be possible.



List<ProductItem> results = products
    .Select<Product, ProductItem>(container => container).ToList();


Enter fullscreen mode Exit fullscreen mode

For above code

Finally the resulting list is written to a json file and note the converter.



            File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
            {
                WriteIndented = true,
                Converters = { new FixedDecimalJsonConverter() }
            }));


Enter fullscreen mode Exit fullscreen mode

Which writes decimals as a string.



  {
    "ProductId": 41,
    "ProductName": "Awesome Soft Shoes",
    "UnitPrice": "8.16",
    "UnitsInStock": 2
  }


Enter fullscreen mode Exit fullscreen mode

To Deserialize, use the following generic method.



public class JsonHelpers
{
    /// <summary>
    /// Read json from string with converter for reading decimal from string
    /// </summary>
    /// <typeparam name="T">Type to convert</typeparam>
    /// <param name="json">valid json string for <see cref="T"/></param>
    public static List<T>? Deserialize<T>(string json)
    {

        JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
        {
            WriteIndented = true
        };

        return JsonSerializer.Deserialize<List<T>>(json, options);

    }
}


Enter fullscreen mode Exit fullscreen mode

Similarly for explicit operator the following would be used.



List<ProductItem> results = products.Select(pc => (ProductItem)pc).ToList();


Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .