如何创建一个不可变的类?

126

我正在创建一个不可变类。
我已将所有属性标记为只读。

我在该类中有一个项目列表。
尽管属性是只读的,但列表仍然可以被修改。

公开列表的 IEnumerable 使其成为不可变的。
我想知道创建不可变类需要遵循哪些基本规则?


3
我强烈建议您阅读Eric Lippert关于不变性的博客系列,特别是关于“不变性种类”的文章。您的评论:“公开列表的IEnumerable使其成为不可变的”对我来说似乎有些奇怪。您的意思是什么? - Jon Skeet
我的意思是,与其允许访问列表(如果对象具有其他某些对象的列表),不如允许用户使用IEnumerable访问成员。这里讨论的是列表,但它可以是任何数据结构。 - Biswanath
3
当MSDN归档这些博客时,Jon Skeet指向Eric Lippert博客系列的链接已经失效。请参见“kinds of immutability”。该系列的其余部分似乎在左侧面板的2007年12月和2008年1月之下。 - John Doe
请查看Eric Lippert关于人们常常混淆原子性易变性不可变性这些术语的博客系列:第一部分第二部分第三部分。这些来自他个人博客,我相信比他在MSDN上的文章更适合新手阅读。 - John Doe
8个回答

131

我认为你走在了正确的道路上 -

  • 所有注入到类中的信息应该在构造函数中提供。
  • 所有属性都应该是只读的。
  • 如果一个集合(或数组)被传递到构造函数中,它应该被复制以防止调用方之后修改它。
  • 如果你要返回你的集合,则应该返回一个副本或只读版本(例如,使用ArrayList.ReadOnly或类似方法 - 你可以将这一点与前面的内容相结合,并 存储 一个只读副本以在调用者访问它时返回),或者使用其他允许对集合进行只读访问的方法/属性,例如返回枚举器等。
  • 请记住,如果你的任何成员是可变的,你仍然可能会有一个可变的类的出现 - 如果是这样,你应该复制掉你想要保留的任何状态,并避免返回整个可变对象,除非你在把它们还给调用者之前复制它们 - 另一个选择是仅返回可变对象的不可变 "部分" - 感谢@Brian Rasmussen鼓励我扩展这一点。

我应该编写一个包装器查找属性,让你只能进行查找吗?谢谢你的回答。 - Biswanath
6
构造函数的参数中,任何可变引用类型都应该被复制。否则调用者将继续持有对状态的引用。 - Brian Rasmussen
@Brian Rasmussen,你是正确的,但即使它是复制的,任何调用者都可能访问可变对象,这取决于提供的访问器。在传递可变对象的情况下,最好的做法是始终返回不同的副本或对象的不可变部分。 - Blair Conrad
@Blair - 如果我在类中有一个字典,我只想用它来查找。一个只读属性进行查找是否可以? - Biswanath
只是提醒一下 - 仅仅返回一个IEnumerable<>并不一定使类成为不可变的,因为IEnumerable<>仍然可以被改变(由其他代码或甚至是您不可变类的用户)。请参见https://dev59.com/FmUo5IYBdhLWcg3wzCBq#15858794。 - Steve
显示剩余3条评论

21

为实现不可变性,您的所有属性和字段都应该是只读的。而任何列表中的项目本身也应该是不可变的。

您可以按如下方式创建一个只读的列表属性:

public class MyClass
{
    public MyClass(..., IList<MyType> items)
    {
        ...
        _myReadOnlyList = new List<MyType>(items).AsReadOnly();
    }

    public IList<MyType> MyReadOnlyList
    {
        get { return _myReadOnlyList; }
    }
    private IList<MyType> _myReadOnlyList

}

1
我认为使用ImmutableList会更好。 - Kevin Wong
使用ImmutableList,同时考虑将类封装。 - kofifus
我并不认为ImmutableList适合这种用例:“制作一个包含列表的类(使其不可变)的基本规则”。ImmutableList的语义在向列表副本添加项目时可以更有效地使用内存,但在我看来,那似乎是一个更具体的用例。 - Joe

12

另外需要注意的是:

public readonly object[] MyObjects;

即使使用readonly关键字标记,数组仍然不是不可变的。您仍然可以通过索引访问器更改单个数组引用/值。


7

你所需要的只是使用 record 和新版本的 C# 9.0 或更新版本。

public record Customer(string FirstName, string LastName, IEnumerable<string> Items);

//...

var person = new Customer("Test", "test", new List<string>() { "Test1", "Test2", "Test3" });
// you can't change anything within person variable
// person.FirstName = "NewName";

这将被翻译成一个不可变的类,称为Customer,它有三个属性:FirstName, LastNameItems

如果你需要一个类的属性是不可变的(只读)集合,最好将其公开为IEnumerable<T>ReadOnlyCollection<T>,而不是来自System.Collections.Immutable的东西。


5
使用ReadOnlyCollection类。它位于System.Collections.ObjectModel命名空间中。
在返回列表的任何地方(或构造函数中),将列表设置为只读集合。
using System.Collections.ObjectModel;

...

public MyClass(..., List<ListItemType> theList, ...)
{
    ...
    this.myListItemCollection= theList.AsReadOnly();
    ...
}

public ReadOnlyCollection<ListItemType> ListItems
{
     get { return this.myListItemCollection; }
}

2

2
另一种选择是使用访问者模式,而不是暴露任何内部集合。

1
原始问题和答案来自2008年底,现在有一个我认为最早出现在.NET Core(1.0)的System.Collections.Immutable命名空间。该命名空间仍不可用于.NET Standard(当前版本2.1)和.NET Framework(当前版本4.8)。该命名空间具有许多不可变集合,包括原始问题中提到的ImmutableList。但是,我认为System.Collections.Immutable命名空间可能会出现在目前处于发布候选2阶段的.NET 5中。
此外,从C#6开始,您可以使用{ get; }来拥有不可变的自动实现属性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接