使用 init 与 with 实现不可变性
使用仅限 init 的设置器创建不可变对象,并使用 with 表达式生成以非破坏方式修改的副本。
使用 init 与 with 实现不可变性 是 CoddyKit 上的免费 C# Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
什么是不可变性
不可变对象在创建后不能被更改。不可变性可以防止意外修改,使对象默认具备线程安全性,并简化代码推理。C# 提供 init 设置器和 with 表达式,让实现不可变性更加便捷。
仅限 init 的设置器
init 访问器允许在对象初始化期间(通过构造函数或对象初始化器)设置属性,但之后不能再设置。它是 set 的不可变对应形式。
public class Point
{
public double X { get; init; }
public double Y { get; init; }
}
var p = new Point { X = 3.0, Y = 4.0 }; // OK — init phase
p.X = 5.0; // COMPILE ERROR — cannot set after init
// All of these are valid init-phase assignments:
var p2 = new Point(X: 1.0, Y: 2.0);
var p3 = new Point { X = 0, Y = 0 };记录位置参数属性中的 init
记录的位置参数属性默认仅支持 init——这正是记录不可变的原因。编译器会为每个位置参数生成 get; init;。
// This record:
public record Person(string Name, int Age);
// Is equivalent to:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string Name, int Age) { this.Name = Name; this.Age = Age; }
// + Deconstruct, Equals, GetHashCode, ToString
}
var alice = new Person("Alice", 30);
alice.Name = "Bob"; // COMPILE ERRORwith 表达式:非破坏性修改
with 表达式会创建记录的副本,并修改其中指定的属性。原记录保持不变,这才是真正的不可变性。
public record Person(string Name, int Age, string Email);
var alice = new Person("Alice", 30, "alice@example.com");
// Create a modified copy — alice is unchanged
var olderAlice = alice with { Age = 31 };
var renamed = alice with { Name = "Alicia", Email = "alicia@example.com" };
Console.WriteLine(alice.Name); // Alice (unchanged)
Console.WriteLine(olderAlice.Age); // 31
Console.WriteLine(renamed.Name); // Alicia在非记录类型上使用 with
C# 10 允许对任何具有复制构造函数语义的结构或类使用 with,但它与记录结合使用最为自然。对于类,您需要手动实现相应逻辑。
// struct with with-expression:
public struct Temperature
{
public double Celsius { get; init; }
public double Fahrenheit => Celsius * 9 / 5 + 32;
}
var t1 = new Temperature { Celsius = 20 };
var t2 = t1 with { Celsius = 25 }; // copy with change
Console.WriteLine(t1.Celsius); // 20 — unchanged
Console.WriteLine(t2.Celsius); // 25串联 with 表达式
可以串联多个 with 表达式来构建复杂转换,每一步都会根据前一步创建一个新的不可变值。
public record Order(int Id, string Status, decimal Total, DateTime UpdatedAt);
var order = new Order(42, "Pending", 99.99m, DateTime.UtcNow);
// Apply a promotion discount then mark as confirmed
var finalOrder = order
with { Total = order.Total * 0.9m } // 10% off
with { Status = "Confirmed" }
with { UpdatedAt = DateTime.UtcNow };
Console.WriteLine(order.Status); // Pending (original unchanged)
Console.WriteLine(finalOrder.Status); // Confirmed不可变类型上的计算属性
不可变类型上的派生属性天然是纯的——它们根据固定的属性值进行计算,并且对于相同的输入始终返回相同的结果。
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return this with { Amount = Amount + other.Amount };
}
public Money Multiply(decimal factor) =>
this with { Amount = Amount * factor };
public override string ToString() =>
$"{Amount:F2} {Currency}";
}
var price = new Money(10.00m, "USD");
var tax = price.Multiply(0.08m);
var total = price.Add(tax);
Console.WriteLine(total); // 10.80 USD不可变集合
将记录与 ImmutableList<T> 以及 System.Collections.Immutable 中的其他类型结合使用,可以构建完全不可变的数据结构。
using System.Collections.Immutable;
public record ShoppingCart(
string UserId,
ImmutableList<CartItem> Items)
{
public ShoppingCart AddItem(CartItem item) =>
this with { Items = Items.Add(item) };
public ShoppingCart RemoveItem(int productId) =>
this with { Items = Items.RemoveAll(i => i.ProductId == productId) };
public decimal Total => Items.Sum(i => i.Price * i.Quantity);
}不可变性带来的线程安全
不可变对象天然具备线程安全性——在多个线程之间共享它们时无需同步,因为对象构造完成后其状态无法改变。
// Immutable config record shared across all threads safely
public record AppConfig(
string ConnectionString,
int MaxRetries,
TimeSpan Timeout);
// Register as singleton — safe because record is immutable
builder.Services.AddSingleton(
new AppConfig(
ConnectionString: config["DB"]!,
MaxRetries: 3,
Timeout: TimeSpan.FromSeconds(30)));
// Any thread can read this simultaneously without locks实际应用:函数式事件溯源
不可变记录与事件溯源非常契合:每个领域事件都是不可变的,状态则通过汇总事件来派生——没有修改,也不会产生意外。
public record OrderState(
int Id,
string Status = "Draft",
decimal Total = 0m);
public static OrderState Apply(OrderState state, object evt) => evt switch
{
OrderPlaced e => state with { Status = "Pending", Total = e.Total },
OrderShipped _ => state with { Status = "Shipped" },
OrderCancelled _ => state with { Status = "Cancelled" },
_ => state
};
// Fold events to get current state:
var state = events.Aggregate(
new OrderState(id),
Apply);快速检查
“with”表达式会对原记录做什么?
总结:使用 init 与 with 实现不可变性
关键要点:
init访问器:只能在初始化期间设置,之后不能再设置- 记录的位置参数属性默认仅支持
init with表达式:创建修改后的副本——原记录保持不变- 串联
with表达式,实现多步骤转换 - 不可变类型无需同步即可保证线程安全
- 与
ImmutableList<T>结合,构建完全不可变的对象图
用 AI 导师学习 C# — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 93
- 课程
- 346
常见问题解答
「使用 init 与 with 实现不可变性」课时是免费的吗?
是的 — 「使用 init 与 with 实现不可变性」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「使用 init 与 with 实现不可变性」这节课中我会学到什么?
使用仅限 init 的设置器创建不可变对象,并使用 with 表达式生成以非破坏方式修改的副本。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「使用 init 与 with 实现不可变性」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 记录类型:基础与语法
- 使用 init 与 with 实现不可变性
- 值相等性与解构
- 领域驱动设计中的记录