Explicit to Implicit Cast Operators in C#

Posted in software by Christopher R. Wirz on Tue Oct 18 2016



Cast operators allow one object or value type to be assignable to another. Implicit casting is automatically performed by the compiler and should not result in any data or precision loss.


int i = 1;
float f = i;

Explicit casting is performed by the programmer and may result in loss of precision or data.

float f = 1.01; int i = (int)f;

Objects can be defined in a way that explicit casts are fully reversable.

Note: While this approach does use weak references, that does not mean it will not cause a memory leak.

Let's begin with the following Class and Wrapper:


class ClassToWrap
{
	// Class content
}

class WrapperClass
{
	private readonly ClassToWrap _wrapped;

	// Data not present in the ClassToWrap
	public readonly long TimeStamp;

	public WrapperClass()
	{
		_wrapped = new ClassToWrap();
		TimeStamp = DateTime.Now.Ticks;
	}

	private WrapperClass(ClassToWrap x)
	{
		_wrapped = x;
		TimeStamp = DateTime.Now.Ticks;
	}

	public static explicit operator ClassToWrap(WrapperClass x)
	{
		return x._wrapped;
	}

	public static implicit operator WrapperClass(ClassToWrap x)
	{
		return new WrapperClass(x);
	}
}

When the Class is wrapped, it is given a timestamp. Therefore it gains precision with the implicit cast operator. Returning the wrapped class removes that precision, which leads to the explicit cast operator. We can test this with some code:


// Test Code
var ctw = new ClassToWrap();
WrapperClass w;
for (int i = 0; i < 5; i++)
{
	w = (WrapperClass)ctw;
	System.Console.WriteLine(w.TimeStamp);
	Console.ReadKey(); // Or time delay
	w = null;
	GC.Collect();
}
w = new WrapperClass();
for (int i = 0; i < 5; i++)
{
	ctw = (ClassToWrap)w;
	System.Console.WriteLine(((WrapperClass)ctw).TimeStamp);
	Console.ReadKey(); // Or time delay
	ctw = null;
	GC.Collect();
}

Running the code can produce results similar to the following:

606728648271881249
606728648280352310
606728648282462570
606728648284399408
606728648286475289
606728648290147555
606728648293511099
606728648297668788
606728648310947587
606728648346554067

Clearly a new WrapperClass is created with each loop. We can fix this by modifying the implicit cast operator.


private static System.Runtime.CompilerServices.ConditionalWeakTable _maps 
	= new System.Runtime.CompilerServices.ConditionalWeakTable();

public WrapperClass()
{
	_wrapped = new ClassToWrap();
	TimeStamp = DateTime.Now.Ticks;
	_maps.Add(_wrapped,this);
}
	
private WrapperClass(ClassToWrap x)
{
	_wrapped = x;
	TimeStamp = DateTime.Now.Ticks;
	_maps.Add(_wrapped,this);
}

public static explicit operator ClassToWrap(WrapperClass x)
{
	return x._wrapped;
}
	
public static implicit operator WrapperClass(ClassToWrap x)
{
	if (x == null) { return null; }
	WrapperClass wc;
	if (!_maps.TryGetValue(x, out wc) || wc == null)
	{
		wc = new WrapperClass(x);
	}
	return wc;
}

Now when we run the test code, we produce results similar to the following:

606728649244308812
606728649244308812
606728649244308812
606728649244308812
606728649244308812
606728649259649150
606728649259649150
606728649259649150
606728649259649150
606728649259649150

Two-way casting appears to be working with no loss of precision. It is probably safe to turn the explicit cast operator to an implicit cast operator.


public static implicit operator ClassToWrap(WrapperClass x)
{
	return x._wrapped;
}