-
Notifications
You must be signed in to change notification settings - Fork 6k
C# language reference for records #23009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
a710a90
draft
tdykstra 16e5a5d
uncomment
tdykstra d17fe78
fix metadata
tdykstra f892814
fix sample output
tdykstra 0d8d639
proofread and acrolinx
tdykstra d900110
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 7b67707
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra b01d4b0
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 0fe9fc2
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 8d58f4e
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 101f20f
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 4b468a8
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 55445cb
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra 048ad75
move iequatable to equals bullet
tdykstra 357c5dd
ef core
tdykstra 8597f88
classes and structs doc
tdykstra 87f5885
add notes about records
tdykstra 70d4aee
Update docs/csharp/language-reference/builtin-types/record.md
tdykstra dbd5d41
programming guide article
tdykstra 027454d
Merge branch 'records' of https://github.com/tdykstra/dotnet-docs int…
tdykstra 8d83ff6
mention records in more places
tdykstra 5b27482
minor corrections
tdykstra c828132
add records
tdykstra 198ac48
fix links
tdykstra b41844f
proofread staging
tdykstra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
--- | ||
title: "Records - C# reference" | ||
description: Learn about the record type in C# | ||
ms.date: 02/25/2021 | ||
f1_keywords: | ||
- "record_CSharpKeyword" | ||
helpviewer_keywords: | ||
- "record keyword [C#]" | ||
- "record type [C#]" | ||
--- | ||
# Records (C# reference) | ||
|
||
Beginning with C# 9, you use the `record` keyword to define a [reference type](reference-types.md) that provides built-in functionality for encapsulating data. You can create record types with immutable properties by using positional parameters or standard property syntax: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="PositionalRecord"::: | ||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="ImmutableRecord"::: | ||
|
||
You can also create record types with mutable properties and fields: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="MutableRecord"::: | ||
|
||
Positional records create immutable data models. You can add additional properties that are mutable to positional records. You can control the mutability of any property in non-positional record types. | ||
|
||
The record type offers the following features: | ||
|
||
* [Concise syntax for creating a reference type with immutable properties](#positional-syntax-for-property-definition) | ||
* Built-in behavior useful for a data-centric reference type: | ||
* [Value equality](#value-equality) | ||
* [Concise syntax for nondestructive mutation](#nondestructive-mutation) | ||
tdykstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* [Built-in formatting for display](#built-in-formatting-for-display) | ||
* [Support for inheritance hierarchies](#inheritance) | ||
|
||
You can also use [structure types](struct.md) to design data-centric types that provide value equality and little or no behavior. But for relatively large data models, structure types have some disadvantages: | ||
|
||
* They don't support inheritance. | ||
* They're less efficient at determining value equality. For value types, the <xref:System.ValueType.Equals%2A?displayProperty=nameWithType> method uses reflection to find all fields. For records, the compiler generates the `Equals` method. In practice, the implementation of value equality in records is measurably faster. | ||
* They use more memory in some scenarios, since every instance has a complete copy of all of the data. Record types are [reference types](reference-types.md), so a record instance contains only a reference to the data. | ||
|
||
## Positional syntax for property definition | ||
|
||
You can use positional parameters to declare properties of a record and to initialize the property values when you create an instance: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="InstantiatePositional"::: | ||
|
||
When you use the positional syntax for property definition, the compiler creates: | ||
|
||
* A public init-only auto-property for each positional parameter provided in the record declaration. An [init-only](../../whats-new/csharp-9.md#init-only-setters) property can only be set in the constructor or by using a property initializer. | ||
* A primary constructor whose parameters match the positional parameters on the record declaration. | ||
* A `Deconstruct` method with an `out` parameter for each positional parameter provided in the record declaration. This method is provided only if there are two or more positional parameters. The method deconstructs properties defined by using positional syntax; it ignores properties that are defined by using standard property syntax. | ||
|
||
If the generated auto-property definition isn't what you want, you can define your own property of the same name. If you do that, the generated constructor and deconstructor will use your property definition. For instance, the following example makes the `FirstName` positional property `protected` instead of `public`. | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="PositionalWithManualProperty"::: | ||
|
||
A record type doesn't have to declare any positional properties. You can declare a record without any positional properties, and you can declare additional 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 `public`. | ||
<!-- Todo -- Explain issues surrounding use of attributes on positional properties. --> | ||
|
||
## Immutability | ||
|
||
A record type is not necessarily immutable. You can declare properties with `set` accessors and fields that aren't `readonly`. But while records can be mutable, they are primarily intended to support immutable data models. | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
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 5, for example, doesn't support updating with immutable entity types. | ||
|
||
|
||
Init-only properties, whether created from positional parameters or by specifying `init` accessors, have *shallow immutability*. After initialization, you can't change the value of value-type properties or the reference of reference-type properties. However, the data that a reference-type property refers to can be changed. The following example shows that the content of a reference-type immutable property (an array in this case) is mutable: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="ShallowImmutability"::: | ||
|
||
The features unique to record types are implemented by compiler-synthesized methods, and none of these methods compromises immutability by modifying object state. | ||
|
||
## Value equality | ||
|
||
Value equality means that two variables of a record type are equal if the types match and all property and field values match. For other reference types, equality means identity. That is, two variables of a reference type are equal if they refer to the same object. The following example illustrates value equality of record types: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="Equality"::: | ||
|
||
To implement value equality, the compiler synthesizes the following methods: | ||
BillWagner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
* An override of <xref:System.Object.Equals(System.Object)?displayProperty=nameWithType>. | ||
|
||
This method is used as the basis for the <xref:System.Object.Equals(System.Object,System.Object)?displayProperty=nameWithType> static method when both parameters are non-null. | ||
|
||
* A virtual `Equals` method whose parameter is the record type. | ||
|
||
The record type implements <xref:System.IEquatable%601> | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
* An override of <xref:System.Object.GetHashCode?displayProperty=nameWithType>. | ||
|
||
* Overrides of operators `==` and `!=`. | ||
|
||
In `class` types, you could manually override equality methods and operators to achieve value equality, but developing and testing that code would be time-consuming and error-prone. Having this functionality built-in prevents bugs that would result from forgetting to update custom override code when properties or fields are added or changed. | ||
|
||
You can write your own implementations to replace any of these synthesized methods. If a record type has a method that matches the signature of any synthesized method, the compiler doesn't synthesize that method. | ||
|
||
If you provide your own implementation of `Equals` in a record type, provide an implementation of `GetHashCode` also. | ||
|
||
## Nondestructive mutation | ||
tdykstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
If you need to mutate immutable properties of a record instance, you can use a `with` expression to achieve *nondestructive mutation*. A `with` expression makes a new record instance that is a copy of an existing record instance, with specified properties and fields modified. You use [object initializer](../../programming-guide/classes-and-structs/object-and-collection-initializers.md) syntax to specify the values to be changed, as shown in the following example: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WithExpressions"::: | ||
|
||
The `with` expression can set positional properties or properties created by using standard property syntax. Non-positional properties must have an `init` or `set` accessor to be changed in a `with` expression. | ||
|
||
The result of a `with` expression is a *shallow copy*, which means that for a reference property, only the reference to an instance is copied. Both the original record and the copy end up with a reference to the same instance. | ||
|
||
To implement this feature, the compiler synthesizes a clone method and a copy constructor. The constructor takes an instance of the record to be copied and calls the clone method. When you use a `with` expression, the compiler creates code that calls the copy constructor and then sets the properties that are specified in the `with` expression. | ||
|
||
If you need different copying behavior, you can write your own copy constructor. If you do that, the compiler won't synthesize one. Make your constructor `private` if the record is `sealed`, otherwise make it `protected`. | ||
|
||
You can't override the clone method, and you can't create a member named `Clone`. The actual name of the clone method is compiler-generated. | ||
|
||
## Built-in formatting for display | ||
|
||
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: | ||
|
||
> \<record type name> { \<property name> = \<value>, \<property name> = \<value>, ...} | ||
|
||
For reference types, the type name of the object that the property refers to is displayed instead of the property value. In the following example, the array is a reference type, so `System.String[]` is displayed instead of the actual array element values: | ||
|
||
``` | ||
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] } | ||
``` | ||
|
||
To implement this feature, the compiler synthesizes a `PrintMembers` method and a <xref:System.Object.ToString%2A> override. | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
The `ToString` override creates a <xref:System.Text.StringBuilder> object with the type name followed by an opening bracket. It calls `PrintMembers` to add property names and values, then adds the closing bracket. The following example shows code similar to what the synthesized override contains: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="ToStringOverrideDefault"::: | ||
|
||
You can provide your own implementation of `PrintMembers` or the `ToString` override. Examples are provided in the [inheritance section](#printmembers-formatting-in-derived-records) later in this article. | ||
|
||
## Inheritance | ||
|
||
A record can inherit from another record. However, a record can't inherit from a class, and a class can't inherit from a record. | ||
|
||
### Positional parameters in derived record types | ||
|
||
The derived record declares positional parameters for all the parameters in the base record primary constructor. The base record declares and initializes those properties. The derived record doesn't hide them, but only creates and initializes properties for parameters that aren't declared in its base record. | ||
|
||
The following example illustrates inheritance with positional property syntax: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="PositionalInheritance"::: | ||
|
||
### Equality in inheritance hierarchies | ||
|
||
For two record variables to be equal, the run-time type must be equal. The type of the containing variable might be different. This is illustrated in the following code example: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="InheritanceEquality"::: | ||
|
||
In the example, all instances have the same properties and the same property values. But `student == teacher` returns `False` although both are `Person`-type variables, and `student == student2` returns `True` although one is a `Person` variable and one is a `Student` variable. | ||
|
||
To implement this behavior, the compiler synthesizes an `EqualityContract` property that returns a <xref:System.Type> object that matches the type of the record. This enables the equality methods to compare the runtime type of objects when they are checking for equality. If the base type of a record is `object`, this property is `virtual`. If the base type is another record type, this property is an override. If the record type is `sealed`, this property is `sealed`. | ||
|
||
When comparing two instances of a derived type, the synthesized equality methods check all properties of the base and derived types for equality. The synthesized `GetHashCode` method uses the `GetHashCode` method from all properties and fields declared in the base type and the derived record type. | ||
|
||
### `with` expressions in derived records | ||
|
||
Because the synthesized clone method uses a [covariant return type](~/_csharplang/proposals/csharp-9.0/covariant-returns.md), the result of a `with` expression has the same run-time type as the expression's operand. All properties of the run-time type get copied, but you can only set properties of the compile-time type, as the following example shows: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="WithExpressionInheritance"::: | ||
|
||
### `PrintMembers` formatting in derived records | ||
|
||
the synthesized `PrintMembers` method of a derived record type calls the base implementation. The result is that all public properties and fields of both derived and base types are included in the `ToString` output, as shown in the following example: | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="ToStringInheritance"::: | ||
|
||
You can provide your own implementation of the `PrintMembers` method. If you do that, use the following signature: | ||
|
||
* For a `sealed` record: `private bool PrintMembers(StringBuilder builder)`; | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
* For a record that isn't `sealed` and derives from object (doesn't declare a base record): `protected virtual bool PrintMembers(StringBuilder builder);` | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
* For a record that isn't `sealed` and derives from another record: `protected override bool PrintMembers(StringBuilder builder);` | ||
|
||
Here is an example of code that replaces the synthesized `PrintMembers` methods, one for a record type that derives from object, and one for a record type that derives from another record: | ||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="PrintMembersImplementation"::: | ||
|
||
### Deconstructor behavior in derived records | ||
|
||
The `Deconstructor` method of a derived record returns the values of all positional properties of the compile-time type. If the variable type is a base record, only the base record properties are deconstructed unless the object is cast to the derived type. The following example demonstrates calling a deconstructor on a derived record. | ||
tdykstra marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
:::code language="csharp" source="snippets/shared/RecordType.cs" id="DeconstructorInheritance"::: | ||
|
||
## Generic constraints | ||
|
||
There is no generic constraint that requires a type to be a record. Records satisfy the `class` constraint. To make a constraint on a specific hierarchy of record types, put the constraint on the base record as you would a base class. For more information, see [Constraints on type parameters](../../programming-guide/generics/constraints-on-type-parameters.md). | ||
|
||
## C# language specification | ||
|
||
For more information, see the [Classes](~/_csharplang/spec/classes.md) section of the [C# language specification](~/_csharplang/spec/introduction.md). | ||
|
||
For more information about features introduced in C# 9 and later, see the following feature proposal notes: | ||
|
||
- [Records](~/_csharplang/proposals/csharp-9.0/records.md) | ||
- [Init-only setters](~/_csharplang/proposals/csharp-9.0/init.md) | ||
- [Covariant returns](~/_csharplang/proposals/csharp-9.0/covariant-returns.md) | ||
|
||
## See also | ||
|
||
- [C# reference](../index.md) | ||
- [Design guidelines - Choosing between class and struct](../../../standard/design-guidelines/choosing-between-class-and-struct.md) | ||
- [Design guidelines - Struct design](../../../standard/design-guidelines/struct.md) | ||
- [Classes and structs](../../programming-guide/classes-and-structs/index.md) | ||
- [`with` expression](../operators/with-expression.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.