Skip to content

Commit

Permalink
Clarify that positional parameters can create fields (dotnet#44691)
Browse files Browse the repository at this point in the history
* Clarify that positional parameters can create fields

Record types with positional parameters can synthesize fields as well as parameters. Make that more clear.

* fix warnings

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

Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com>

* respond to feedback

---------

Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com>
  • Loading branch information
BillWagner and gewarren authored Feb 5, 2025
1 parent 59d6b4d commit f4c9cfb
Show file tree
Hide file tree
Showing 10 changed files with 41 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/csharp/fundamentals/coding-style/identifier-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public record PhysicalAddress(
string ZipCode);
```

For more information on positional records, see [Positional syntax for property definition](../../language-reference/builtin-types/record.md#positional-syntax-for-property-definition).
For more information on positional records, see [Positional syntax for property definition](../../language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition).

### Camel case

Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/fundamentals/functional/deconstruct.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Some system types provide the `Deconstruct` method as a convenience. For example

## `record` types

When you declare a [record](../../language-reference/builtin-types/record.md) type by using two or more positional parameters, the compiler creates a `Deconstruct` method with an `out` parameter for each positional parameter in the `record` declaration. For more information, see [Positional syntax for property definition](../../language-reference/builtin-types/record.md#positional-syntax-for-property-definition) and [Deconstructor behavior in derived records](../../language-reference/builtin-types/record.md#deconstructor-behavior-in-derived-records).
When you declare a [record](../../language-reference/builtin-types/record.md) type by using two or more positional parameters, the compiler creates a `Deconstruct` method with an `out` parameter for each positional parameter in the `record` declaration. For more information, see [Positional syntax for property definition](../../language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition) and [Deconstructor behavior in derived records](../../language-reference/builtin-types/record.md#deconstructor-behavior-in-derived-records).

## See also

Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/fundamentals/types/records.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Immutability isn't appropriate for all data scenarios. [Entity Framework Core](/

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:

* You can use [positional parameters](../../language-reference/builtin-types/record.md#positional-syntax-for-property-definition) in a [primary constructor](../../programming-guide/classes-and-structs/instance-constructors.md#primary-constructors) to create and instantiate a type with immutable properties.
* 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.
* 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.
* You can use a [`with` expression](../../language-reference/builtin-types/record.md#nondestructive-mutation) to create a copy of an immutable object with new values in selected properties.
* A record's `ToString` method creates a formatted string that shows an object's type name and the names and values of all its public properties.
Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/language-reference/attributes/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ If <xref:System.AttributeUsageAttribute.Inherited> is `false`, then derived clas

In this case, `NonInheritedAttribute` isn't applied to `DClass` via inheritance.

You can also use these keywords to specify where an attribute should be applied. For example, you can use the `field:` specifier to add an attribute to the backing field of an [automatically implemented property](../../programming-guide/classes-and-structs/properties.md#automatically-implemented-properties). Or you can use the `field:`, `property:` or `param:` specifier to apply an attribute to any of the elements generated from a positional record. For an example, see [Positional syntax for property definition](../builtin-types/record.md#positional-syntax-for-property-definition).
You can also use these keywords to specify where an attribute should be applied. For example, you can use the `field:` specifier to add an attribute to the backing field of an [automatically implemented property](../../programming-guide/classes-and-structs/properties.md#automatically-implemented-properties). Or you can use the `field:`, `property:` or `param:` specifier to apply an attribute to any of the elements generated from a positional record. For an example, see [Positional syntax for property definition](../builtin-types/record.md#positional-syntax-for-property-and-field-definition).

## `AsyncMethodBuilder` attribute

Expand Down
22 changes: 13 additions & 9 deletions docs/csharp/language-reference/builtin-types/record.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Records"
description: Learn about the record type in C#
ms.date: 11/22/2023
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.
ms.date: 02/05/2025
f1_keywords:
- "record_CSharpKeyword"
helpviewer_keywords:
Expand Down Expand Up @@ -35,7 +35,7 @@ Record structs can be mutable as well, both positional record structs and record

While records can be mutable, they're primarily intended for supporting immutable data models. The record type offers the following features:

* [Concise syntax for creating a reference type with immutable properties](#positional-syntax-for-property-definition)
* [Concise syntax for creating a reference type with immutable properties](#positional-syntax-for-property-and-field-definition)
* Built-in behavior useful for a data-centric reference type:
* [Value equality](#value-equality)
* [Concise syntax for nondestructive mutation](#nondestructive-mutation)
Expand All @@ -49,9 +49,9 @@ The preceding examples show some distinctions between records that are reference

The remainder of this article discusses both `record class` and `record struct` types. The differences are detailed in each section. You should decide between a `record class` and a `record struct` similar to deciding between a `class` and a `struct`. The term *record* is used to describe behavior that applies to all record types. Either `record struct` or `record class` is used to describe behavior that applies to only struct or class types, respectively.

## Positional syntax for property definition
## Positional syntax for property and field definition

You can use positional parameters to declare properties of a record and to initialize the property values when you create an instance:
You can use positional parameters to declare properties of a record or to initialize property or field values. The following example creates a record with two positional properties:

:::code language="csharp" source="snippets/shared/RecordType.cs" id="InstantiatePositional":::

Expand All @@ -70,19 +70,23 @@ You might want to add attributes to any of these elements the compiler creates f

The preceding example also shows how to create XML documentation comments for the record. You can add the `<param>` tag to add documentation for the primary constructor's parameters.

If the generated automatically implemented property definition isn't what you want, you can define your own property of the same name. For example, you might want to change accessibility or mutability, or provide an implementation for either the `get` or `set` accessor. If you declare the property in your source, you must initialize it from the positional parameter of the record. If your property is an automatically implemented property, you must initialize the property. If you add a backing field in your source, you must initialize the backing field. The generated deconstructor uses your property definition. For instance, the following example declares the `FirstName` and `LastName` properties of a positional record `public`, but restricts the `Id` positional parameter to `internal`. You can use this syntax for records and record struct types.
If the generated automatically implemented property definition isn't what you want, you can define your own property or field of the same name. For example, you might want to change accessibility or mutability, or provide an implementation for either the `get` or `set` accessor. If you declare the member in your source, you must initialize it from the positional parameter of the record. If your property is an automatically implemented property, you must initialize the property. If you add a backing field in your source, you must initialize the backing field. The generated deconstructor uses your property or field definition. For instance, the following example declares the `FirstName` and `LastName` properties of a positional record `public`, but restricts the `Id` positional parameter to `internal`. You can use this syntax for records and record struct types.

:::code language="csharp" source="snippets/shared/RecordType.cs" id="PositionalWithManualProperty":::

If you want to create a field instead of a property, assign the positional parameter to a field, as shown in the following example:

:::code language="csharp" source="snippets/shared/RecordType.cs" id="PositionalWithManualField":::

A record type doesn't have to declare any positional properties. You can declare a record without any positional properties, and you can declare other fields and properties, as in the following example:

:::code language="csharp" source="snippets/shared/RecordType.cs" id="MixedSyntax":::

If you define properties by using standard property syntax but omit the access modifier, the properties are implicitly `private`.
Properties that the compiler generates from positional parameters are `public`. You declare the access modifiers on any properties you explicitly declare.

## Immutability

A *positional record* and a *positional readonly record struct* declare init-only properties. A *positional record struct* declares read-write properties. You can override either of those defaults, as shown in the previous section.
A *positional record class* and a *positional readonly record struct* declare init-only properties. A *positional record struct* declares read-write properties. You can override either of those defaults, as shown in the previous section.

Immutability can be useful when you need a data-centric type to be thread-safe or you're depending on a hash code remaining the same in a hash table. Immutability isn't appropriate for all data scenarios, however. [Entity Framework Core](/ef/core/), for example, doesn't support updating with immutable entity types.

Expand Down Expand Up @@ -211,7 +215,7 @@ Here's an example of code that replaces the synthesized `PrintMembers` methods,
:::code language="csharp" source="snippets/shared/RecordType.cs" id="PrintMembersImplementation":::

> [!NOTE]
> The compiler will synthesize `PrintMembers` in derived records even when a base record has sealed the `ToString` method. You can also create your own implementation of `PrintMembers`.
> The compiler synthesizes `PrintMembers` in derived records even when a base record sealed the `ToString` method. You can also create your own implementation of `PrintMembers`.
### Deconstructor behavior in derived records

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ public static void Main()
}
}

namespace positionalwithmanualfield
{
public static class Example
{
// <PositionalWithManualField>
public record Person(string FirstName, string LastName, string Id)
{
internal readonly string Id = Id; // this.Id set to parameter Id
}

public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy

}
// </PositionalWithManualField>
}
}

namespace shallowimmutability
{
public static class Example
Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/language-reference/keywords/required.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The `required` modifier indicates that the *field* or *property* it's applied to
- A type with any `required` members may not be used as a type argument when the type parameter includes the `new()` constraint. The compiler can't enforce that all required members are initialized in the generic code.
- The `required` modifier isn't allowed on the declaration for positional parameters on a record. You can add an explicit declaration for a positional property that does include the `required` modifier.

Some types, such as [positional records](../builtin-types/record.md#positional-syntax-for-property-definition), use a primary constructor to initialize positional properties. If any of those properties include the `required` modifier, the primary constructor adds the [`SetsRequiredMembers`](../attributes/general.md#setsrequiredmembers-attribute) attribute. This indicates that the primary constructor initializes all required members. You can write your own constructor with the <xref:System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute?displayProperty=nameWithType> attribute. However, the compiler doesn't verify that these constructors do initialize all required members. Rather, the attribute asserts to the compiler that the constructor does initialize all required members. The `SetsRequiredMembers` attribute adds these rules to constructors:
Some types, such as [positional records](../builtin-types/record.md#positional-syntax-for-property-and-field-definition), use a primary constructor to initialize positional properties. If any of those properties include the `required` modifier, the primary constructor adds the [`SetsRequiredMembers`](../attributes/general.md#setsrequiredmembers-attribute) attribute. This indicates that the primary constructor initializes all required members. You can write your own constructor with the <xref:System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute?displayProperty=nameWithType> attribute. However, the compiler doesn't verify that these constructors do initialize all required members. Rather, the attribute asserts to the compiler that the constructor does initialize all required members. The `SetsRequiredMembers` attribute adds these rules to constructors:

- A constructor that chains to another constructor annotated with the `SetsRequiredMembers` attribute, either `this()`, or `base()`, must also include the `SetsRequiredMembers` attribute. That ensures that callers can correctly use all appropriate constructors.
- Copy constructors generated for `record` types have the `SetsRequiredMembers` attribute applied if any of the members are `required`.
Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/language-reference/operators/deconstruction.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ In the preceding example, the `Y` and `label` members are discarded. You can spe

## Record deconstruction

[Record](../builtin-types/record.md) types that have a [primary constructor](../builtin-types/record.md#positional-syntax-for-property-definition) support deconstruction for positional parameters. The compiler synthesizes a `Deconstruct` method that extracts the properties synthesized from positional parameters in the primary constructor. The compiler-synthesized `Deconstruction` method doesn't extract properties declared as properties in the record type.
[Record](../builtin-types/record.md) types that have a [primary constructor](../builtin-types/record.md#positional-syntax-for-property-and-field-definition) support deconstruction for positional parameters. The compiler synthesizes a `Deconstruct` method that extracts the properties synthesized from positional parameters in the primary constructor. The compiler-synthesized `Deconstruction` method doesn't extract properties declared as properties in the record type.

The `record` shown in the following code declares two positional properties, `SquareFeet` and `Address`, along with another property, `RealtorNotes`:

Expand Down
2 changes: 1 addition & 1 deletion docs/csharp/language-reference/operators/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ You can also extend a positional pattern in any of the following ways:

:::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="WithTypeCheck":::

The preceding example uses [positional records](../builtin-types/record.md#positional-syntax-for-property-definition) that implicitly provide the `Deconstruct` method.
The preceding example uses [positional records](../builtin-types/record.md#positional-syntax-for-property-and-field-definition) that implicitly provide the `Deconstruct` method.

- Use a [property pattern](#property-pattern) within a positional pattern, as the following example shows:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Records are also supported for both serialization and deserialization, as shown

:::code language="csharp" source="snippets/how-to-contd/csharp/Records.cs":::

You can apply any of the attributes to the property names, using the `property:` target on the attribute. For more information on positional records, see the article on [records](../../../csharp/language-reference/builtin-types/record.md#positional-syntax-for-property-definition) in the C# language reference.
You can apply any of the attributes to the property names, using the `property:` target on the attribute. For more information on positional records, see the article on [records](../../../csharp/language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition) in the C# language reference.

## Non-public members and property accessors

Expand Down

0 comments on commit f4c9cfb

Please sign in to comment.