Подтвердить что ты не робот

Шаблон для создания простого и эффективного типа значений

Мотивация:

При чтении блога Mark Seemanns на Код запах: автоматическое свойство, он говорит в конце:

Суть в том, что автоматические свойства редко подходят. Фактически, они подходят только тогда, когда тип свойства является тип значения и все мыслимые значения разрешены.

Он дает int Temperature как пример плохого запаха и предлагает лучшее исправление - это тип удельного значения единицы, например Celsius. Поэтому я решил попробовать создать настраиваемый тип значения Celsius, который инкапсулирует все проверки границ и логику преобразования типов как упражнение в более SOLID.

Основные требования:

  • Невозможно иметь недопустимое значение
  • Инкапсулирует операции преобразования
  • Эффективное копирование (эквивалентное его замене)
  • Как можно интуитивно использовать для использования (попытка семантики int)

Реализация:

[System.Diagnostics.DebuggerDisplay("{m_value}")]
public struct Celsius // : IComparable, IFormattable, etc...
{
    private int m_value;

    public static readonly Celsius MinValue = new Celsius() { m_value = -273 };           // absolute zero
    public static readonly Celsius MaxValue = new Celsius() { m_value = int.MaxValue };

    private Celsius(int temp)
    {
        if (temp < Celsius.MinValue)
            throw new ArgumentOutOfRangeException("temp", "Value cannot be less then Celsius.MinValue (absolute zero)");
        if (temp > Celsius.MaxValue)
            throw new ArgumentOutOfRangeException("temp", "Value cannot be more then Celsius.MaxValue");

        m_value = temp;
    }

    public static implicit operator Celsius(int temp)
    {
        return new Celsius(temp);
    }

    public static implicit operator int(Celsius c)
    {
        return c.m_value;
    }

    // operators for other numeric types...

    public override string ToString()
    {
        return m_value.ToString();
    }

    // override Equals, HashCode, etc...
}

Тесты:

[TestClass]
public class TestCelsius
{
    [TestMethod]
    public void QuickTest()
    {
        Celsius c = 41;             
        Celsius c2 = c;
        int temp = c2;              
        Assert.AreEqual(41, temp);
        Assert.AreEqual("41", c.ToString());
    }

    [TestMethod]
    public void OutOfRangeTest()
    {
        try
        {
            Celsius c = -300;
            Assert.Fail("Should not be able to assign -300");
        }
        catch (ArgumentOutOfRangeException)
        {
            // pass
        }
        catch (Exception)
        {
            Assert.Fail("Threw wrong exception");
        }
    }
}

Вопросы:

  • Есть ли способ сделать MinValue/MaxValue const вместо readonly? Глядя на BCL, мне нравится, как определение метаданных int четко указывает MaxValue и MinValue как константы времени компиляции. Как я могу имитировать это? Я не вижу способ создать объект Celsius, не вызывая конструктор или не раскрывая детали реализации, которые Celsius хранит int.
  • Не хватает ли каких-либо функций юзабилити?
  • Есть ли лучший шаблон для создания настраиваемого типа значения одного поля?
4b9b3361

Ответ 1

Есть ли способ сделать MinValue/MaxValue const вместо readonly?

Нет. Однако BCL тоже этого не делает. Например, DateTime.MinValue - static readonly. Ваш текущий подход для MinValue и MaxValue подходит.

Что касается двух других вопросов - удобство использования и сам шаблон.

Лично я бы избегал автоматических преобразований (неявных операторов преобразования) для типа "температуры", подобного этому. Температура не является целочисленным значением (на самом деле, если вы собираетесь это сделать, я бы сказал, что она должна быть плавающей точкой - 93,2 градуса по шкале C. Совершенно верно.) Обработка температуры как целого и особенно обработка любого целочисленного значения неявно, поскольку температура кажется неуместной и потенциальной причиной ошибок.

Я нахожу, что структуры с неявным преобразованием часто вызывают больше проблем использования, чем они адресуют. Принуждение пользователя к записи:

 Celsius c = new Celcius(41);

Не намного сложнее, чем неявное преобразование из целого числа. Тем не менее, это намного яснее.

Ответ 2

Я думаю, что с точки зрения удобства использования я бы выбрал тип Temperature, а не Celsius. Celsius - это всего лишь единица измерения, тогда как a Temperature будет представлять собой фактическое измерение. Тогда ваш тип может поддерживать несколько единиц, таких как Цельсия, Фаренгейта и Кельвина. Я бы также выбрал десятичное значение в качестве резервного хранилища.

Что-то в этом роде:

public struct Temperature
{
    private decimal m_value;

    private const decimal CelsiusToKelvinOffset = 273.15m;

    public static readonly Temperature MinValue = Temperature.FromKelvin(0);
    public static readonly Temperature MaxValue = Temperature.FromKelvin(Decimal.MaxValue);

    public decimal Celsius
    {
        get { return m_value - CelsiusToKelvinOffset; }
    }

    public decimal Kelvin 
    {
        get { return m_value; }
    }

    private Temperature(decimal temp)
    {
        if (temp < Temperature.MinValue.Kelvin)
               throw new ArgumentOutOfRangeException("temp", "Value {0} is less than Temperature.MinValue ({1})", temp, Temperature.MinValue);
        if (temp > Temperature.MaxValue.Kelvin)
               throw new ArgumentOutOfRangeException("temp", "Value {0} is greater than Temperature.MaxValue ({1})", temp, Temperature.MaxValue);
         m_value = temp;
    }

    public static Temperature FromKelvin(decimal temp)
    {     
           return new Temperature(temp);
    }

    public static Temperature FromCelsius(decimal temp)
    {
        return new Temperature(temp + CelsiusToKelvinOffset);
    }

    ....
}

Я бы избегал неявного преобразования, поскольку Рид утверждает, что он делает вещи менее очевидными. Однако я бы перегрузил операторы (<, > , ==, +, -, *,/), поскольку в этом случае было бы целесообразно выполнять такие операции. И кто знает, в какой-то будущей версии .net мы могли бы даже указывать ограничения оператора и, наконец, могли бы писать более многократно используемые структуры данных (представьте себе класс статистики, который может вычислять статистику для любого типа, который поддерживает +, -, *,/).

Ответ 3

DebuggerDisplay - полезное прикосновение. Я бы добавил единицы измерений "{m_value} C", чтобы вы могли сразу увидеть тип.

В зависимости от использования цели вы также можете иметь общую структуру преобразования в/из базовых единиц в дополнение к конкретным классам. То есть хранить значения в единицах СИ, но иметь возможность отображать/редактировать на основе культуры (градусы C, км, кг) против (градусов F, mi, lb).

Вы также можете проверить единицы измерения F # для дополнительных идей (http://msdn.microsoft.com/en-us/library/dd233243.aspx) - обратите внимание, что это компиляция времени.

Ответ 4

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

Только одно: поскольку Celsius неявно конвертируется в/из int, вы можете определить такие границы:

public const int MinValue = -273;
public const int MaxValue = int.MaxValue;

Однако на практике нет практических различий между static readonly и const.