Шаблон для создания простого и эффективного типа значения - PullRequest
24 голосов
/ 07 ноября 2011

Мотивация:

При чтении блога Марка Симанна на Запах кода: автоматическое свойство он говорит в конце:

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

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

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

  1. Невозможно иметь недопустимое значение
  2. Инкапсулирует операции преобразования
  3. Эффективное копирование (эквивалентно замене int)
  4. Как интуитивно понятно использоватьнасколько возможно (пробуя семантику 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 хранит целое число.
  • Я пропустил какие-либо функции удобства использования?
  • Есть ли лучший шаблон для создания пользовательского типа значения отдельного поля?

Ответы [ 4 ]

20 голосов
/ 07 ноября 2011

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

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

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

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

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

 Celsius c = new Celcius(41);

Не намного сложнее, чем неявное преобразование из целого числа.Однако гораздо яснее.

9 голосов
/ 08 ноября 2011

Я думаю, что с точки зрения удобства использования я бы выбрал тип Temperature, а не Celsius. Celsius - это просто единица измерения, а 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 мы можем даже указать операторные ограничения и, наконец, написать более многократно используемые структуры данных (представьте класс статистики, который может вычислять статистику для любого типа, который поддерживает +, -, *, /).

2 голосов
/ 08 ноября 2011

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

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

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

0 голосов
/ 07 ноября 2011

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...