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>
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; }
}
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));
}
Example usage
private static void TemperatureExample()
{
Celsius celsius1 = new(10);
Fahrenheit fahrenheit = celsius1;
Celsius celsius2 = fahrenheit;
}
💡 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;
}
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[/]");
}
}
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));
}
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.
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));
}
}
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();
}
}
}
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();
}
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,
};
}
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();
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();
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() }
}));
Which writes decimals as a string.
{
"ProductId": 41,
"ProductName": "Awesome Soft Shoes",
"UnitPrice": "8.16",
"UnitsInStock": 2
}
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);
}
}
Similarly for explicit operator the following would be used.
List<ProductItem> results = products.Select(pc => (ProductItem)pc).ToList();