Как создать универсальный конвертер для единиц измерения в C#?

Я пытался узнать немного больше о делегатах и лямбдах во время работы над небольшим кулинарным проектом, который включает преобразование температуры, а также некоторые преобразования измерения приготовления пищи, такие как Imperial в Metric, и я пытался придумать способ сделать расширяемый конвертер единиц.

вот с чего я начал, вместе с комментариями кода о том, что некоторые из моих планов были. Я не планирую использовать его, как показано ниже, я просто тестировал некоторые функции C# I не очень хорошо знаю, я также не уверен, как это сделать дальше. Есть ли у кого-нибудь предложения о том, как создать то, о чем я говорю в комментариях ниже? Спасибо

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

6 ответов


Я подумал, что это интересная небольшая проблема, поэтому я решил посмотреть, насколько хорошо это может быть обернуто в общую реализацию. Это не хорошо протестировано (и не обрабатывает все случаи ошибок - например, если вы не регистрируете преобразование для определенного типа единицы, а затем передаете это), но это может быть полезно. Основное внимание уделялось созданию унаследованного класса (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 универсального типа для перечисления, представляющего единицы измерения, и тип значения. Чтобы использовать его, вам просто нужно наследовать от этого класса (предоставляя типы) и зарегистрировать некоторые лямбды для преобразования. Вот пример для температуры (с некоторыми фиктивными расчетами):

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

Теперь у вас есть простой тип, который вы можете использовать для обеспечения того, чтобы преобразования, которые вы делаете, принимали температуру надлежащего масштаба и возвращали температуру результата, известную в другом масштабе.

вашим Функс будет нужна дополнительная строка для проверки что входной сигнал соответствует выходу; вы можете продолжать использовать лямбда, или вы можете сделайте еще один шаг вперед с помощью простого шаблона стратегии:

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 strategy-picker, но уменьшает количество классов стратегий преобразования, которые необходимо определить.

также обратите внимание, что проверка ошибок выполняется во время выполнения, когда вы фактически пытаетесь сделать преобразование, требуя, чтобы этот код был тщательно протестирован во всех обычаях, чтобы убедиться, что он всегда правильный. Чтобы избежать этого, можно получить базовый класс температуры для создания структур CelsiusTemperature и FahrenheitTemperature, которые просто определяют их масштаб как постоянное значение. Затем ITemperatureConverter можно сделать общим для двух типов, обеих температур, давая вам время компиляции, проверяя, что вы указываете преобразование, которое вы думаете. TemperatureConverter также можно заставить динамически находить ITemperatureConverters, определять типы, которые они будут преобразовывать, и автоматически настраивать словарь конвертеров, чтобы вам никогда не приходилось беспокоиться о добавлении новых. Это происходит за счет увеличения Количество классов на основе температуры; вам понадобятся четыре класса домена (базовый и три производных класса) вместо одного. Это также замедлит создание класса TemperatureConverter, поскольку код для рефлексивного построения словаря конвертера будет использовать довольно много отражения.

вы также можете изменить перечисления для единиц измерения, чтобы стать "классами маркеров"; пустые классы, которые не имеют никакого значения, кроме того, что они относятся к этому классу и являются производными от других классов. Тогда ты мог бы определите полную иерархию классов "UnitOfMeasure", которые представляют различные единицы измерения и могут использоваться в качестве аргументов и ограничений универсального типа; ITemperatureConverter может быть общим для двух типов, оба из которых ограничены классами TemperatureScale, а реализация CelsiusFahrenheitConverter закроет общий интерфейс для типов CelsiusDegrees и FahrenheitDegrees, производных от TemperatureScale. Это позволяет выставлять сами единицы измерения как ограничения конверсии, в свою очередь, допускающие конверсии между типами единиц измерения (некоторые единицы определенных материалов имеют известные конверсии; 1 британская имперская Пинта воды весит 1,25 фунта).

все это проектные решения, которые упростят один тип изменений в этом дизайне, но с некоторой стоимостью (либо что-то еще сложнее сделать, либо снижение производительности алгоритма). Это зависит от вас, чтобы решить, что действительно "легко" для вас, в контексте общего среда применения и кодирования, в которой вы работаете.

EDIT: использование вы хотите, от вашего редактирования, очень легко для температуры. Однако если вы хотите универсальный UnitConverter, который может работать с любой UnitofMeasure, то вы больше не хотите перечисления для представления ваших единиц измерения, потому что перечисления не могут иметь пользовательскую иерархию наследования (они происходят непосредственно из системы.Перечисление.)

вы можете указать, что конструктор по умолчанию может принимать любое перечисление, но затем вы должны убедиться, что перечисление является одним из типов, который является единицей измерения, иначе вы можете передать значение DialogResult, и преобразователь выйдет из строя во время выполнения.

вместо этого, если вы хотите один UnitConverter, который может конвертировать в любой UnitOfMeasure заданные лямбды для других единиц измерения, я бы указал единицы измерения как "классы маркеров"; небольшие безгосударственные "токены", которые имеют значение только в том, что они являются их собственным типом и происходят от их родители:

//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 будет использовать.


похоже, вы хотите что-то вроде:

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.NET - ... Он включен!--2-->GitHub и NuGet для. Он обеспечивает наиболее распространенные единицы и преобразования, поддерживает как статическую типизацию, так и перечисление единиц и разбор / печать аббревиатур. Однако он не анализирует выражения, и вы не можете расширить существующие классы единиц, но вы можете расширить его новыми сторонними единицами.

пример преобразования:

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

обычно я хотел добавить это в качестве комментария к посту Дэнни 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));
    }
}

можно определить общий тип физических единиц, если для каждой единицы имеется тип, реализующий 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 wantsAreaUnit ' может конвертировать дюймы в ангстремы, а затем в сантиметры.