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