C#Immutable&可变类型没有重复

时间:2013-07-18 07:29:09

标签: c# immutability

鉴于以下可变和不可变类型的实现,是否有办法避免重复代码(主要是重复属性)

我希望默认使用不可变类型,除非需要可变类型(例如绑定到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);
    }
}

5 个答案:

答案 0 :(得分:2)

不,没有简单的方法可以避免重复的代码。

您实施的是有效的builder模式。 .NET StringBuilder类遵循相同的方法。

对C#中不可变类型的支持有点缺乏,并且可以使用某些特定于语言的功能来简化它。你必须创建一个建设者是一个真正的痛苦,因为你已经发现了。另一种方法是使用一个构造函数来获取所有值,但是你最终会得到所有构造函数的母版,这会使代码变得不可读。

答案 1 :(得分:1)

我做了一些你最近提出的问题(使用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()的工作实现更好的ToString(),其输出如&#34; Person {FirstName = John,LastName = Doe,Age = 21}&#34;
  • 具有(...)方法的人,可用于生成具有0或更多属性的新的不可变克隆(例如var janeDoe = johnDoe.With(firstName:&#34; Jane&#34;,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; }

答案 2 :(得分:0)

由于属性不具有相同的可见性,因此这不是重复的代码。如果他们的可见性相同,那么Person可以继承Mutable以避免重复。现在,我认为没有代码可以分解你所展示的内容。

答案 3 :(得分:0)

考虑使用代码生成将每个mutable映射到其不可变的等效项。 我个人喜欢T4代码生成,由T4Toolbox库辅助。 您可以使用EnvDTE轻松解析代码。

您可以在Oleg Sych博客上找到大量关于T4的高质量信息 http://www.olegsych.com/

代码生成在开始时可能很难处理,但它解决了代码的臭名昭着的问题 - 必须重复。

答案 4 :(得分:0)

根据您是否正在创建面向公众的API,需要考虑的一个问题是考虑Eric Lippert所讨论的“popcicle immutability”。关于这一点的好处是你根本不需要任何重复。

我反过来使用了一些东西,我的类是可变的,直到某个计算将要发生的某一点,我称之为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
}