2

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

我希望默认使用不可变类型,除非需要可变类型(例如绑定到 UI 元素时)。

我们正在使用 .NET 框架 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() 的工作实现 更好的 ToString() 输出,例如“Person { FirstName=John, LastName=Doe, Age=21 }”
  • 一种 Person With(...) 方法,可用于生成具有 0 个或多个属性更改的新不可变克隆(例如 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

由于属性没有相同的可见性,这不是重复的代码。如果它们的可见性相同,则 Person 可以从 Mutable 继承以避免重复。现在,我认为没有代码可以将您显示的内容分解。

于 2013-07-18T07:47:23.603 回答
0

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

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

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

于 2013-07-18T09:27:26.227 回答
0

根据您是否正在创建面向公众的 API,您需要考虑的一种方法是考虑Eric Lippert所讨论的“popcicle 不变性” 。这样做的好处是您根本不需要任何重复。

我使用了相反的东西,我的类是可变的,直到某个点发生某些计算,此时我调用 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 回答