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

时间:2011-10-21 15:10:12

标签: c# .net generics delegates units-of-measurement

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

以下是我的开始,以及我的一些计划的代码评论。我没有计划像下面这样使用它,我只是测试了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);
        }
    }
}

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

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

伪代码示例:

enum Temperature
{
    Celcius,
    Fahrenheit,
    Kelvin
}

UnitConverter CelsiusConverter = new UnitConverter(Temperature.Celsius);
CelsiusConverter.AddCalc("FahrenheitToCelsius", lambda here);
CelsiusConverter.Convert(Temperature.Fahrenheit, 11);

6 个答案:

答案 0 :(得分:23)

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

泛型类型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);
    }
}

然后使用它非常简单:

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

答案 1 :(得分:5)

答案 2 :(得分:3)

听起来你想要这样的东西:

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;

但是,您可能不仅要考虑使用decimal,还要考虑使用知道单位的类型,这样您就不会意外地(比方说)将“Celsius应用于开尔文”转换为非摄氏度值。可能会看一下F# Units of Measure的灵感方法。

答案 3 :(得分:1)

你可以看看Units.NET。它位于GitHubNuGet上。它提供了最常见的单元和转换,支持静态类型和单元枚举以及解析/打印缩写。它虽然没有解析表达式,但您无法扩展现有的单元类,但您可以使用新的第三方单元扩展它。

转换示例:

Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double feet = meter.Feet; // 3.28084

答案 4 :(得分:0)

通常我想将此添加为对Danny Tuppeny帖子的评论,但似乎我无法将其添加为评论。

我从@Danny Tuppeny稍微改进了解决方案。我不想用两个会话因素添加每个转换,因为只需要一个。此外,类型Func的参数似乎不是必需的,它只会使用户更加复杂。

所以我的电话会是这样的:

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

我还添加了一种方法来获取单位之间的转换因子。 这是修改后的UnitConverter类:

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

答案 5 :(得分:0)

可以定义一个物理单位泛型类型,这样,如果每个单元都有一个实现new并且包含该单元与该类型“基本单元”之间的转换方法的类型,则可以执行算术对以不同单位表示的值并根据需要进行转换,使用类型系统,使AreaUnit<LengthUnit.Inches>类型的变量只接受以平方英寸为单位的事物,但如果有人说myAreaInSquareInches= AreaUnit<LengthUnit.Inches>.Product(someLengthInCentimeters, someLengthInFathoms);它在执行乘法之前会自动翻译其他单位。当使用方法调用语法时,它实际上可以很好地工作,因为像Product<T1,T2>(T1 p1, T2 p2)方法这样的方法可以接受它们的操作数的泛型类型参数。不幸的是,没有办法让运算符变得通用,也没有办法让像AreaUnit<T> where T:LengthUnitDescriptor这样的类型定义转换到其他任意泛型AreaUnit<U>或从其他任意泛型AreaUnit<T>转换的方法。 AreaUnit<Angstrom>可以定义与之间的转换,例如AreaUnit<Centimeters> and wants,但是编译器无法告知给出{{1}} AreaUnit`的代码可以将英寸转换为埃,然后转换为厘米。