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

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

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

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 ie 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 eg 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 CelsiusToFahrenheit = x => (x * (9M / 5M)) + 32M; public static Func FahrenheitToCelsius = x => (x - 32M) * (5M / 9M); public static decimal Convert(decimal valueToConvert, Func conversion) { return conversion.Invoke(valueToConvert); } } } 

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

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

伪代码示例:

 enum Temperature { Celcius, Fahrenheit, Kelvin } UnitConverter CelsiusConverter = new UnitConverter(Temperature.Celsius); CelsiusConverter.AddCalc("FahrenheitToCelsius", lambda here); CelsiusConverter.Convert(Temperature.Fahrenheit, 11); 

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

 ///  /// Generic conversion class for converting between values of different units. ///  /// The type representing the unit type (eg. enum) /// The type of value for this unit (float, decimal, int, etc.) abstract class UnitConverter { ///  /// The base unit, which all calculations will be expressed in terms of. ///  protected static TUnitType BaseUnit; ///  /// Dictionary of functions to convert from the base unit type into a specific type. ///  static ConcurrentDictionary> ConversionsTo = new ConcurrentDictionary>(); ///  /// Dictionary of functions to convert from the specified type into the base unit type. ///  static ConcurrentDictionary> ConversionsFrom = new ConcurrentDictionary>(); ///  /// Converts a value from one unit type to another. ///  /// The value to convert. /// The unit type the provided value is in. /// The unit type to convert the value to. /// The converted value. 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; } ///  /// Registers functions for converting to/from a unit. ///  /// The type of unit to convert to/from, from the base unit. /// A function to convert from the base unit. /// A function to convert to the base unit. protected static void RegisterConversion(TUnitType convertToUnit, Func conversionTo, Func conversionFrom) { if (!ConversionsTo.TryAdd(convertToUnit, conversionTo)) throw new ArgumentException("Already exists", "convertToUnit"); if (!ConversionsFrom.TryAdd(convertToUnit, conversionFrom)) throw new ArgumentException("Already exists", "convertToUnit"); } } 

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

 enum Temperature { Celcius, Fahrenheit, Kelvin } class TemperatureConverter : UnitConverter { 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 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, ITemperatureConverter> converters = new Dictionary, ITemperatureConverter> { {Tuple.Create, new FahrenheitToCelsius()}, {Tuple.Create, 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); } } 

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

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

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

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

编辑:您想要的用途,从您的编辑,非常容易温度。 但是,如果您想要一个可以使用任何UnitofMeasure的通用UnitConverter,那么您不再需要Enums来表示您的度量单位,因为Enums不能具有自定义inheritance层次结构(它们直接从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 { 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> converters = new Dictionary>(); public void AddConverter(UnitOfMeasure measure, Func conversion) { converters.Add(measure, conversion); } public void Convert(UnitOfMeasure measure, decimal input) { return converters[measure](input); } } 

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

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

 Func celsiusToKelvin = x => x + 273.15m; Func kelvinToCelsius = x => x - 273.15m; Func fahrenheitToKelvin = x => ((x + 459.67m) * 5m) / 9m; Func kelvinToFahrenheit = x => ((x * 9m) / 5m) - 459.67m; 

但是,您可能不仅要考虑使用decimal ,而且要考虑一个知道单位的类型,这样您就不会意外地(比方说)将“Celsius to Kelvin”转换应用于非Celsius值。 可能会看一下F#测量单位方法的灵感。

你可以看看Units.NET。 它在GitHub和NuGet上 。 它提供了最常见的单元和转换,支持静态类型和单元枚举以及解析/打印缩写。 它不解析表达式,并且您不能扩展现有的单元类,但您可以使用新的第三方单元扩展它。

转换示例:

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

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

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

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

 public enum TimeUnit { Milliseconds, Second, Minute, Hour, Day, Week } public class TimeConverter : UnitConverter { 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类:

 ///  /// Generic conversion class for converting between values of different units. ///  /// The type representing the unit type (eg. enum) /// The type of value for this unit (float, decimal, int, etc.) /// http://stackoverflow.com/questions/7851448/how-do-i-create-a-generic-converter-for-units-of-measurement-in-c ///  public abstract class UnitConverter where TValueType : struct, IComparable, IComparable, IEquatable, IConvertible { ///  /// The base unit, which all calculations will be expressed in terms of. ///  protected static TUnitType BaseUnit; ///  /// Dictionary of functions to convert from the base unit type into a specific type. ///  static ConcurrentDictionary> ConversionsTo = new ConcurrentDictionary>(); ///  /// Dictionary of functions to convert from the specified type into the base unit type. ///  static ConcurrentDictionary> ConversionsFrom = new ConcurrentDictionary>(); ///  /// Converts a value from one unit type to another. ///  /// The value to convert. /// The unit type the provided value is in. /// The unit type to convert the value to. /// The converted value. 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); } ///  /// Registers functions for converting to/from a unit. ///  /// The type of unit to convert to/from, from the base unit. /// a factor converting into the base unit. 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 multiply = Expression.Lambda>(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 divide = Expression.Lambda>(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)); } } 

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