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

如何为C#中的测量单位创建通用转换器?,c#,.net,generics,delegates,units-of-measurement,C#,.net,Generics,Delegates,Units Of Measurement,我一直在努力学习更多关于代表和lambda的知识,同时正在进行一个小型烹饪项目,该项目涉及温度转换以及一些烹饪测量转换,如英制到公制,我一直在努力思考一种制作可扩展单位转换器的方法 以下是我开始的内容,以及关于我的一些计划的代码注释。我不打算像下面那样使用它,我只是测试了C#的一些特性,我不太清楚,我也不确定如何进一步使用它。有人对如何创造我在下面的评论中所说的东西有什么建议吗?谢谢 namespace TemperatureConverter { class Program {

我一直在努力学习更多关于代表和lambda的知识,同时正在进行一个小型烹饪项目,该项目涉及温度转换以及一些烹饪测量转换,如英制到公制,我一直在努力思考一种制作可扩展单位转换器的方法

以下是我开始的内容,以及关于我的一些计划的代码注释。我不打算像下面那样使用它,我只是测试了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);
        }
    }
}

听起来你想要的是:

Func<decimal, decimal> celsiusToKelvin = x => x + 273.15m;
Func<decimal, decimal> kelvinToCelsius = x => x - 273.15m;
Func<decimal, decimal> fahrenheitToKelvin = x => ((x + 459.67m) * 5m) / 9m;
Func<decimal, decimal> kelvinToFahrenheit = x => ((x * 9m) / 5m) - 459.67m;
Func celsiusToKelvin=x=>x+273.15m;
Func kelvinToCelsius=x=>x-273.15m;
Func华氏开尔文=x=>((x+459.67m)*5m)/9m;
Func kelvinToFahrenheit=x=>((x*9m)/5m)-459.67m;

但是,你可能要考虑的不仅仅是使用<代码>十进制< /代码>,而是有一个知道单元的类型,这样你就不会意外地(比如说)将“摄氏到欧凯文”转换为非摄氏度值。也许你可以看看这个方法来获得灵感。

你有一个很好的开始,但正如乔恩所说,它目前不是类型安全的;转换器没有错误检查,以确保其获取的十进制值为摄氏值

因此,为了更进一步,我将开始引入结构类型,这些结构类型接受数值并将其应用于度量单位。在企业架构模式(也称为四人组设计模式)中,这在最常见的用法之后被称为“货币”模式,表示一种货币的数量。该模式适用于任何需要度量单位才有意义的数字量

例如:

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;
   }
}
现在,您有了一个简单的类型,您可以使用它强制执行正在进行的转换采用适当刻度的温度,并返回另一刻度的结果温度

您的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);
   }
}
公共接口项温度转换器
{
公共温度转换(温度输入);
}
公共类华氏摄氏度:ItemTemperatureConverter
{
公共温度转换(温度输入)
{
if(input.Scale!=温度刻度.华氏度)
抛出新的ArgumentException(“输入刻度不是华氏度”);
返回新温度(输入0.5摄氏度*5米/9米-32,温度刻度0.5摄氏度);
}
}
//将其他转换方法实现为ItemTemperatureConverters
公共类温度转换器
{
公共字典转换器=
新词典
{
{Tuple.Create,
新的华氏温度(),
{Tuple.Create,
新的CelsiusToFahrenheit()},
...
}
公共温度转换(温度输入、温度刻度到刻度)
{
if(!converters.ContainsKey(Tuple.Create(input.Scale,toScale))
抛出新的InvalidOperationException(“没有可用于此转换的转换器”);
返回转换器[Tuple.Create(input.Scale,toScale)].Convert(input);
}
}
因为这些类型的转换是双向的,您可以考虑设置接口来处理两种方式,并使用“转换回”。方法或类似方法,将温度转换为摄氏度并转换为华氏度。这会减少类计数。然后,您的字典值可以作为指向转换器实例上方法的指针,而不是类实例。这在一定程度上增加了设置主TemperatureConverter策略选择器的复杂性,但减少必须定义的转换策略类的数量

还请注意,错误检查是在运行时进行的,当您实际尝试进行转换时,需要对该代码在所有用法中进行彻底测试,以确保其始终正确。为了避免这种情况,您可以派生基本温度类以生成CelsiustTemperature和FahrenheiteTemperature结构,这将简单地定义t然后,ItemTemperatureConverter可以泛化为两种类型,两种温度,让您在编译时检查是否指定了您认为是的转换。TemperatureConverter还可以动态查找ItemTemperatureConverter,确定它们将在哪种类型之间转换,以及以逻辑方式设置转换器字典,这样您就不必担心添加新的转换器。这是以增加基于温度的类计数为代价的;您将需要四个域类(一个基类和三个派生类)它还将减慢TemperatureConverter类的创建,因为以反射方式构建转换器字典的代码将使用相当多的反射

您还可以将度量单位的枚举更改为“标记类”;空类除了属于该类并派生自其他类之外没有其他意义。然后您可以定义“度量单位”的完整层次结构表示各种度量单位的类,可以用作泛型类型参数和约束;ItemTemperatureConverter可以泛型为两种类型,这两种类型都被约束为TemperatureScale类,CelsiusFahrenheitConverter实现将关闭CelsiusDegrees和Fahrenh类型的泛型接口EIT度数均源自温度刻度。这允许您将度量单位本身作为转换的约束条件,从而允许在不同类型的度量单位之间进行转换(某些材料的某些单位具有已知的转换;1英制品脱水重1.25磅)

所有这些都是设计决策,将简化此设计的一种更改类型,但在某些情况下
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);
   }
}
//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); }
}
/// <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");
    }
}
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);
    }
}
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));
public enum TimeUnit
{
    Milliseconds,
    Second,
    Minute,
    Hour,
    Day,
    Week
}

public class TimeConverter : UnitConverter<TimeUnit, double>
{
    static TimeConverter()
    {
        BaseUnit = TimeUnit.Second;
        RegisterConversion(TimeUnit.Milliseconds, 1000);
        RegisterConversion(TimeUnit.Minute, 1/60);
        RegisterConversion(TimeUnit.Hour, 1/3600);
        RegisterConversion(TimeUnit.Day, 1/86400);
        RegisterConversion(TimeUnit.Week, 1/604800);
    }
}
/// <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>
/// <remarks>http://stackoverflow.com/questions/7851448/how-do-i-create-a-generic-converter-for-units-of-measurement-in-c
/// </remarks>
public abstract class UnitConverter<TUnitType, TValueType> where TValueType : struct, IComparable, IComparable<TValueType>, IEquatable<TValueType>, IConvertible
{
    /// <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;
    }

    public double ConversionFactor(TUnitType from, TUnitType to)
    {
        return Convert(One(), from, to).ToDouble(CultureInfo.InvariantCulture);
    }

    /// <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="conversionToFactor">a factor converting into the base unit.</param>
    protected static void RegisterConversion(TUnitType convertToUnit, TValueType conversionToFactor)
    {
        if (!ConversionsTo.TryAdd(convertToUnit, v=> Multiply(v, conversionToFactor)))
            throw new ArgumentException("Already exists", "convertToUnit");

        if (!ConversionsFrom.TryAdd(convertToUnit, v => MultiplicativeInverse(conversionToFactor)))
            throw new ArgumentException("Already exists", "convertToUnit");
    }

    static TValueType Multiply(TValueType a, TValueType b)
    {
        // declare the parameters
        ParameterExpression paramA = Expression.Parameter(typeof(TValueType), "a");
        ParameterExpression paramB = Expression.Parameter(typeof(TValueType), "b");
        // add the parameters together
        BinaryExpression body = Expression.Multiply(paramA, paramB);
        // compile it
        Func<TValueType, TValueType, TValueType> multiply = Expression.Lambda<Func<TValueType, TValueType, TValueType>>(body, paramA, paramB).Compile();
        // call it
        return multiply(a, b);
    }

    static TValueType MultiplicativeInverse(TValueType b)
    {
        // declare the parameters
        ParameterExpression paramA = Expression.Parameter(typeof(TValueType), "a");
        ParameterExpression paramB = Expression.Parameter(typeof(TValueType), "b");
        // add the parameters together
        BinaryExpression body = Expression.Divide(paramA, paramB);
        // compile it
        Func<TValueType, TValueType, TValueType> divide = Expression.Lambda<Func<TValueType, TValueType, TValueType>>(body, paramA, paramB).Compile();
        // call it
        return divide(One(), b);
    }

    //Returns the value "1" as converted Type
    static TValueType One()
    {
        return (TValueType) System.Convert.ChangeType(1, typeof (TValueType));
    }
}
Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double feet = meter.Feet; // 3.28084