将 属性 轻松转换为多种类型的 C# 最佳实践是什么?

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

我正在编写 C# class。例如,class 是一个距离 class,用于跟踪以米为单位的距离 float 值,同时还具有针对不同单位的多个属性。比如厘米,公里等等

我希望能够使这些属性可以隐式地代替数字 (float) 以及 string 来使用。意思是,我希望能够 distance.centimeters + 1 计算 1 加上以厘米为单位的距离,并在其他地方使用它,我还希望能够 Console.WriteLine(distance.centimeters) 打印自定义字符串,比如附加单位(例如 104 cm 如果 meters 属性 的值为 1.04)。

在研究过程中,我发现您可以进行自定义隐式类型转换,但这适用于 class/object 级别,但不适用于 属性(即 float)。

所以现在我想创建:

  1. 不同于浮点数 属性 的字符串 属性(名称末尾有“String”)
  2. 或者 returns 字符串的方法(名称末尾有“ToString()”,类似于被覆盖的“.ToString()”方法)。

关于此问题的最佳做法是什么?我可能需要考虑的这两个选项之间有什么区别?我将不胜感激与该主题相关的所有想法。

示例代码:

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";
    }
}

我为你做了一些东西,我希望它能帮助你,满足你的需求。 默认值始终为厘米。但是你当然可以随意更改它。

public class Distance
{
    public float Value { get; set; }
    public Distance(float value, MetricSystem metric)
    {
        Value = metric switch
        {
            MetricSystem.Centimeter => value,
            MetricSystem.Meter => value * 100,
            _ => Value
        };
    }

    public void AddValue(float value, MetricSystem metric)
    {
        Value += metric switch
        {
            MetricSystem.Centimeter => value,
            MetricSystem.Meter => value * 100,
            _ => 0
        };
    }

    public string ReadValue(MetricSystem metric)
    {
        return metric switch
        {
            MetricSystem.Centimeter => $"{Value} cm",
            MetricSystem.Meter => $"{Value / 100} m",
            _ => string.Empty
        };
    }
}

public enum MetricSystem
{
    Centimeter,
    Meter
}

我的结果是:

您可以将它们分解成不同的 classes。每种测量类型一个。这样你就有了一个专用于特定类型(比如厘米)的 class,并且你可以在 class 到 return 中有不同的属性,你的值以多种格式格式化。原则是你可能试图在一个 class 中做太多事情。这是一些示例代码。这可能有多种变体,例如抽象基础 class 等。这是“A”解决方案,通过拆分为多个 classes 来向您展示我的意思。

代码来自 LinqPad。

void Main()
{
    var dc = new Distance(10);
    
    dc.ToString().Dump();
    var cent = dc.ToCentimeters();
    cent.Centimeters.Dump();
    cent.ToString().Dump();
    
    var mil = dc.ToMillimeters();
    mil.Millimeters.Dump();
    mil.ToString().Dump();
    
    // outputs
    //10 Meters
    //1000
    //1000 Centimeters
    //10000
    //10000 Millimeters
}

// You can define other methods, fields, classes and namespaces here
public class Distance
{
    private float _meters;
    public Distance (float meters)
    {
        _meters = meters;
    }
    
    public DistanceCentimeters ToCentimeters(){
        return new DistanceCentimeters(_meters * 100);
    }
    
    public DistanceMillimeters ToMillimeters(){
        return new DistanceMillimeters(_meters * 1000);
    }
    
    public override string ToString(){
        return $"{_meters} Meters";
    }
}

public class DistanceCentimeters{
    public DistanceCentimeters(float centimeters){
        _centimeters = centimeters;
    }

    private float _centimeters;
    public float Centimeters
    {
        get
        {
            return _centimeters;
        }
    }
    
    public override string ToString(){
        return $"{Centimeters} Centimeters";
    }
}

public class DistanceMillimeters {
    public DistanceMillimeters(float millimeters){
        _millimeters = millimeters;
    }
    
    private float _millimeters;
    public float Millimeters
    {
        get
        {
            return _millimeters;
        }
    }
    
    public override string ToString(){
        return $"{Millimeters} Millimeters";
    }
}

您可以将每个距离类型封装在自己的 class 中,然后使用 implicit operator 提供转换。

这种方法的缺点是每个距离 class 必须包含 implicit operator 实现以将所有其他距离类型转换为其类型。

话虽如此,这里有一个例子可以说明我的意思:

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;
    }
}

.net 上的可运​​行示例 Fiddle:https://dotnetfiddle.net/tJE62S


如果你想添加一些重载的算术运算符,它会涉及更多:

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;
    }
}

.net 上的可运​​行示例 Fiddle:https://dotnetfiddle.net/uoN1Wr

看完所有答案后,我选择接受Matthew Watson的答案,因为这是最接近我想做的事情。这是我的代码看起来像是受他的回答启发的样子。如果我需要额外的度量前缀,我只需将 To<InsertPrefix>() 添加到接口,在每个 class 中实现它,并为其创建一个 class 并相应地实现方法。

public interface IDistance
{
    public double Value();
    public string ToString();
    public Centimeters ToCentimeters();
    public Meters ToMeters();
}

public class Meters : IDistance
{
    private double _meters;

    public Meters(double m)
    {
        _meters = m;
    }

    public Meters(IDistance distance)
    {
        _meters = distance.ToMeters()._meters;
    }

    public double Value()
    {
        return _meters;
    }

    public override string ToString() => $"{_meters} m";

    public Centimeters ToCentimeters() => new Centimeters(_meters * 100);

    public Meters ToMeters() => this;

    public static Meters operator +(Meters m, IDistance d) => new Meters(m.Value() + d.ToMeters().Value());
    public static Meters operator +(IDistance d, Meters m) => new Meters(m.Value() + d.ToMeters().Value());


}

public class Centimeters : IDistance
{
    private double _centimeters;

    public Centimeters(double c)
    {
        _centimeters = c;
    }

    public Centimeters(IDistance distance)
    {
        _centimeters = distance.ToCentimeters()._centimeters;
    }

    public double Value()
    {
        return _centimeters;
    }

    public override string ToString() => $"{_centimeters} cm";

    public Centimeters ToCentimeters() => this;

    public Meters ToMeters() => new Meters(_centimeters / 100);

    public static Centimeters operator +(Centimeters c, IDistance d) => new Centimeters(c.Value() + d.ToCentimeters().Value());
    public static Centimeters operator +(IDistance d, Centimeters c) => new Centimeters(c.Value() + d.ToCentimeters().Value());
}

用法

public void Test()
{
    IDistance distance = new Centimeters(4);
    distance = distance + new Meters(4);
    Trace.WriteLine(distance.ToMeters().ToString()); // "1.04 m"
}