【问题标题】:Change property of immutable type更改不可变类型的属性
【发布时间】:2016-12-11 20:34:26
【问题描述】:

我已将不可变类型存储在临时 CQRS 读取存储中(查询/读取端,实际上是由具有抽象访问层的简单列表实现的,此时我不想使用完整的文档数据库)。这些读取存储包含如下项目:

public class SomeItem
{
    private readonly string name;
    private readonly string description;

    public SomeItem(string name, string description)
    {
        this.name = name;
        this.description = description;
    }

    public string Name
    {
        get { return this.name; }
    }

    public string Description
    {
        get { return this.description; }
    }
}

现在我想更改名称并在第二个命令中更改描述。 这些更改应保持当前状态,这意味着对于上面的示例:

// initial state
var someItem = new SomeItem("name", "description");

// update name -> newName
someItem = new SomeItem("newName", someItem.Description);

// update description -> newDescription
someItem = new SomeItem(someItem.Name, "newDescription");

如果您有多个属性,这对我来说确实很容易出错……您必须设法保持当前状态。我可以为每种类型添加类似 Clone() 的东西,但我认为/希望那里有更好的东西,性能良好且易于使用,我不想编写太多重复代码(懒惰的程序员)。有什么建议可以改进上面的代码吗? SomeItem 类需要保持不可变(通过几个不同的线程传输)。

【问题讨论】:

    标签: c# clone immutability icloneable


    【解决方案1】:

    遗憾的是,C# 中没有简单的方法。 F# 有 with 关键字,你可以看看镜头,​​但在 C# 中这一切都有些乏味。我能给你的最好的东西是这样的:

    class SomeItem
    {
      private readonly string name;
      private readonly string description;
    
      public SomeItem(string name, string description)
      {
        this.name = name;
        this.description = description;
      }
    
      public SomeItem With
        (
          Option<string> name = null,
          Option<string> description = null
        )
      {
        return new SomeItem
          (
            name.GetValueOrDefault(this.name), 
            description.GetValueOrDefault(this.description)
          );
      }
    }
    

    这使您可以像这样进行更新

    var newItem = oldItem.With(name: "My name!");
    

    我已经将这种方法与扩展方法和 T4 一起使用,效果很好,但是即使您手动编写代码,它也相当可靠 - 如果您添加一个新字段,您也必须将其添加到 With,所以效果很好。

    如果您愿意容忍运行时代码生成并降低类型安全性,还有其他一些方法,但这有点违背 IMO 的原则。

    【讨论】:

      【解决方案2】:

      在 C#9 中,我们为此使用了 with 运算符。

         public record Car
          {
              public string Brand { get; init; }   
              public string Color { get; init; }    
          }
          var car = new Car{ Brand = "BMW", Color = "Red" }; 
          var anotherCar = car with { Brand = "Tesla"};
      

      With-expressions 处理不可变数据时,常见的模式是 从现有的值中创造新的值来代表一个新的状态。为了 例如,如果我们的人要更改他们的姓氏,我们会 将其表示为一个新对象,它是旧对象的副本,除了 一个不同的姓氏。这种技术通常被称为 非破坏性突变。而不是代表这个人 时间,记录代表一个人在给定时间的状态。到 帮助这种编程风格,记录允许一种新的 表达; with 表达式:

      News in C#9

      注意 With 运算符仅受记录支持。

      记录 经典面向对象编程的核心是对象具有强标识并封装可变状态的思想 随着时间的推移而演变。 C# 在这方面一直很有效,但是 有时你想要的恰恰相反,这里是 C# 默认值往往会妨碍您,让事情变得非常费力。

      Recods in C#9

      【讨论】:

        【解决方案3】:

        您要查找的内容通常称为 with 运算符

        // returns a new immutable object with just the single property changed
        someItem = { someItem with Name = "newName" };
        

        不幸的是,与 F# 不同,C# 没有这样的运算符(还没有?)。

        其他 C# 开发人员也缺少此功能,这就是为什么 someone wrote a Fody extension to do exactly that:

        这是另一种方法,它手动实现UpdateWith 方法,但需要Option&lt;T&gt; 辅助类。 Luaan's answer 更详细地描述了这种方法:

        【讨论】:

        【解决方案4】:

        如果您想做的是(正如您评论的那样)更新现有对象的名称,则只读属性可能是糟糕的设计。 否则,如果它真的是您想要创建的新对象,您可能希望您的类使用“Dispose”方法实现一些接口。

        【讨论】:

        • 这不是他想要的。他想使用不可变数据类型并以相当简单的方式创建数据的更新版本。这在不可变/函数式编程世界中非常典型。关键是他从不想更改原始对象(就像您不想更改 string 一样)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-10-29
        • 2011-02-02
        • 1970-01-01
        • 2021-09-05
        • 1970-01-01
        • 2010-11-29
        • 2018-04-24
        相关资源
        最近更新 更多