Skip to content

Commit fe08267

Browse files
BillWagnergewarren
andauthored
Clarify with initialization order (#47977)
* Clarify `with` initialization order Fixes #47503 In articles where `record` and `with` expressions are explained, add details about the order of initialization and with expressions. Provide guidance to compute the property value on access, not initialization. * Grammar pass + build issues * Apply suggestions from code review Co-authored-by: Genevieve Warren <[email protected]> --------- Co-authored-by: Genevieve Warren <[email protected]>
1 parent b8beea4 commit fe08267

File tree

4 files changed

+82
-9
lines changed

4 files changed

+82
-9
lines changed

docs/csharp/fundamentals/types/records.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Record types"
33
description: Learn about C# record types and how to create them. A record is a class that provides value semantics.
4-
ms.date: 05/24/2023
4+
ms.date: 08/15/2025
55
helpviewer_keywords:
66
- "records [C#]"
77
- "C# language, records"
@@ -31,7 +31,7 @@ Immutability isn't appropriate for all data scenarios. [Entity Framework Core](/
3131

3232
## How records differ from classes and structs
3333

34-
The same syntax that [declares](classes.md#declaring-classes) and [instantiates](classes.md#creating-objects) classes or structs can be used with records. Just substitute the `class` keyword with the `record`, or use `record struct` instead of `struct`. Likewise, the same syntax for expressing inheritance relationships is supported by record classes. Records differ from classes in the following ways:
34+
The same syntax that [declares](classes.md#declaring-classes) and [instantiates](classes.md#creating-objects) classes or structs can be used with records. Just substitute the `class` keyword with the `record`, or use `record struct` instead of `struct`. Likewise, record classes support the same syntax for expressing inheritance relationships. Records differ from classes in the following ways:
3535

3636
* You can use [positional parameters](../../language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition) in a [primary constructor](../../programming-guide/classes-and-structs/instance-constructors.md#primary-constructors) to create and instantiate a type with immutable properties.
3737
* The same methods and operators that indicate reference equality or inequality in classes (such as <xref:System.Object.Equals(System.Object)?displayProperty=nameWithType> and `==`), indicate [value equality or inequality](../../language-reference/builtin-types/record.md#value-equality) in records.
@@ -57,6 +57,8 @@ The following example demonstrates use of a `with` expression to copy an immutab
5757

5858
:::code language="csharp" source="./snippets/records/ImmutableRecord.cs" id="ImmutableRecord":::
5959

60+
In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy. If you must initialize a computed property rather than compute on access, you should consider a [`class`](./classes.md) instead of a record.
61+
6062
For more information, see [Records (C# reference)](../../language-reference/builtin-types/record.md).
6163

6264
## C# Language Specification

docs/csharp/language-reference/builtin-types/record.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Records"
33
description: Learn about the record modifier for class and struct types in C#. Records provide standard support for value based equality on instances of record types.
4-
ms.date: 02/05/2025
4+
ms.date: 08/15/2025
55
f1_keywords:
66
- "record_CSharpKeyword"
77
helpviewer_keywords:
@@ -146,6 +146,27 @@ If you need different copying behavior, you can write your own copy constructor
146146

147147
You can't override the clone method, and you can't create a member named `Clone` in any record type. The actual name of the clone method is compiler-generated.
148148

149+
> [!IMPORTANT]
150+
> In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy.
151+
152+
You ensure correctness on computed properties by computing the value on access, as shown in the following declaration:
153+
154+
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherComputed":::
155+
156+
The preceding record type computes the `Distance` when accessed, as shown in the following example:
157+
158+
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherComputedUsage":::
159+
160+
Contrast that with the following declaration, where the `Distance` property is computed and cached as part of the initialization of a new instance:
161+
162+
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherInit":::
163+
164+
Because `Distance` is computed as part of initialization, the value is computed and cached before the `with` expression changes the value of `Y` in the copy. The result is that the distance is incorrect:
165+
166+
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WitherInitUsage":::
167+
168+
The `Distance` computation isn't expensive to compute on each access. However, some computed properties might require access to more data or more extensive computation. In those cases, instead of a record, use a `class` type and compute the cached value when one of the components changes value.
169+
149170
## Built-in formatting for display
150171

151172
Record types have a compiler-generated <xref:System.Object.ToString%2A> method that displays the names and values of public properties and fields. The `ToString` method returns a string of the following format:

docs/csharp/language-reference/builtin-types/snippets/shared/RecordType.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public static void Examples()
2323
var p = new Point();
2424
(double x, double y, double z) = p;
2525

26+
Console.WriteLine("=================================================");
27+
ComputedWither.ExampleUsage.Example();
28+
2629
}
2730
// <PositionalRecord>
2831
public record Person(string FirstName, string LastName);
@@ -102,7 +105,7 @@ public static class PositionalAttributes
102105
/// map to the JSON elements "firstName" and "lastName" when
103106
/// serialized or deserialized.
104107
/// </remarks>
105-
public record Person([property: JsonPropertyName("firstName")] string FirstName,
108+
public record Person([property: JsonPropertyName("firstName")] string FirstName,
106109
[property: JsonPropertyName("lastName")] string LastName);
107110
// </PositionalAttributes>
108111

@@ -335,7 +338,8 @@ protected override bool PrintMembers(StringBuilder stringBuilder)
335338
if (base.PrintMembers(stringBuilder))
336339
{
337340
stringBuilder.Append(", ");
338-
};
341+
}
342+
;
339343
stringBuilder.Append($"Grade = {Grade}");
340344
return true;
341345
}
@@ -426,4 +430,47 @@ public static void Main()
426430
// </WithExpressionInheritance>
427431
}
428432
}
433+
434+
namespace ComputedWither
435+
{
436+
// <WitherComputed>
437+
public record Point(int X, int Y)
438+
{
439+
public double Distance => Math.Sqrt(X * X + Y * Y);
440+
}
441+
// </WitherComputed>
442+
443+
// <WitherInit>
444+
public record PointInit(int X, int Y)
445+
{
446+
public double Distance { get; } = Math.Sqrt(X * X + Y * Y);
447+
}
448+
// </WitherInit>
449+
450+
public static class ExampleUsage
451+
{
452+
public static void Example()
453+
{
454+
// <WitherComputedUsage>
455+
Point p1 = new Point(3, 4);
456+
Console.WriteLine($"Original point: {p1}");
457+
p1 = p1 with { Y = 8 };
458+
Console.WriteLine($"Modified point: {p1}");
459+
// Output:
460+
// Original point: Point { X = 3, Y = 4, Distance = 5 }
461+
// Modified point: Point { X = 3, Y = 8, Distance = 8.54400374531753 }
462+
// </WitherComputedUsage>
463+
464+
// <WitherInitUsage>
465+
PointInit pt1 = new PointInit(3, 4);
466+
Console.WriteLine($"Original point: {pt1}");
467+
pt1 = pt1 with { Y = 8 };
468+
Console.WriteLine($"Incorrect Modified point: {pt1}");
469+
// Output:
470+
// Original point: PointInit { X = 3, Y = 4, Distance = 5 }
471+
// Modified point: PointInit { X = 3, Y = 8, Distance = 5 }
472+
// </WitherInitUsage>
473+
}
474+
}
475+
}
429476
}

docs/csharp/language-reference/operators/with-expression.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
2-
title: "with expression - create new objects that are modified copies of existing objects"
2+
title: "The with expression - create new objects that are modified copies of existing objects"
33
description: "Learn about a with expression that performs nondestructive mutation of C# records and structures. The `with` keyword provides the means to modify one or more properties in the new object."
4-
ms.date: 11/22/2024
4+
ms.date: 08/15/2025
55
f1_keywords:
66
- "with_CSharpKeyword"
77
helpviewer_keywords:
88
- "with expression [C#]"
99
- "with operator [C#]"
1010
---
11-
# with expression - Nondestructive mutation creates a new object with modified properties
11+
# The `with` expression - Nondestructive mutation creates a new object with modified properties
1212

1313
A `with` expression produces a copy of its operand with the specified properties and fields modified. You use the [object initializer](../../programming-guide/classes-and-structs/object-and-collection-initializers.md) syntax to specify what members to modify and their new values:
1414

@@ -20,7 +20,7 @@ The result of a `with` expression has the same run-time type as the expression's
2020

2121
:::code language="csharp" source="snippets/with-expression/InheritanceExample.cs" :::
2222

23-
In the case of a reference-type member, only the reference to a member instance is copied when an operand is copied. Both the copy and original operand have access to the same reference-type instance. The following example demonstrates that behavior:
23+
When a member is a reference type, only the reference to a member instance is copied when an operand is copied. Both the copy and original operand have access to the same reference-type instance. The following example demonstrates that behavior:
2424

2525
:::code language="csharp" source="snippets/with-expression/ExampleWithReferenceType.cs" :::
2626

@@ -32,6 +32,9 @@ Any record class type has the *copy constructor*. A *copy constructor* is a cons
3232

3333
You can't customize the copy semantics for structure types.
3434

35+
> [!IMPORTANT]
36+
> In the preceding examples, all properties are independent. None of the properties are computed from other property values. A `with` expression first copies the existing record instance, then modifies any properties or fields specified in the `with` expression. Computed properties in `record` types should be computed on access, not initialized when the instance is created. Otherwise, a property could return the computed value based on the original instance, not the modified copy. For more information, see the language reference article on [`record` types](../builtin-types/record.md#nondestructive-mutation).
37+
3538
## C# language specification
3639

3740
For more information, see the following sections of the [records feature proposal note](~/_csharplang/proposals/csharp-9.0/records.md):

0 commit comments

Comments
 (0)