Search code examples
c#castingpropertiescustomization

What's a C# best practice for having a property be easily cast to multiple types?


I'm writing a C# class. As an example, the class is a Distance class to keep track of a distance float value in meters, while also having multiple properties for different units. Like centimeters, kilometers, etc.

I want to be able to make these properties be implicitly usable in place of numbers (float) and also as strings. Meaning, I want to be able to do distance.centimeters + 1 to calculate 1 plus the distance in centimeters units and use it somewhere else, and I also want to be able to do Console.WriteLine(distance.centimeters) to print a custom string, like appending the unit (e.g. 104 cm if the meters property has the value 1.04).

While researching, I discovered that you can have custom implicit type conversions, but that works on the class/object level, but not the property (which is a float).

So now I want to create either:

  1. a string property different from the float property (that has "String" at the end of its name)
  2. Or a method that returns a string (that has "ToString()" at the end of its name to be similar to the overridden ".ToString()" method).

What is a best practice when it comes to this issue? What would be some differences for between these two options that I might need to consider? I'd appreciate all thoughts related to the subject.

Example Code:

public class Distance
{
    public float meters;
    public Distance (float m)
    {
        meters = m;
    }

    public float centimeters
    {
        get
        {
            return meters * 100;
        }
    }

    // Option 1
    public string centimetersString
    {
        get
        {
            return centimeters + " cm";
        }
    }

    // Option 2
    public string centimetersToString()
    {
        return centimeters + " cm";
    }
}

Solution

  • You could encapsulate each distance type in its own class, and then use implicit operator to provide conversions.

    A drawback of this approach is that every distance class must contain implicit operator implementations to convert every other distance type to its type.

    With that said, here's an example showing what I mean:

    using System;
    
    namespace Demo
    {
        static class Program
        {
            static void Main()
            {
                Metres metres = 10;
                Centimetres centimetres = metres;
                Console.WriteLine(centimetres); // Prints "1000cm"
    
                metres = centimetres;
                Console.WriteLine(metres); // Prints "10m"
    
                var kilometres = (Kilometres) centimetres;
                Console.WriteLine(kilometres); // Prints "0.01km"
            }
        }
    
        public sealed class Metres
        {
            public Metres(float metres)
            {
                _metres = metres;
            }
    
            public float Distance => _metres;
    
            public static implicit operator Metres(Centimetres centimetres) => new (centimetres.Distance / 100.0f);
            public static implicit operator Metres(Kilometres kilometres)   => new (kilometres.Distance / 100_000.0f);
            public static implicit operator Metres(float metres)            => new (metres);
    
            public override string ToString()
            {
                return $"{_metres}m";
            }
    
            readonly float _metres;
        }
    
        public sealed class Centimetres
        {
            public Centimetres(float centimetres)
            {
                _centimetres = centimetres;
            }
    
            public float Distance => _centimetres;
    
            public static implicit operator Centimetres(Metres metres)         => new (metres.Distance * 100.0f);
            public static implicit operator Centimetres(Kilometres kilometres) => new (kilometres.Distance * 100_000.0f);
            public static implicit operator Centimetres(float centimetres)     => new (centimetres);
    
            public override string ToString()
            {
                return $"{_centimetres}cm";
            }
    
            readonly float _centimetres;
        }
    
        public sealed class Kilometres
        {
            public Kilometres(float kilometres)
            {
                _kilometres = kilometres;
            }
    
            public float Distance => _kilometres;
    
            public static implicit operator Kilometres(Metres metres)           => new (metres.Distance / 1000.0f);
            public static implicit operator Kilometres(Centimetres centimetres) => new (centimetres.Distance /100_000.0f);
            public static implicit operator Kilometres(float kilometres)        => new (kilometres);
    
            public override string ToString()
            {
                return $"{_kilometres}km";
            }
    
            readonly float _kilometres;
        }
    }
    

    Runnable example on .net Fiddle: https://dotnetfiddle.net/tJE62S


    If you want to add some overloaded arithmetic operators it gets a little more involved:

    using System;
    
    namespace Demo
    {
        static class Program
        {
            static void Main()
            {
                Metres metres = 10;
                Centimetres centimetres = metres;
                Console.WriteLine(centimetres); // Prints "1000cm"
    
                metres = centimetres;
                Console.WriteLine(metres); // Prints "10m"
    
                var kilometres = (Kilometres) centimetres;
                Console.WriteLine(kilometres); // Prints "0.01km"
    
                var addedMetres = metres + 10;
                Console.WriteLine(addedMetres); // Prints "20m"
                
                var subtractedCm = centimetres - 100;
                Console.WriteLine(subtractedCm); // Prints "900cm"
    
                // This would be an ambiguous call - should the result be Centimetres or Metres?
                //     var difference = addedMetres - subtractedCm;
                // So fix it by casting one of the operands to the result type that you want:
    
                var diffCm = (Centimetres)addedMetres - subtractedCm;
                Console.WriteLine(diffCm); // Prints "1100cm" 
    
                var diffM = addedMetres - (Metres)subtractedCm;
                Console.WriteLine(diffM); // Prints "11m" 
            }
        }
    
        public sealed class Metres
        {
            public Metres(float metres)
            {
                _metres = metres;
            }
    
            public float Distance => _metres;
    
            public static implicit operator Metres(Centimetres centimetres) => new (centimetres.Distance / 100.0f);
            public static implicit operator Metres(Kilometres kilometres)   => new (kilometres.Distance / 100_000.0f);
            public static implicit operator Metres(float metres)            => new (metres);
    
            public static Metres operator +(Metres a, Metres b) => new (a.Distance + b.Distance);
            public static Metres operator -(Metres a, Metres b) => new (a.Distance - b.Distance);
    
            public override string ToString()
            {
                return $"{_metres}m";
            }
    
            readonly float _metres;
        }
    
        public sealed class Centimetres
        {
            public Centimetres(float centimetres)
            {
                _centimetres = centimetres;
            }
    
            public float Distance => _centimetres;
    
            public static implicit operator Centimetres(Metres metres)         => new (metres.Distance * 100.0f);
            public static implicit operator Centimetres(Kilometres kilometres) => new (kilometres.Distance * 100_000.0f);
            public static implicit operator Centimetres(float centimetres)     => new (centimetres);
    
            public static Centimetres operator +(Centimetres a, Centimetres b) => new (a.Distance + b.Distance);
            public static Centimetres operator -(Centimetres a, Centimetres b) => new (a.Distance - b.Distance);
    
            public override string ToString()
            {
                return $"{_centimetres}cm";
            }
    
            readonly float _centimetres;
        }
    
        public sealed class Kilometres
        {
            public Kilometres(float kilometres)
            {
                _kilometres = kilometres;
            }
    
            public float Distance => _kilometres;
    
            public static implicit operator Kilometres(Metres metres)           => new (metres.Distance / 1000.0f);
            public static implicit operator Kilometres(Centimetres centimetres) => new (centimetres.Distance /100_000.0f);
            public static implicit operator Kilometres(float kilometres)        => new (kilometres);
    
            public static Kilometres operator +(Kilometres a, Kilometres b) => new (a.Distance + b.Distance);
            public static Kilometres operator -(Kilometres a, Kilometres b) => new (a.Distance - b.Distance);
    
            public override string ToString()
            {
                return $"{_kilometres}km";
            }
    
            readonly float _kilometres;
        }
    }
    

    Runnable example on .net Fiddle: https://dotnetfiddle.net/uoN1Wr