C#中的不可变性:提升代码可读性与安全性

在企业级软件开发中,代码的复杂性是一个主要问题。代码的可读性应该是在构建软件项目时首先尝试实现的目标。没有它,将无法对软件的正确性做出合格的判断,或者至少对其的推理能力将显著降低。

为什么选择不可变性?

可变对象是增加还是减少了代码的可读性?让来看一个例子:

var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); IReadOnlyCollection<Customer> customers = Search(queryObject); if (customers.Count == 0) AdjustSearchCriteria(queryObject, name); Search(queryObject);

这里,不能确定queryObject是否在第二次搜索客户时被更改了。这取决于第一次是否找到了任何东西,以及AdjustSearchCriteria方法是否更改了标准。要找出确切发生了什么,需要查看AdjustSearchCriteria方法的代码。不能仅通过查看方法签名就确定。

现在,让比较以下代码:

var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); IReadOnlyCollection<Customer> customers = Search(queryObject); if (customers.Count == 0) { QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name); Search(newQueryObject); }

现在很明显,AdjustSearchCriteria方法创建了新的标准,用于执行新的搜索。

可变数据结构的问题

如果不确定数据是否被更改,那么很难对代码进行推理。如果需要查看它调用的方法而不仅仅是方法本身,那么跟踪流程将变得困难。如果正在构建一个多线程应用程序,跟踪和调试代码将变得更加困难。

如何构建不可变类型

如果有一个相对简单的类,应该总是考虑使其不可变。这个经验法则与Value Objects的概念相关:值对象是简单的,并且很容易变得不可变。

那么,如何构建不可变类型呢?让以一个名为ProductPile的类为例,它代表出售的一堆产品:

public class ProductPile { public string ProductName { get; set; } public int Amount { get; set; } public decimal Price { get; set; } }

要使其不可变,需要将其属性标记为只读,并创建一个构造函数:

public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } }

假设需要在销售一件商品时将产品数量减少一个。不应该更改现有对象,而应该基于当前对象创建一个新的对象:

public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } public ProductPile SubtractOne() { return new ProductPile(ProductName, Amount - 1, Price); } }

这里得到了什么?有了不可变类,只需要在构造函数中验证一次它的代码契约。绝对确定对象总是处于正确的状态。对象自动是线程安全的。代码的可读性提高了,因为没有必要进入方法以确保它们不会更改任何东西。

局限性

当然,一切都有代价。虽然小型和简单的类最受益于不可变性,但这种方法并不总是适用于较大的类。

首先,与性能问题相关。如果对象相当大,每次更改都需要创建它的副本可能会影响应用程序的性能。

一个好例子是不可变集合。它们的作者考虑到了潜在的性能问题,并添加了一个Builder类,允许修改集合。准备完成后,可以将其转换为不可变集合:

var builder = ImmutableList.CreateBuilder<string>(); builder.Add("1"); ImmutableList<string> list = builder.ToImmutable(); ImmutableList<string> list2 = list.Add("2");

另一个问题是,有些类本质上是可变的,试图使它们不可变会带来更多的问题而不是解决。

但不要因为这些问题而阻止创建不可变数据类型。考虑每个设计决策的利弊,并始终考虑常识。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485