Как создать общий конвертер для единиц измерения в C #? - PullRequest
7 голосов
/ 21 октября 2011

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

Вот то, с чего я начал, а также комментарии к коду о том, каковы были некоторые из моих планов. Я не планирую использовать его, как показано ниже, я просто тестировал некоторые возможности 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);

Ответы [ 6 ]

24 голосов
/ 21 октября 2011

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

Аргументы универсального типа предназначены для перечисления, представляющего единицы, и типа для значения.Чтобы использовать его, вам просто нужно наследовать от этого класса (предоставляя типы) и зарегистрировать несколько лямбд, чтобы сделать преобразование.Вот пример для температуры (с некоторыми фиктивными вычислениями):

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));
5 голосов
/ 21 октября 2011

У вас хорошее начало, но, как сказал Джон, в настоящее время оно не безопасно для типов; в преобразователе нет проверки ошибок, чтобы убедиться, что полученное десятичное число является значением в градусах Цельсия.

Итак, чтобы продолжить, я бы начал вводить структурные типы, которые принимают числовое значение и применяют его к единице измерения. В шаблонах архитектуры предприятия (также называемых шаблонами проектирования «бригада четырех») это называется шаблоном «деньги» после наиболее распространенного использования для обозначения количества типа валюты. Шаблон действует для любой числовой суммы, которая требует значимости единицы измерения.

Пример:

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 понадобится дополнительная строка, чтобы проверить, соответствует ли ввод выводу; вы можете продолжать использовать лямбды или сделать еще один шаг вперед с помощью простого паттерна стратегии:

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

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

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

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

РЕДАКТИРОВАТЬ: Использование, которое вы хотите, из вашего редактирования, очень легко для температуры.Однако, если вам нужен универсальный UnitConverter, который может работать с любым UnitofMeasure, вы больше не хотите, чтобы Enums представляли ваши единицы измерения, потому что Enums не может иметь пользовательскую иерархию наследования (они наследуются непосредственно от System.Enum).

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

Вместо этого, если вам нужен один 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.

3 голосов
/ 21 октября 2011

Звучит так, будто вы хотите что-то вроде:

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 # Единицы измерения подход для вдохновения.

1 голос
/ 15 мая 2014

Вы можете взглянуть на Units.NET.Это на GitHub и NuGet .Он предоставляет наиболее распространенные единицы измерения и преобразования, поддерживает как статическую типизацию и перечисление единиц, так и разбор / печать сокращений.Тем не менее, он не анализирует выражения, и вы не можете расширить существующие классы модулей, но вы можете расширить его новыми сторонними модулями.

Примеры преобразований:

Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double feet = meter.Feet; // 3.28084
0 голосов
/ 24 октября 2013

Можно определить общий тип физических единиц таким образом, что, если для каждого модуля имеется тип, который реализует new и включает метод перевода между этим модулем и «базовым модулем» этого типа, можно выполнить арифметику для значения, которые выражены в разных единицах и которые необходимо преобразовать по мере необходимости, используя систему типов так, чтобы переменная типа AreaUnit<LengthUnit.Inches> принимала только вещи, измеренные в квадратных дюймах, но если один из них сказал myAreaInSquareInches= AreaUnit<LengthUnit.Inches>.Product(someLengthInCentimeters, someLengthInFathoms);, она автоматически переводила бы эти другие единицы перед выполнением умножения. На самом деле это может сработать довольно хорошо при использовании синтаксиса вызова метода, поскольку такие методы, как Product<T1,T2>(T1 p1, T2 p2) method, могут принимать параметры обобщенного типа в качестве своих операндов. К сожалению, нет никакого способа сделать операторы универсальными, и нет способа для типа, подобного AreaUnit<T> where T:LengthUnitDescriptor, определить средства преобразования в или из какого-либо другого произвольного универсального типа AreaUnit<U>. AreaUnit<T> может определять преобразования в и из, например. AreaUnit<Angstrom>, но компилятору нельзя было бы сказать, что код, которому присваивается AreaUnit<Centimeters> and wants AreaUnit`, может конвертировать дюймы в ангстремы, а затем в сантиметры.

0 голосов
/ 24 октября 2013

Обычно я хотел добавить это как комментарий к посту Дэнни Таппени, но, похоже, я не могу добавить это как комментарий.

Я немного улучшил решение от @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>/6328403/kak-sozdat-obschii-konverter-dlya-edinits-izmereniya-v-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));
    }
}
...