2011-10-21 20 views
6

我一直在努力學習更多關於代表和lambdas的知識,同時在涉及溫度轉換以及一些烹飪測量轉換(如Imperial到Metric)的小型烹飪項目上工作時,我一直在想辦法制作一個可擴展的單位轉換器。如何爲C#中的度量單位創建通用轉換器?

這是我開始的,以及代碼評論我的一些計劃是什麼。我沒有計劃像下面那樣使用它,我只是測試了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); 
     } 
    } 
} 

更新:試圖澄清我的問題:

使用以下只是我的溫度例子,我將如何創建一個包含拉姆達轉換的列表,以攝氏一類你再傳遞一個給定的溫度,它會嘗試將其轉換成攝氏(如果計算是可用的)

實施例的僞代碼:

enum Temperature 
{ 
    Celcius, 
    Fahrenheit, 
    Kelvin 
} 

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

單位在F#被支持,我認爲它可以是一個很好[特徵](https://github.com/dotnet/roslyn/issues/144)爲C# vNext。現在我發現這個項目[QuantityTypes](https://github.com/objorke/QuantityTypes),它實現了C#中的度量單位。 – orad

回答

21

我認爲這是一個有趣的小問題,所以我決定看看它可以如何很好地包裝到一個通用的實現。這沒有經過充分測試(並且不處理所有錯誤情況 - 例如,如果您沒有爲特定單位類型註冊轉換,然後通過),但它可能很有用。重點是讓繼承的類(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用於表示單位的枚舉和值的類型。要使用它,你只需要從這個類繼承(提供類型)並註冊一些lambda來進行轉換。下面是溫度的例子(有一些虛擬計算):

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

完全是我以後,很好的回答:)很多通讀和充分理解兩個,但非常感謝幫助。 – Pricey

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,但有這樣一種類型,知道單位這樣你就不會意外(比如)應用「攝氏到開氏」轉換爲非攝氏值。可能看看F# Units of Measure方法的靈感。

+0

是的,我試圖讓我可以有一個類實現單位轉換器接口,並且該類只知道如何將溫度類型列表轉換爲攝氏溫度,但然後我可以創建一個全新的類實現單位轉換器接口,並知道如何將杯測量轉換成湯匙或英制到公制等。我將嘗試編輯我的問題來進一步解釋我自己。再次感謝F#測量單位鏈接:) – Pricey

+0

抱歉,帝國公制是我上面評論中的一個不好的例子,更好的一個是英鎊到克,這是更具體。 – Pricey

5

你有一個好的開始,但像喬恩說的那樣,它目前不是類型安全的;轉換器沒有錯誤檢查,以確保它得到的十進制是一個攝氏度值。

因此,爲了進一步研究,我將開始引入結構類型,它將數值應用到度量單位。在企業架構模式(也稱爲四人組設計模式)中,在最常見的用法之後,這被稱爲「貨幣」模式,以表示一種貨幣類型。該模式適用於任何需要度量單位有意義的數字量。

例子:

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

現在,你有一個簡單的類型,你可以用它來執行,你都使得轉換需要的溫度是適當的規模,並返回已知結果溫度在其他範圍內。

你的Funcs將需要一個額外的行來檢查輸入是否與輸出匹配;您可以繼續使用lambda表達式,或者你可以用一個簡單的策略模式邁出了這一步:

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

由於這些類型的轉換是雙向的,你可以考慮建立接口來處理兩種方式,採用「ConvertBack」方法或類似方法,將攝氏溫度的溫度轉換爲華氏溫標。這減少了你的班級數量。然後,而不是類實例,您的字典值可能是指向轉換器實例上方法的指針。這會增加設置主要TemperatureConverter策略選擇器的複雜度,但會減少您必須定義的轉換策略類的數量。

另請注意,錯誤檢查是在運行時完成的,當您嘗試進行轉換時,需要在所有用法中對此代碼進行徹底測試,以確保它始終正確。爲了避免這種情況,您可以派生基本溫度等級來生成攝氏溫度和華氏溫度結構,它們將簡單地將它們的比例定義爲常數值。然後,IT溫度轉換器可以分爲兩種類型,包括溫度,通過編譯時檢查您是否指定了自己認爲的轉換。 TemperatureConverter還可以動態查找ITemperatureConverters,確定它們之間將要轉換的類型,並自動設置轉換器字典,因此您無需擔心添加新的轉換器。這是以增加基於溫度的班級數量爲代價的;你需要四個域類(一個基類和三個派生類)而不是一個。它還會減慢TemperatureConverter類的創建速度,因爲反射式構建轉換器字典的代碼將使用相當多的反思。

您也可以將度量單位的枚舉更改爲「標記類」;沒有意義的空類,除了它們屬於那個類並從其他類派生。然後,您可以定義一個完整的「UnitOfMeasure」類的層次結構,它們表示各種度量單位,並可用作泛型類型參數和約束; ITemperatureConverter可以是兩種類型的通用類型,它們都被限制爲TemperatureScale類,而CelsiusFahrenheitConverter實現可以關閉通用接口,類型CelsiusDegrees和FahrenheitDegrees都源自TemperatureScale。這使您可以將計量單位本身作爲轉換的約束條件,從而允許在各種計量單位之間進行轉換(某些物料的某些單位具有已知的轉換; 1英制英制一品脫水重1.25磅)。所有這些都是設計決定,它將簡化對此設計的一種變化,但需要付出一定代價(要麼做出更難的事情,要麼降低算法性能)。它是由你來決定什麼是對你真的很「容易」,在整個應用程序的您工作的環境和編碼環境

編輯:你想,從你編輯的使用,對溫度極其容易。但是,如果你想要一個可以與任何UnitofMeasure一起工作的通用UnitUnverter,那麼你不再需要Enums來表示你的度量單位,因爲Enums不能有一個自定義的繼承層次結構(它們直接來自System.Enum)。

您可以指定默認構造函數可以接受任何枚舉,但是您必須確保枚舉是度量單位的類型之一,否則您可以傳入DialogResult值並且轉換器會發飆在運行時。如果你想要一個UnitConverter可以轉換爲任何UnitOfMeasure給定其他度量單位的lambda表達式,我會指定度量單位爲「標記類」;小無狀態「令牌」,只有具有意義,因爲它們自己的類型,並從他們的父母得到:

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

你可以把錯誤檢查(檢查輸入單元具有規定的變換,檢查一個正如您認爲合適的那樣,添加的轉換適用於具有與基本類型相同的父代的UOM等)。您也可以派生UnitConverter來創建TemperatureConverter,允許您添加靜態編譯時類型檢查並避免UnitConverter必須使用的運行時檢查。

+0

+1爲偉大的迴應和你的意見真的幫助點我沒有考慮 – Pricey

0

通常我想補充這是丹尼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)); 
    } 
} 
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 AreaUnit`的代碼可以將英寸轉換成埃,然後再轉換成釐米。

1

你可以看看Units.NET。它在GitHubNuGet。它提供了最常見的單位和轉換,支持靜態類型和枚舉單位以及解析/打印縮寫。儘管它不解析表達式,並且不能擴展現有類型的單位,但是可以使用新的第三方單位來擴展它。

實施例的轉換:測量的

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