C# 9的init魔法:比readonly更优雅的不可变性设计

作者:微信公众号:【架构师老卢】
7-7 7:58
3

C#长久以来通过readonly字段支持不可变对象,但C# 9引入的init关键字带来了更灵活的不可变性控制方案。这个看似简单的语法糖,能在保持代码简洁性的同时,为你的对象构建过程加上编译时安全锁。

🔍 init是什么?
表面看init类似属性setter,但它的核心区别在于:仅允许在对象初始化阶段赋值,构造完成后立即变为只读状态。

传统可变属性:

public class Person
{
    public string Name { get; set; }  // 随时可修改
    public int Age { get; set; }
}

使用init的不可变属性:

public class Person
{
    public string Name { get; init; }  // 仅初始化时可设值
    public int Age { get; init; }
}

🚀 init的三大优势

  1. 强制不可变模式
    防止对象构造后意外修改,杜绝副作用。以下代码将引发编译错误:
var person = new Person { Name = "Alice" };
person.Name = "Bob";  // ❌ CS0272 不可修改
  1. 保持对象初始化器的优雅语法
// 初始化阶段仍可使用流畅语法
var product = new ProductDTO { 
    Id = 1001, 
    Name = "Surface Pro" 
};
  1. 完美适配DTO和值类型
    特别适合API响应模型等场景:
public class ApiResponse<T>
{
    public T Data { get; init; }
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}

与readonly的终极对比 | 特性 | readonly字段 | init属性 | |---------------------|---------------------|-----------------------| | 赋值时机 | 仅构造函数内 | 对象初始化阶段 | | 是否支持初始化器语法 | ❌ 不支持 | ✅ 完美支持 | | 适用场景 | 完全不可变的核心字段 | 需初始化灵活性的模型 |

🔥 与record的黄金组合
C# 9的record类型本身具有值语义不可变性,配合init实现双重保护:

public record FinancialTransaction(
    decimal Amount,
    string Currency)
{
    public Guid TransactionId { get; init; } = Guid.NewGuid();
}

// 使用示例
var tx = new FinancialTransaction(99.99m, "USD") { 
    TransactionId = Guid.Parse("...") 
};

💡 设计决策指南

  • 需要完全不可变?用readonly字段+构造函数
  • 需要初始化灵活性?用init属性
  • 需要值语义比较?用record+init

🎯 最佳实践场景

  1. API响应模型 - 防止序列化后篡改
  2. 配置对象 - 确保运行时配置不可变
  3. 领域值对象 - 配合DDD模式使用
  4. 并发共享数据 - 天然线程安全

init关键字虽小,却是C#不可变性设计的重要拼图。它巧妙地填补了readonly严格限制与setter完全自由之间的空白,让开发者能以最小成本获得最大安全性。下次设计模型时,不妨让init成为你的首选武器。

相关留言评论
昵称:
邮箱:
阅读排行