C# 中的 Immutable

2025年4月7日 | 阅读7分钟

在 C# 中,如果一个对象在创建后无法被修改,那么它就是不可变的。不可变性对于许多用例都很有价值,包括数据传输对象。

不可变的事物在其整个生命周期内都具有固定的状态,因为它们是不可更改的。这种可预测性通过使代码的行为更易于理解,从而简化了调试和维护。

创建不可变对象

为了在 C# 中创建不可变对象,请遵循最佳实践,例如:

  • 将字段声明为“readonly”可确保它们仅被分配一次,通常在对象创建时分配。
  • 避免在构造函数之后修改对象状态的可变属性或方法。
  • 提供创建具有更新值的对象修改副本的机制,而不是直接修改原始对象。

示例

让我们以一个例子来说明 C# 中Immutable 的使用。

示例

编译并运行

输出

 The temperature in Celsius is: 28
The temperature in Fahrenheit is: 82.4
The new temperature in Celsius is: 32
The new temperature in Fahrenheit is: 89.6
The new temperature in Celsius is: 35
The new temperature in Fahrenheit is: 95
The original temperature in Celsius is: 28
The original temperature in Fahrenheit is: 82.4

说明

在此示例中,Temperature 类以摄氏度作为基本测量单位,用于表示温度值。Temperature 实例是不可变的;它们是使用摄氏度值创建的。该类包括 InCelsius()InFahrenheit() 方法,用于创建具有摄氏度或华氏度温度的新实例。当在 Demo 类中创建实例时,它们的温度会以摄氏度和华氏度显示。之后,温度值的变化会创建新的实例,同时保留原始实例,从而证明了不可变性。在涉及温度处理的情况下,此模式可确保线程安全和可预测性。

1. 使用值类型

在 C# 中,值类型默认是不可变的。值类型包括结构体、枚举以及 int、float 和 DateTime 等基本类型。当我们为值类型变量分配新值时,会创建一个新实例。

示例

让我们以一个例子来说明 C# 中使用值类型Immutable 的用途。

示例

编译并运行

输出

 The Initial Value is: 256
The New value is: 743

说明

在此示例中,ImmutableStruct 是一个定义为结构体的值类型。它包含一个单一的只读字段 Value,该字段确保一旦在构造函数中为其赋值,就无法再次更改。它确保了不可变性,因为结构体是值类型,并且在传递实例时会对其进行复制。ImmutableStruct 的一个实例在 Main 方法中创建,初始值为 256。由于 Value 是只读字段,尝试在实例化后更改其 Value 会导致编译错误。

如果需要不同的值,则会使用所需的值创建新的 ImmutableStruct 实例,而不是修改当前实例。这证明了 C# 值类型的不可变性。

2. 不可变对象

这些对象的状态无法更改。通常,使用只读字段或仅具有 getter 和没有 setter 的属性来构造不可变对象。

示例

让我们以一个例子来说明 C# 中使用不可变对象Immutable 的用途。

示例

编译并运行

输出

The Value of an Immutable Object is: 437

说明

  • 一个名为 ImmutableObject 的类,它有一个名为 _Value 的单一不可变字段,代表一个不可变对象。
  • 为了确保创建对象后无法更改其 Value,_value 字段在构造函数中进行初始化,并被指定为 readonly。
  • 可以通过 Value 属性只读访问 _value 字段。
  • ImmutableObject 的一个实例在 Main 方法中创建,初始值为 437。
  • 通过尝试在创建后更改不可变字段的 Value 时会导致的编译错误,可以证明该对象的不可变性。

3. 函数式编程构造

函数式编程鼓励不可变性,并避免可变状态。在 C# 中可以使用函数式编程思想编写不可变的代码,例如高阶函数、lambda 表达式和纯函数。

示例

让我们以一个例子来说明 C# 中使用函数式编程构造Immutable 的用途。

示例

编译并运行

输出

The Doubled Numbers are:
20
46
86
68
30

说明

  • 可以将两个整数传递给我们的纯函数 add,它会返回它们的和。
  • DoubleFunc 是我们定义的用于将整数加倍的 lambda 表达式。
  • 定义了一个名为 Map 的高阶函数。它接受一个整数列表和一个函数,将该函数应用于列表中的每个成员,然后返回一个包含已修改元素的新列表。
  • 创建了一个整数列表,并使用 doubleFunc lambda 表达式和 Map 函数将每个数字加倍。
  • 最后,它打印加倍后的值。

Immutable 的优点

C# 中的Immutable具有以下几个优点:

  1. 线程安全:由于不可变对象的状态在创建后无法更改,因此它们是线程安全的。通过消除锁定机制的需要,它简化了并发编程,并减少了出错的可能性。
  2. 可预测性:由于不可变对象在其生命周期内保持相同的状态,因此它们提供了可预测的代码行为。这简化了代码推理、调试和维护。
  3. 并发性:不可变性通过消除数据竞争和不一致状态的可能性,促进了更安全的并发编程。在多线程应用程序中,不可变数据结构通过启用无锁共享访问来提高可伸缩性和速度。
  4. 可重用性:由于不可变对象无法更改,因此它们自然可以被共享和重用。它鼓励仅在输入上使用函数,从而产生可预测的输出,并最大程度地减少函数式编程中的副作用。
  5. 测试和调试:由于不可变代码消除了可变状态和副作用,因此通常可以更轻松地进行测试和调试。通过使测试用例更可靠和可重现,可以生成健壮可靠的软件。
  6. 函数式编程范例:函数式编程允许程序员通过结合不可变性与纯函数和引用透明性等概念来创建更具组织性、模块化和可重用的代码。

Immutable 的缺点

C# 中的Immutable具有以下几个缺点:

  1. 内存开销:由于每次修改都必须创建不可变数据结构的新实例,因此它们有时比可变数据结构需要更多的内存。这可能会影响内存消耗,尤其是在涉及大型数据集或频繁更新的情况下。
  2. 性能开销:不可变数据结构可能会导致性能开销,尤其是在向集合添加元素或进行频繁修改时,这些情况很常见。计算密集型操作包括复制数据和创建新实例。
  3. 复杂性:管理不可变数据结构可能很复杂,尤其是在与主要依赖可变性的现有代码库集成时,或者当不可变状态不可避免时。习惯了可变范式的开发人员可能需要改变他们的观点来适应不可变的编程方法。
  4. API 设计:为了确保一致性和可用性,在为不可变数据结构设计 API 时可能需要仔细考虑。对于不熟悉不可变概念的开发人员来说,不可变 API 和可变 API 之间的主要差异可能会导致学习曲线。

下一个主题null