2

次の可変型と不変型の実装を考えると、コードの重複 (主にプロパティの重複)を回避する方法はありますか?

可変型が必要でない限り (UI 要素にバインドする場合など)、デフォルトで不変型を使用したいと思います。

.NET Framework 4.0 を使用していますが、まもなく 4.5 に切り替える予定です。

public class Person {
    public string Name { get; private set; }
    public List<string> Jobs { get; private set; } // Change to ReadOnlyList<T>
    public Person() {}
    public Person(Mutable m) {
        Name = m.Name;
    }
    public class Mutable : INotifyPropertyChanged {
        public string Name { get; set; }
        public List<string> Jobs { get; set; }
        public Mutable() {
            Jobs = new List<string>();
        }
        public Mutable(Person p) {
            Name = p.Name;
            Jobs = new List<string>(p.Jobs);
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName) {
            // TODO: implement
        }
    }
}

public class Consumer {
    public Consumer() {
        // We can use object initializers :)
        Person.Mutable m = new Person.Mutable {
            Name = "M. Utable"
        };
        // Consumers can happily mutate away....
        m.Name = "M. Utated";
        m.Jobs.Add("Herper");
        m.Jobs.Add("Derper");

        // But the core of our app only deals with "realio-trulio" immutable types.

        // Yey! Have constructor with arity of one as opposed to
        // new Person(firstName, lastName, email, address, im, phone)
        Person im = new Person(m);
    }
}
4

5 に答える 5

3

最近あなたが求めていることを行うものを作成しました(T4テンプレートを使用)ので、絶対に可能です:

https://github.com/xaviergonz/T4Immutable

たとえば、次のようになります。

[ImmutableClass(Options = ImmutableClassOptions.IncludeOperatorEquals)]
class Person {
  private const int AgeDefaultValue = 18;

  public string FirstName { get; }
  public string LastName { get; }
  public int Age { get; }

  [ComputedProperty]
  public string FullName {
    get {
      return FirstName + " " + LastName;
    }
  }
}

別の部分クラス ファイルに次のものが自動的に生成されます。

  • 値を初期化する public Person(string firstName, string lastName, int age = 18) などのコンストラクター。
  • Equals(object other) と Equals(Person other) の実用的な実装。また、IEquatable インターフェイスが追加されます。operator== および operator!= の実用的な実装
  • GetHashCode() の実用的な実装 "Person { FirstName=John, LastName=Doe, Age=21 }" などの出力を持つより優れた ToString()
  • 0 個以上のプロパティが変更された新しい不変クローンを生成するために使用できる Person With(...) メソッド (例: var janeDoe = johnDoe.With(firstName: "Jane", age: 20)

したがって、これを生成します(一部の冗長な属性を除く):

using System;

partial class Person : IEquatable<Person> {
  public Person(string firstName, string lastName, int age = 18) {
    this.FirstName = firstName;
    this.LastName = lastName;
    this.Age = age;
    _ImmutableHashCode = new { this.FirstName, this.LastName, this.Age }.GetHashCode();
  }

  private bool ImmutableEquals(Person obj) {
    if (ReferenceEquals(this, obj)) return true;
    if (ReferenceEquals(obj, null)) return false;
    return T4Immutable.Helpers.AreEqual(this.FirstName, obj.FirstName) && T4Immutable.Helpers.AreEqual(this.LastName, obj.LastName) && T4Immutable.Helpers.AreEqual(this.Age, obj.Age);
  }

  public override bool Equals(object obj) {
    return ImmutableEquals(obj as Person);
  }

  public bool Equals(Person obj) {
    return ImmutableEquals(obj);
  }

  public static bool operator ==(Person a, Person b) {
    return T4Immutable.Helpers.AreEqual(a, b);
  }

  public static bool operator !=(Person a, Person b) {
    return !T4Immutable.Helpers.AreEqual(a, b);
  }

  private readonly int _ImmutableHashCode;

  private int ImmutableGetHashCode() {
    return _ImmutableHashCode;
  }

  public override int GetHashCode() {
    return ImmutableGetHashCode();
  }

  private string ImmutableToString() {
    var sb = new System.Text.StringBuilder();
    sb.Append(nameof(Person) + " { ");

    var values = new string[] {
      nameof(this.FirstName) + "=" + T4Immutable.Helpers.ToString(this.FirstName),
      nameof(this.LastName) + "=" + T4Immutable.Helpers.ToString(this.LastName),
      nameof(this.Age) + "=" + T4Immutable.Helpers.ToString(this.Age),
    };

    sb.Append(string.Join(", ", values) + " }");
    return sb.ToString();
  }

  public override string ToString() {
    return ImmutableToString();
  }

  private Person ImmutableWith(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
    return new Person(
      !firstName.HasValue ? this.FirstName : firstName.Value,
      !lastName.HasValue ? this.LastName : lastName.Value,
      !age.HasValue ? this.Age : age.Value
    );
  }

  public Person With(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
    return ImmutableWith(firstName, lastName, age);
  }

}

プロジェクトページで説明されているように、さらにいくつかの機能があります。

PS: 他の不変オブジェクトのリストであるプロパティが必要な場合は、次を追加するだけです:

public ImmutableList<string> Jobs { get; }
于 2016-11-06T00:50:38.130 に答える
2

いいえ、コードの重複を避ける簡単な方法はありません。

あなたが実装したのは、効果的にbuilderパターンです。.NETStringBuilderクラスも同じアプローチに従います。

C# での不変型のサポートは少し不足しており、いくつかの言語固有の機能を使用して簡単にすることができます。あなたが発見したように、ビルダーを作成しなければならないのは本当に苦痛です。別の方法として、すべての値を受け取るコンストラクターを使用することもできますが、最終的にすべてのコンストラクターの母になってしまう傾向があり、コードが判読できなくなります。

于 2013-07-18T08:04:24.437 に答える
0

一般向けの API を作成するかどうかに応じて考慮すべきことの 1 つは、Eric Lippertで説明されているように、「ポプシクルの不変性」を考慮することです。これの良いところは、重複がまったく必要ないことです。

私は逆に何かを使用しました。私のクラスは、Freeze() メソッドを呼び出すいくつかの計算が行われる特定の時点まで変更可能です。プロパティへのすべての変更は、BeforeValueChanged() メソッドを呼び出します。このメソッドは、凍結された場合に例外をスローします。

必要なのは、クラスがデフォルトで凍結されており、変更可能にする必要がある場合は凍結を解除することです。他の人が言ったように、凍結されている場合は、リストなどの読み取り専用コピーを返す必要があります.

以下は、私がまとめた小さなクラスの例です。

/// <summary>
/// Defines an object that has a modifiable (thawed) state and a read-only (frozen) state
/// </summary>
/// <remarks>
/// All derived classes should call <see cref="BeforeValueChanged"/> before modifying any state of the object. This
/// ensures that a frozen object is not modified unexpectedly.
/// </remarks>
/// <example>
/// This sample show how a derived class should always use the BeforeValueChanged method <see cref="BeforeValueChanged"/> method.
/// <code>
/// public class TestClass : Freezable
/// {
///    public String Name
///    {
///       get { return this.name; }
///       set
///       {
///          BeforeValueChanged();
///          this.name = name;
///       }
///    }
///    private string name;
/// }
/// </code>
/// </example>
[Serializable]
public class Freezable
{
    #region Locals

    /// <summary>Is the current instance frozen?</summary>
    [NonSerialized]
    private Boolean _isFrozen;

    /// <summary>Can the current instance be thawed?</summary>
    [NonSerialized]
    private Boolean _canThaw = true;

    /// <summary>Can the current instance be frozen?</summary>
    [NonSerialized]
    private Boolean _canFreeze = true;

    #endregion

    #region Properties

    /// <summary>
    /// Gets a value that indicates whether the object is currently modifiable.
    /// </summary>
    /// <value>
    ///   <c>true</c> if this instance is frozen; otherwise, <c>false</c>.
    /// </value>
    public Boolean IsFrozen 
    {
        get { return this._isFrozen; }
        private set { this._isFrozen = value; } 
    }

    /// <summary>
    /// Gets a value indicating whether this instance can be frozen.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this instance can be frozen; otherwise, <c>false</c>.
    /// </value>
    public Boolean CanFreeze
    {
        get { return this._canFreeze; }
        private set { this._canFreeze = value; }
    }

    /// <summary>
    /// Gets a value indicating whether this instance can be thawed.
    /// </summary>
    /// <value>
    ///   <c>true</c> if this instance can be thawed; otherwise, <c>false</c>.
    /// </value>
    public Boolean CanThaw
    {
        get { return this._canThaw; }
        private set { this._canThaw = value; }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Freeze the current instance.
    /// </summary>
    /// <exception cref="System.InvalidOperationException">Thrown if the instance can not be frozen for any reason.</exception>
    public void Freeze()
    {
        if (this.CanFreeze == false)
            throw new InvalidOperationException("The instance can not be frozen at this time.");

        this.IsFrozen = true;
    }

    /// <summary>
    /// Does a Deep Freeze for the duration of an operation, preventing it being thawed while the operation is running.
    /// </summary>
    /// <param name="operation">The operation to run</param>
    internal void DeepFreeze(Action operation)
    {
        try
        {
            this.DeepFreeze();
            operation();
        }
        finally
        {
            this.DeepThaw();
        }
    }

    /// <summary>
    /// Applies a Deep Freeze of the current instance, preventing it be thawed, unless done deeply.
    /// </summary>
    internal void DeepFreeze()
    {
        // Prevent Light Thawing
        this.CanThaw = false;
        this.Freeze();
    }

    /// <summary>
    /// Applies a Deep Thaw of the current instance, reverting a Deep Freeze.
    /// </summary>
    internal void DeepThaw()
    {
        // Enable Light Thawing
        this.CanThaw = true;
        this.Thaw();
    }

    /// <summary>
    /// Thaws the current instance.
    /// </summary>
    /// <exception cref="System.InvalidOperationException">Thrown if the instance can not be thawed for any reason.</exception>
    public void Thaw()
    {
        if (this.CanThaw == false)
            throw new InvalidOperationException("The instance can not be thawed at this time.");

        this.IsFrozen = false;
    }

    /// <summary>
    /// Ensures that the instance is not frozen, throwing an exception if modification is currently disallowed.
    /// </summary>
    /// <exception cref="System.InvalidOperationException">Thrown if the instance is currently frozen and can not be modified.</exception>
    protected void BeforeValueChanged()
    {
        if (this.IsFrozen)
            throw new InvalidOperationException("Unable to modify a frozen object");
    }

    #endregion
}
于 2013-07-18T09:57:27.490 に答える
0

プロパティは同じ可視性を持たないため、これは重複コードではありません。それらの可視性が同じである場合、重複を避けるために Person は Mutable から継承できます。現在、表示されているものを因数分解するコードはないと思います。

于 2013-07-18T07:47:23.603 に答える