如何在C#中为测量单位创建通用转换器?

Pri*_*cey 7 .net c# generics delegates units-of-measurement

在研究涉及温度转换的小型烹饪项目以及一些烹饪测量转换(例如Imperial to Metric)时,我一直在努力学习更多关于代表和lambdas的知识,我一直试图想办法制作一个可扩展的单位转换器.

这是我的开始,以及我的一些计划的代码评论.我没有计划像下面那样使用它,我只是测试了C#的一些功能我不太了解,我也不确定如何进一步采取这一点.有没有人对如何在下面的评论中创建我正在谈论的内容有任何建议?谢谢

namespace TemperatureConverter
{
    class Program
    {
        static void Main(string[] args)
        {
            // Fahrenheit to Celsius :  [°C] = ([°F] ? 32) × 5?9
            var CelsiusResult = Converter.Convert(11M,Converter.FahrenheitToCelsius);

            // Celsius to Fahrenheit : [°F] = [°C] × 9?5 + 32
            var FahrenheitResult = Converter.Convert(11M, Converter.CelsiusToFahrenheit);

            Console.WriteLine("Fahrenheit to Celsius : " + CelsiusResult);
            Console.WriteLine("Celsius to Fahrenheit : " + FahrenheitResult);
            Console.ReadLine();

            // If I wanted to add another unit of temperature i.e. Kelvin 
            // then I would need calculations for Kelvin to Celsius, Celsius to Kelvin, Kelvin to Fahrenheit, Fahrenheit to Kelvin
            // Celsius to Kelvin : [K] = [°C] + 273.15
            // Kelvin to Celsius : [°C] = [K] ? 273.15
            // Fahrenheit to Kelvin : [K] = ([°F] + 459.67) × 5?9
            // Kelvin to Fahrenheit : [°F] = [K] × 9?5 ? 459.67
            // The plan is to have the converters with a single purpose to convert to
            //one particular unit type e.g. Celsius and create separate unit converters 
            //that contain a list of calculations that take one specified unit type and then convert to their particular unit type, in this example its Celsius.
        }
    }

    // at the moment this is a static class but I am looking to turn this into an interface or abstract class
    // so that whatever implements this interface would be supplied with a list of generic deligate conversions
    // that it can invoke and you can extend by adding more when required.
    public static class Converter
    {
        public static Func<decimal, decimal> CelsiusToFahrenheit = x => (x * (9M / 5M)) + 32M;
        public static Func<decimal, decimal> FahrenheitToCelsius = x => (x - 32M) * (5M / 9M);

        public static decimal Convert(decimal valueToConvert, Func<decimal, decimal> conversion) {
            return conversion.Invoke(valueToConvert);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:试图澄清我的问题:

使用下面的我的温度示例,我将如何创建一个包含lambda转换列表到Celsius的类,然后将其传递给定温度,然后尝试将其转换为摄氏度(如果计算可用)

伪代码示例:

enum Temperature
{
    Celcius,
    Fahrenheit,
    Kelvin
}

UnitConverter CelsiusConverter = new UnitConverter(Temperature.Celsius);
CelsiusConverter.AddCalc("FahrenheitToCelsius", lambda here);
CelsiusConverter.Convert(Temperature.Fahrenheit, 11);
Run Code Online (Sandbox Code Playgroud)

Dan*_*eny 24

我认为这是一个有趣的小问题,所以我决定看看这可以很好地包含在一个通用的实现中.这没有经过充分测试(并且不处理所有错误情况 - 例如,如果您没有为特定单元类型注册转换,则将其传入),但它可能很有用.重点是让继承的class(TemperatureConverter)尽可能整洁.

/// <summary>
/// Generic conversion class for converting between values of different units.
/// </summary>
/// <typeparam name="TUnitType">The type representing the unit type (eg. enum)</typeparam>
/// <typeparam name="TValueType">The type of value for this unit (float, decimal, int, etc.)</typeparam>
abstract class UnitConverter<TUnitType, TValueType>
{
    /// <summary>
    /// The base unit, which all calculations will be expressed in terms of.
    /// </summary>
    protected static TUnitType BaseUnit;

    /// <summary>
    /// Dictionary of functions to convert from the base unit type into a specific type.
    /// </summary>
    static ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>> ConversionsTo = new ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>>();

    /// <summary>
    /// Dictionary of functions to convert from the specified type into the base unit type.
    /// </summary>
    static ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>> ConversionsFrom = new ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>>();

    /// <summary>
    /// Converts a value from one unit type to another.
    /// </summary>
    /// <param name="value">The value to convert.</param>
    /// <param name="from">The unit type the provided value is in.</param>
    /// <param name="to">The unit type to convert the value to.</param>
    /// <returns>The converted value.</returns>
    public TValueType Convert(TValueType value, TUnitType from, TUnitType to)
    {
        // If both From/To are the same, don't do any work.
        if (from.Equals(to))
            return value;

        // Convert into the base unit, if required.
        var valueInBaseUnit = from.Equals(BaseUnit)
                                ? value
                                : ConversionsFrom[from](value);

        // Convert from the base unit into the requested unit, if required
        var valueInRequiredUnit = to.Equals(BaseUnit)
                                ? valueInBaseUnit
                                : ConversionsTo[to](valueInBaseUnit);

        return valueInRequiredUnit;
    }

    /// <summary>
    /// Registers functions for converting to/from a unit.
    /// </summary>
    /// <param name="convertToUnit">The type of unit to convert to/from, from the base unit.</param>
    /// <param name="conversionTo">A function to convert from the base unit.</param>
    /// <param name="conversionFrom">A function to convert to the base unit.</param>
    protected static void RegisterConversion(TUnitType convertToUnit, Func<TValueType, TValueType> conversionTo, Func<TValueType, TValueType> conversionFrom)
    {
        if (!ConversionsTo.TryAdd(convertToUnit, conversionTo))
            throw new ArgumentException("Already exists", "convertToUnit");
        if (!ConversionsFrom.TryAdd(convertToUnit, conversionFrom))
            throw new ArgumentException("Already exists", "convertToUnit");
    }
}
Run Code Online (Sandbox Code Playgroud)

泛型类型args用于表示单位的枚举,以及值的类型.要使用它,您只需继承此类(提供类型)并注册一些lambdas来进行转换.这是温度的一个例子(有一些虚拟计算):

enum Temperature
{
    Celcius,
    Fahrenheit,
    Kelvin
}

class TemperatureConverter : UnitConverter<Temperature, float>
{
    static TemperatureConverter()
    {
        BaseUnit = Temperature.Celcius;
        RegisterConversion(Temperature.Fahrenheit, v => v * 2f, v => v * 0.5f);
        RegisterConversion(Temperature.Kelvin, v => v * 10f, v => v * 0.05f);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后使用它非常简单:

var converter = new TemperatureConverter();

Console.WriteLine(converter.Convert(1, Temperature.Celcius, Temperature.Fahrenheit));
Console.WriteLine(converter.Convert(1, Temperature.Fahrenheit, Temperature.Celcius));

Console.WriteLine(converter.Convert(1, Temperature.Celcius, Temperature.Kelvin));
Console.WriteLine(converter.Convert(1, Temperature.Kelvin, Temperature.Celcius));

Console.WriteLine(converter.Convert(1, Temperature.Kelvin, Temperature.Fahrenheit));
Console.WriteLine(converter.Convert(1, Temperature.Fahrenheit, Temperature.Kelvin));
Run Code Online (Sandbox Code Playgroud)


Kei*_*thS 5

你有一个良好的开端,但像乔恩所说,它目前不是类型安全的; 转换器没有错误检查,以确保它获得的小数是一个摄氏度值.

因此,为了更进一步,我将开始介绍采用数值并将其应用于度量单位的结构类型.在企业架构模式(又称四人组设计模式)中,在最常见的用法之后,这被称为"货币"模式,以表示一种货币类型.该模式适用于需要度量单位有意义的任何数字量.

例:

public enum TemperatureScale
{
   Celsius,
   Fahrenheit,
   Kelvin
}

public struct Temperature
{
   decimal Degrees {get; private set;}
   TemperatureScale Scale {get; private set;}

   public Temperature(decimal degrees, TemperatureScale scale)
   {
       Degrees = degrees;
       Scale = scale;
   }

   public Temperature(Temperature toCopy)
   {
       Degrees = toCopy.Degrees;
       Scale = toCopy.Scale;
   }
}
Run Code Online (Sandbox Code Playgroud)

现在,您有一个简单的类型,您可以使用它来强制执行您正在进行的转换采用适当比例的温度,并返回结果已知温度在另一个比例中.

您的Func将需要一个额外的行来检查输入是否与输出匹配; 你可以继续使用lambdas,或者你可以通过一个简单的策略模式更进一步:

public interface ITemperatureConverter
{
   public Temperature Convert(Temperature input);
}

public class FahrenheitToCelsius:ITemperatureConverter
{
   public Temperature Convert(Temperature input)
   {
      if (input.Scale != TemperatureScale.Fahrenheit)
         throw new ArgumentException("Input scale is not Fahrenheit");

      return new Temperature(input.Degrees * 5m / 9m - 32, TemperatureScale.Celsius);
   }
}

//Implement other conversion methods as ITemperatureConverters

public class TemperatureConverter
{
   public Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter> converters = 
      new Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter>
      {
         {Tuple.Create<TemperatureScale.Fahrenheit, TemperatureScale.Celcius>,
            new FahrenheitToCelsius()},
         {Tuple.Create<TemperatureScale.Celsius, TemperatureScale.Fahrenheit>,
            new CelsiusToFahrenheit()},
         ...
      }

   public Temperature Convert(Temperature input, TemperatureScale toScale)
   {
      if(!converters.ContainsKey(Tuple.Create(input.Scale, toScale))
         throw new InvalidOperationException("No converter available for this conversion");

      return converters[Tuple.Create(input.Scale, toScale)].Convert(input);
   }
}
Run Code Online (Sandbox Code Playgroud)

因为这些类型的转换是双向的,所以您可以考虑设置接口来处理两种方式,使用"ConvertBack"方法或类似方法,它将采用摄氏温度的温度并转换为华氏温度.这会减少你的课数.然后,您的字典值可以指向转换器实例上的方法,而不是类实例.这增加了设置主要TemperatureConverter策略选择器的复杂性,但减少了必须定义的转换策略类的数量.

还要注意,当您实际尝试进行转换时,错误检查是在运行时完成的,要求在所有使用中对此代码进行彻底测试,以确保它始终正确.为了避免这种情况,您可以派生基本Temperature类来生成CelsiusTemperature和FahrenheitTemperature结构,这将简单地将它们的Scale定义为常量值.然后,ITemperatureConverter可以通用两种类型,即温度,为您提供编译时检查,指定您认为自己的转换.TemperatureConverter还可以动态查找ITemperatureConverters,确定它们之间的转换类型,并自动设置转换器字典,这样您就不必担心添加新的转换器.这是以增加基于温度的班级计数为代价的; 你需要四个域类(一个基类和三个派生类)而不是一个.它还会减慢TemperatureConverter类的创建速度,因为反射构建转换器字典的代码将使用相当多的反射.

您还可以将度量单位的枚举更改为"标记类"; 除了它们属于该类并且从其他类派生之外没有任何意义的空类.然后,您可以定义表示各种度量单位的"UnitOfMeasure"类的完整层次结构,并且可以用作泛型类型参数和约束; ITemperatureConverter可以是两种类型的通用,这两种类型都被约束为TemperatureScale类,而CelsiusFahrenheitConverter实现将关闭通用接口到CelsiusDegrees和FahrenheitDegrees类型,这两种类型都是从TemperatureScale派生的.这允许您将测量单位本身作为转换的约束公开,从而允许在测量单位类型之间进行转换(某些材料的某些单位具有已知转换; 1英国帝国品脱水重1.25磅).

所有这些都是设计决策,它将简化这种设计的一种变化,但需要付出一些代价(要么做出其他难以做到的事情,要么降低算法性能).在您所使用的整体应用程序和编码环境中,您可以自行决定什么是"非常简单".

编辑:从编辑开始,您想要的使用温度非常容易.但是,如果您想要一个可以使用任何UnitofMeasure的通用UnitConverter,那么您不再需要Enums来表示您的度量单位,因为Enums不能具有自定义继承层次结构(它们直接从System.Enum派生).

您可以指定默认构造函数可以接受任何枚举,但是您必须确保Enum是作为度量单位的类型之一,否则您可以传入DialogResult值并且转换器将在运行时发生故障.

相反,如果你想要一个可以转换为任何UnitOfMeasure的UnitConverter给定其他测量单位的lambdas,我会将度量单位指定为"标记类"; 小的无国籍"代币",只有它们是他们自己的类型并且来自他们的父母才有意义:

//The only functionality any UnitOfMeasure needs is to be semantically equatable
//with any other reference to the same type.
public abstract class UnitOfMeasure:IEquatable<UnitOfMeasure> 
{ 
   public override bool Equals(UnitOfMeasure other)
   {
      return this.ReferenceEquals(other)
         || this.GetType().Name == other.GetType().Name;
   }

   public override bool Equals(Object other) 
   {
      return other is UnitOfMeasure && this.Equals(other as UnitOfMeasure);
   }    

   public override operator ==(Object other) {return this.Equals(other);}
   public override operator !=(Object other) {return this.Equals(other) == false;}

}

public abstract class Temperature:UnitOfMeasure {
public static CelsiusTemperature Celsius {get{return new CelsiusTemperature();}}
public static FahrenheitTemperature Fahrenheit {get{return new CelsiusTemperature();}}
public static KelvinTemperature Kelvin {get{return new CelsiusTemperature();}}
}
public class CelsiusTemperature:Temperature{}
public class FahrenheitTemperature :Temperature{}
public class KelvinTemperature :Temperature{}

...

public class UnitConverter
{
   public UnitOfMeasure BaseUnit {get; private set;}
   public UnitConverter(UnitOfMeasure baseUnit) {BaseUnit = baseUnit;}

   private readonly Dictionary<UnitOfMeasure, Func<decimal, decimal>> converters
      = new Dictionary<UnitOfMeasure, Func<decimal, decimal>>();

   public void AddConverter(UnitOfMeasure measure, Func<decimal, decimal> conversion)
   { converters.Add(measure, conversion); }

   public void Convert(UnitOfMeasure measure, decimal input)
   { return converters[measure](input); }
}
Run Code Online (Sandbox Code Playgroud)

您可以根据需要进行错误检查(检查输入单元是否指定了转换,检查正在添加的转换是针对具有与基本类型相同的父级的UOM等).您还可以派生UnitConverter来创建TemperatureConverter,允许您添加静态编译时类型检查并避免UnitConverter必须使用的运行时检查.