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)
你有一个良好的开端,但像乔恩所说,它目前不是类型安全的; 转换器没有错误检查,以确保它获得的小数是一个摄氏度值.
因此,为了更进一步,我将开始介绍采用数值并将其应用于度量单位的结构类型.在企业架构模式(又称四人组设计模式)中,在最常见的用法之后,这被称为"货币"模式,以表示一种货币类型.该模式适用于需要度量单位有意义的任何数字量.
例:
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必须使用的运行时检查.