C#不可变&;无重复的可变类型

本文关键字:类型 不可变 amp | 更新日期: 2023-09-27 18:23:38

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

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

C#不可变&;无重复的可变类型

我最近做了一些能满足你要求的东西(使用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,stringlastName,int age=18),它将初始化这些值
  • Equals(其他对象)和Equals的工作实现(其他人)。此外,它还将为您添加IEquatable接口。工作运算符==和运算符!=的实现
  • GetHashCode()的工作实现一个更好的ToString(),输出如"Person{FirstName=John,LastName=Doe,Age=21}"
  • Person With(…)方法,可用于生成更改了0个或多个属性的新的不可变克隆(例如var janeDoe=johnDoe.Wwith(firstName:"Jane",年龄: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; }

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

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

C#中对不可变类型的支持有点缺乏,可以使用一些特定于语言的特性来简化它。正如你所说的,必须创建一个建设者是一种真正的痛苦。另一种选择是有一个接受所有值的构造函数,但最终往往是所有构造函数之母,这会使代码无法读取。

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

考虑使用代码生成将每个可变映射到其不可变等价物。我个人喜欢在T4Toolbox库的帮助下生成T4代码。使用EnvDTE可以很容易地解析代码。

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

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

根据您是否创建面向公众的API,您需要考虑的一个问题是考虑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
}