26

動機:

Code Smell: Automatic Propertyに関する Mark Seemann のブログを読んで、彼は最後の方で次のように述べています。

肝心なのは、自動プロパティが適切であることはめったにないということです。実際、プロパティの型が値型であり、考えられるすべての値が許可されている場合にのみ適切です。

彼は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 にする方法はありますか? BCL を見ると、 intのメタデータ定義でMaxValue と MinValue がコンパイル時の定数として明確に示されているのが気に入っています。どうすればそれを模倣できますか?コンストラクターを呼び出すか、摂氏が int に格納する実装の詳細を公開せずに摂氏オブジェクトを作成する方法がわかりません。
  • ユーザビリティ機能が不足していませんか?
  • カスタム単一フィールド値タイプを作成するためのより良いパターンはありますか?
4

4 に答える 4

22

読み取り専用の代わりに MinValue/MaxValue を const にする方法はありますか?

いいえ。ただし、BCL もこれを行いません。たとえば、DateTime.MinValuestatic readonlyです。あなたの現在のアプローチは、MinValue適切MaxValueです。

他の 2 つの質問については、使いやすさとパターン自体です。

個人的には、このような「温度」タイプの自動変換 (暗黙の変換演算子) は避けたいと思います。温度は整数値ではありません (実際、これを行う場合、浮動小数点である必要があると主張します。93.2 ℃ は完全に有効です)。温度を整数として扱い、特に整数値を扱います。温度が不適切であり、バグの潜在的な原因と思われるため、暗黙のうちに。

暗黙的な変換を伴う構造体は、対処するよりも多くのユーザビリティの問題を引き起こすことがよくあります。ユーザーに強制的に書き込む:

 Celsius c = new Celcius(41);

整数から暗黙的に変換するよりもそれほど難しくありません。ただし、はるかに明確です。

于 2011-11-07T18:53:45.113 に答える
9

Temperature使いやすさの観点から、ではなくタイプを選ぶと思いますCelsiusCelsiusは単なる測定単位ですが、aTemperatureは実際の測定値を表します。次に、タイプが摂氏、華氏、ケルビンなどの複数の単位をサポートできます。また、バッキング ストレージとして 10 進数を選択します。

これらの行に沿ったもの:

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

    ....
}

暗黙の変換は避けたいと思います。Reed が述べているように、暗黙の変換によって物事がわかりにくくなるからです。ただし、演​​算子 (<、>、==、+、-、*、/) をオーバーロードします。この場合、これらの種類の操作を実行するのが理にかなっているからです。そして、.net の将来のバージョンでは、演算子の制約を指定して、最終的に再利用可能なデータ構造を記述できるようになる可能性があります (+、-、​​、 /)。

于 2011-11-07T20:33:38.687 に答える
2

DebuggerDisplay便利なタッチです。測定単位「{m_value}C」を追加して、タイプをすぐに確認できるようにします。

ターゲットの使用法によっては、具象クラスに加えて、基本単位との間の一般的な変換フレームワークが必要になる場合もあります。つまり、値をSI単位で保存しますが、(摂氏C、km、kg)と(F、mi、lb)のような文化に基づいて表示/編集することができます。

また、F#測定単位で追加のアイデアを確認することもできます(http://msdn.microsoft.com/en-us/library/dd233243.aspx)。これはコンパイル時の構成であることに注意してください。

于 2011-11-08T02:56:16.097 に答える