From 020de5352bf1341500f35410b20294b8d1f46d44 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 25 Mar 2026 14:28:40 -0400 Subject: [PATCH 1/8] Create new structs article Create the new fundamentals article on struct types. --- docs/csharp/fundamentals/types/index.md | 2 +- .../types/snippets/structs/Program.cs | 138 ++++++++++++++++++ .../types/snippets/structs/structs.csproj | 10 ++ docs/csharp/fundamentals/types/structs.md | 100 +++++++++++++ docs/csharp/toc.yml | 3 +- 5 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 docs/csharp/fundamentals/types/snippets/structs/Program.cs create mode 100644 docs/csharp/fundamentals/types/snippets/structs/structs.csproj create mode 100644 docs/csharp/fundamentals/types/structs.md diff --git a/docs/csharp/fundamentals/types/index.md b/docs/csharp/fundamentals/types/index.md index a979d12439b60..bc092d5ee529f 100644 --- a/docs/csharp/fundamentals/types/index.md +++ b/docs/csharp/fundamentals/types/index.md @@ -40,7 +40,7 @@ C# provides [built-in types](built-in-types.md) for common data: integers, float Beyond built-in types, you can create your own types by using several constructs: - [**Classes**](classes.md) — Reference types for modeling behavior and complex objects. Support inheritance and polymorphism. -- [**Structs**](../../language-reference/builtin-types/struct.md) — Value types for small, lightweight data. Each variable holds its own copy. +- [**Structs**](structs.md) — Value types for small, lightweight data. Each variable holds its own copy. - [**Records**](records.md) — Classes or structs with compiler-generated equality, `ToString`, and nondestructive mutation through `with` expressions. - [**Interfaces**](interfaces.md) — Contracts that define members any class or struct can implement. - [**Enumerations**](enums.md) — Named sets of integral constants, such as days of the week or file access modes. diff --git a/docs/csharp/fundamentals/types/snippets/structs/Program.cs b/docs/csharp/fundamentals/types/snippets/structs/Program.cs new file mode 100644 index 0000000000000..ab223d74f5677 --- /dev/null +++ b/docs/csharp/fundamentals/types/snippets/structs/Program.cs @@ -0,0 +1,138 @@ +// --- Top-level statements (must precede type declarations) --- + +// +var p1 = new Point { X = 3, Y = 4 }; +var p2 = p1; // copies the data +p2.X = 10; + +Console.WriteLine(p1); // (3, 4) — p1 is unchanged +Console.WriteLine(p2); // (10, 4) — only p2 was modified +// + +// +var custom = new ConnectionSettings(); +Console.WriteLine($"{custom.Host}:{custom.Port} (retries: {custom.MaxRetries})"); +// localhost:8080 (retries: 3) + +var defaults = default(ConnectionSettings); +Console.WriteLine($"{defaults.Host ?? "(null)"}:{defaults.Port} (retries: {defaults.MaxRetries})"); +// (null):0 (retries: 0) +// + +// +var tile = new GameTile(2, 5); +Console.WriteLine($"Tile ({tile.Row}, {tile.Column}), blocked: {tile.IsBlocked}"); +// Tile (2, 5), blocked: False +// + +// +var temp = new Temperature(100); +Console.WriteLine(temp); // 100.0°C (212.0°F) +// temp.Celsius = 50; // Error: property is read-only +// + +// +var v = new Velocity { X = 3, Y = 4 }; +Console.WriteLine(v.Speed); // 5 +Console.WriteLine(v); // (3, 4) speed=5.00 +v.X = 6; +Console.WriteLine(v.Speed); // 7.211... +// + +// +var home = new Coordinate(47.6062, -122.3321); +var copy = home; + +Console.WriteLine(home); // Coordinate { Latitude = 47.6062, Longitude = -122.3321 } +Console.WriteLine(home == copy); // True — value equality + +var shifted = home with { Longitude = -122.0 }; +Console.WriteLine(shifted); // Coordinate { Latitude = 47.6062, Longitude = -122 } +Console.WriteLine(home == shifted); // False +// + +// +var (lat, lon) = home; +Console.WriteLine($"Lat: {lat}, Lon: {lon}"); +// Lat: 47.6062, Lon: -122.3321 +// + +// --- Type declarations --- + +// +struct Point +{ + public double X { get; set; } + public double Y { get; set; } + + public readonly double DistanceTo(Point other) + { + var dx = X - other.X; + var dy = Y - other.Y; + return Math.Sqrt(dx * dx + dy * dy); + } + + public override string ToString() => $"({X}, {Y})"; +} +// + +// +struct ConnectionSettings +{ + public string Host { get; set; } + public int Port { get; set; } + public int MaxRetries { get; set; } + + public ConnectionSettings() + { + Host = "localhost"; + Port = 8080; + MaxRetries = 3; + } +} +// + +// +struct GameTile +{ + public int Row { get; set; } + public int Column { get; set; } + public bool IsBlocked { get; set; } + + public GameTile(int row, int column) + { + Row = row; + Column = column; + // IsBlocked is automatically initialized to false + } +} +// + +// +readonly struct Temperature +{ + public double Celsius { get; } + + public Temperature(double celsius) => Celsius = celsius; + + public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0; + + public override string ToString() => $"{Celsius:F1}°C ({Fahrenheit:F1}°F)"; +} +// + +// +struct Velocity +{ + public double X { get; set; } + public double Y { get; set; } + + public readonly double Speed => Math.Sqrt(X * X + Y * Y); + + public readonly override string ToString() => $"({X}, {Y}) speed={Speed:F2}"; +} +// + +// +record struct Coordinate(double Latitude, double Longitude); +// diff --git a/docs/csharp/fundamentals/types/snippets/structs/structs.csproj b/docs/csharp/fundamentals/types/snippets/structs/structs.csproj new file mode 100644 index 0000000000000..dfb40caafcf9a --- /dev/null +++ b/docs/csharp/fundamentals/types/snippets/structs/structs.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/csharp/fundamentals/types/structs.md b/docs/csharp/fundamentals/types/structs.md new file mode 100644 index 0000000000000..7e0d3ab5f4625 --- /dev/null +++ b/docs/csharp/fundamentals/types/structs.md @@ -0,0 +1,100 @@ +--- +title: "C# structs" +description: Learn how to define and use structs in C#, including value semantics, parameterless constructors, auto-default behavior, readonly members, and record structs. +ms.date: 03/25/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- +# C# structs + +> [!TIP] +> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter structs once you need lightweight value types in your code. +> +> **Experienced in another language?** C# structs are value types similar to structs in C++ or Swift, but they live on the managed heap when boxed and support interfaces, constructors, and methods. Skim the [readonly structs](#readonly-structs-and-readonly-members) and [record structs](#record-structs) sections for C#-specific patterns. + +A *struct* is a value type that holds its data directly in the variable, rather than through a reference to an object on the heap. When you assign a struct to a new variable, the runtime copies the entire value. Changes to one variable don't affect the other. Use structs for small, lightweight data where identity isn't important—coordinates, colors, measurements, or configuration settings. + +## Declare a struct + +Define a struct with the `struct` keyword. A struct can contain fields, properties, methods, and constructors, just like a class: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="BasicStruct"::: + +The `Point` struct stores two `double` values and provides a method to calculate the distance between two points. The `DistanceTo` method is marked `readonly` because it doesn't modify the struct's state—a pattern covered in [readonly members](#readonly-structs-and-readonly-members). + +## Value semantics + +Structs are *value types*. Assignment copies the data, so each variable holds its own independent copy: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="ValueSemantics"::: + +This copy-on-assignment behavior differs from [classes](classes.md), where assignment copies only the reference. Both variables then point to the same object. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). + +## Struct constructors + +You can define constructors in structs the same way you do in classes. Starting with C# 10, structs can have *parameterless constructors* that set custom default values: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="ParameterlessConstructor"::: + +A parameterless constructor runs when you use `new` with no arguments. The `default` expression bypasses the constructor and sets all fields to their default values (`0`, `null`, `false`). Be aware of the difference: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="DefaultVsConstructor"::: + +## Auto-default structs + +Starting with C# 11, the compiler automatically initializes any fields you don't explicitly set in a constructor. Before C# 11, every field had to be assigned before the constructor returned. Now you can initialize only the fields that need non-default values: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="AutoDefault"::: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="UsingAutoDefault"::: + +The `IsBlocked` property isn't assigned in the constructor, so the compiler sets it to `false` (the default for `bool`). This feature reduces boilerplate in constructors that only need to set a few fields. + +## Readonly structs and readonly members + +A `readonly struct` guarantees that no instance member modifies the struct's state. The compiler enforces this guarantee by requiring all fields and auto-implemented properties to be read-only: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="ReadonlyStruct"::: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="UsingReadonly"::: + +When you don't need the entire struct to be immutable, you can mark individual members as `readonly` instead. A `readonly` member can't modify the struct's state, and the compiler verifies that guarantee: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="ReadonlyMembers"::: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="UsingReadonlyMembers"::: + +Marking members `readonly` helps the compiler optimize defensive copies. When you pass a `readonly` struct to a method that accepts an `in` parameter, the compiler knows no copy is needed. + +## Record structs + +A `record struct` combines value type semantics with the compiler-generated members that records provide—value equality, formatted `ToString`, and nondestructive mutation through `with` expressions. Declare a record struct with positional parameters for concise syntax: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="RecordStruct"::: + +The compiler generates properties, `Equals`, `GetHashCode`, `ToString`, and a `Deconstruct` method from the positional parameters: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="UsingRecordStruct"::: + +You can also deconstruct a positional record struct into individual variables: + +:::code language="csharp" source="snippets/structs/Program.cs" ID="RecordStructDeconstruct"::: + +Record structs are mutable by default, unlike `record class` types, which use `init`-only properties. Add the `readonly` modifier (`readonly record struct`) when you want immutability. For a deeper look at records, including record classes, inheritance, and customization, see [Records](records.md). + +## Choose structs or classes + +Use a struct when your type: + +- Represents a single value or a small group of related values (roughly 16 bytes or less). +- Has value semantics—two instances with the same data should be equal. +- Doesn't need inheritance from a base type (structs can't inherit from other structs or classes, but they can implement interfaces). + +Use a class when your type has complex behavior, needs inheritance, or when instances represent a shared identity rather than a copied value. For a broader comparison that includes records, tuples, and interfaces, see [Choose which kind of type](index.md#choose-which-kind-of-type). + +## See also + +- [Type system overview](index.md) +- [Classes](classes.md) +- [Records](records.md) +- [Structure types (C# reference)](../../language-reference/builtin-types/struct.md) diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 20a0944fd0842..9295be1d79d56 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -59,7 +59,8 @@ items: # TODO: Edit Records article so it doesn't depend on classes. - name: Records href: fundamentals/types/records.md - # TODO: structs + - name: Structs + href: fundamentals/types/structs.md - name: Interfaces href: fundamentals/types/interfaces.md - name: Enumerations From c47c54c9ba8013b676a0148074d46015347a79cd Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 25 Mar 2026 14:41:43 -0400 Subject: [PATCH 2/8] Revise classes articles. --- docs/csharp/fundamentals/types/classes.md | 118 +++++++++--------- .../types/snippets/classes/Containers.cs | 9 +- .../types/snippets/classes/Program.cs | 104 +++++++++++---- .../types/snippets/classes/classes.csproj | 2 +- 4 files changed, 145 insertions(+), 88 deletions(-) diff --git a/docs/csharp/fundamentals/types/classes.md b/docs/csharp/fundamentals/types/classes.md index 67a764635fa97..68720e7381220 100644 --- a/docs/csharp/fundamentals/types/classes.md +++ b/docs/csharp/fundamentals/types/classes.md @@ -1,105 +1,103 @@ --- -title: "Classes in the C# type system." -description: Learn about class types, how to use classes, and how to create new class type declarations for your app. -ms.date: 10/10/2025 -helpviewer_keywords: - - "classes [C#]" - - "C# language, classes" +title: "C# classes" +description: Learn how to define and use classes in C#, including reference type semantics, constructors, static classes, object initializers, collection initializers, and inheritance basics. +ms.date: 03/25/2026 +ms.topic: concept-article +ai-usage: ai-assisted --- -# Introduction to classes +# C# classes -## Reference types +> [!TIP] +> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter classes once you need to model objects with behavior and state. +> +> **Experienced in another language?** C# classes are similar to classes in Java or C++. Skim the [object initializers](#object-initializers) and [collection initializers](#collection-initializers) sections for C#-specific patterns, and see [Records](records.md) for a data-focused alternative. -A type that is defined as a [`class`](../../language-reference/keywords/class.md) is a *reference type*. At run time, when you declare a variable of a reference type, the variable contains the value [`null`](../../language-reference/keywords/null.md) until you explicitly create an instance of the class by using the [`new`](../../language-reference/operators/new-operator.md) operator, or assign it an object of a compatible type created elsewhere, as shown in the following example: +A *class* is a reference type that defines a blueprint for objects. When you create a variable of a class type, the variable holds a *reference* to an object on the managed heap—not the object data itself. Assigning a class variable to another variable copies the reference, so both variables point to the same object. Classes are the most common way to define custom types in C#. Use them when you need complex behavior, inheritance, or shared identity between references. -```csharp -// Declaring an object of type MyClass. -MyClass mc = new MyClass(); +## Declare a class -// Declaring another object of the same type, assigning it the value of the first object. -MyClass mc2 = mc; -``` +Define a class with the `class` keyword followed by the type name. An optional access modifier controls visibility—the default is `internal`: -When the object is created, enough memory is allocated on the managed heap for that specific object, and the variable holds only a reference to the location of said object. The memory used by an object is reclaimed by the automatic memory management functionality of the Common Language Runtime (CLR), which is known as *garbage collection*. For more information about garbage collection, see [Automatic memory management and garbage collection](../../../standard/garbage-collection/fundamentals.md). +:::code language="csharp" source="snippets/classes/Program.cs" ID="ClassDeclaration"::: -## Declaring classes +The class body contains fields, properties, methods, and events, collectively called *class members*. The name must be a valid C# [identifier name](../coding-style/identifier-names.md). -Classes are declared by using the `class` keyword followed by a unique identifier, as shown in the following example: +## Create objects -:::code source="./snippets/classes/Program.cs" id="ClassDeclaration"::: +A class defines a type, but it isn't an object itself. You create an object (an *instance* of the class) with the `new` keyword: -An optional access modifier precedes the `class` keyword. The default access for a `class` type is `internal`. Because [`public`](../../language-reference/keywords/public.md) is used in this case, anyone can create instances of this class. The name of the class follows the `class` keyword. The name of the class must be a valid C# [identifier name](../coding-style/identifier-names.md). The remainder of the definition is the class body, where the behavior and data are defined. Fields, properties, methods, and events on a class are collectively referred to as *class members*. +:::code language="csharp" source="snippets/classes/Program.cs" ID="CreateObject"::: -## Creating objects +The variable `customer` holds a reference to the object, not the object itself. You can assign multiple variables to the same object—changes through one reference are visible through the other: -Although they're sometimes used interchangeably, a class and an object are different things. A class defines a type of object, but it isn't an object itself. An object is a concrete entity based on a class, and is sometimes referred to as an instance of a class. +:::code language="csharp" source="snippets/classes/Program.cs" ID="ReferenceSemantics"::: -Objects can be created by using the `new` keyword followed by the name of the class, like this: +This reference-sharing behavior is what distinguishes classes from [structs](structs.md), where assignment copies the data. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). -:::code source="./snippets/classes/Program.cs" id="InstantiateClass"::: +## Constructors and initialization -When an instance of a class is created, a reference to the object is passed back to the programmer. In the previous example, `object1` is a reference to an object that is based on `Customer`. This reference refers to the new object but doesn't contain the object data itself. In fact, you can create an object reference without creating an object at all: +When you create an instance, you want its fields and properties initialized to useful values. C# offers several approaches: -:::code source="./snippets/classes/Program.cs" id="DeclareVariable"::: +**Field initializers** set a default value directly on the field declaration: -We don't recommend creating object references that don't refer to an object because trying to access an object through such a reference fails at run time. A reference can refer to an object, either by creating a new object, or by assigning it an existing object, such as this: +:::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerFieldInitializer"::: -:::code source="./snippets/classes/Program.cs" id="AssignReference"::: +**Constructor parameters** require callers to provide values: -This code creates two object references that both refer to the same object. Therefore, any changes to the object made through `object3` are reflected in subsequent uses of `object4`. Because objects that are based on classes are referred to by reference, classes are known as reference types. +:::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerConstructor"::: -## Constructors and initialization +**Primary constructors** (C# 12) add parameters directly to the class declaration. Those parameters are available throughout the class body: + +:::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerPrimaryConstructor"::: -The preceding sections introduced the syntax to declare a class type and create an instance of that type. When you create an instance of a type, you want to ensure that its fields and properties are initialized to useful values. There are several ways to initialize values: +**Required properties** enforce that callers set specific properties through an object initializer: -- Accept default values -- Field initializers -- Constructor parameters -- Object initializers +:::code language="csharp" source="snippets/classes/Program.cs" ID="RequiredProperties"::: -Every .NET type has a default value. Typically, that value is 0 for number types, and `null` for all reference types. You can rely on that default value when it's reasonable in your app. +:::code language="csharp" source="snippets/classes/Program.cs" ID="UsingRequired"::: -When the .NET default isn't the right value, you can set an initial value using a *field initializer*: +For a deeper look at constructor patterns, including parameter validation and constructor chaining, see [Constructors](../object-oriented/index.md). -:::code source="./snippets/classes/Containers.cs" id="ContainerFieldInitializer"::: +## Static classes -You can require callers to provide an initial value by defining a *constructor* that's responsible for setting that initial value: +A `static` class can't be instantiated and contains only static members. Use static classes as containers for utility methods that don't operate on instance data: -:::code source="./snippets/classes/Containers.cs" id="ContainerConstructor"::: +:::code language="csharp" source="snippets/classes/Program.cs" ID="StaticClass"::: -Beginning with C# 12, you can define a *primary constructor* as part of the class declaration: +:::code language="csharp" source="snippets/classes/Program.cs" ID="UsingStaticClass"::: -:::code source="./snippets/classes/Containers.cs" id="ContainerPrimaryConstructor"::: +The .NET class library includes many static classes, such as and . A static class is sealed implicitly—you can't derive from it or instantiate it. -Adding parameters to the class name defines the *primary constructor*. Those parameters are available in the class body, which includes its members. You can use them to initialize fields or anywhere else where they're needed. +## Object initializers -You can also use the `required` modifier on a property and allow callers to use an *object initializer* to set the initial value of the property: +*Object initializers* let you set properties when you create an object, without writing a constructor for every combination of values: -:::code source="./snippets/classes/Program.cs" id="RequiredProperties"::: +:::code language="csharp" source="snippets/classes/Program.cs" ID="ObjectInitializer"::: -The addition of the `required` keyword mandates that callers must set those properties as part of a `new` expression: +:::code language="csharp" source="snippets/classes/Program.cs" ID="UsingObjectInitializer"::: -```csharp -var p1 = new Person(); // Error! Required properties not set -var p2 = new Person() { FirstName = "Grace", LastName = "Hopper" }; -``` +Object initializers work with any accessible settable property. They combine naturally with `required` properties and with constructors that accept some parameters while letting the caller set others. -## Class inheritance +## Collection initializers -Classes fully support *inheritance*, a fundamental characteristic of object-oriented programming. When you create a class, you can inherit from any other class that isn't defined as [`sealed`](../../language-reference/keywords/sealed.md). Other classes can inherit from your class and override class virtual methods. Furthermore, you can implement one or more interfaces. +*Collection initializers* let you populate a collection inline when you create it. You can use the traditional brace syntax or, starting with C# 12, *collection expressions* with bracket syntax: -Inheritance is accomplished by using a *derivation*, which means a class is declared by using a *base class* from which it inherits data and behavior. A base class is specified by appending a colon and the name of the base class following the derived class name, like this: +:::code language="csharp" source="snippets/classes/Program.cs" ID="CollectionInitializers"::: -:::code source="./snippets/classes/Program.cs" id="DerivedClass"::: +Collection expressions work with arrays, `List`, `Span`, and any type that supports collection initialization. The spread operator (`..`) lets you compose collections from existing sequences. For more information, see [Collection expressions (C# reference)](../../language-reference/operators/collection-expressions.md). -When a class declaration includes a base class, it inherits all the members of the base class except the constructors. For more information, see [Inheritance](../object-oriented/inheritance.md). +## Inheritance -A class in C# can only directly inherit from one base class. However, because a base class can itself inherit from another class, a class might indirectly inherit multiple base classes. Furthermore, a class can directly implement one or more interfaces. For more information, see [Interfaces](interfaces.md). +Classes support *inheritance*—you can define a new class that reuses, extends, or modifies the behavior of an existing class. The class you inherit from is the *base class*, and the new class is the *derived class*: -A class can be declared as [`abstract`](../../language-reference/keywords/abstract.md). An abstract class contains abstract methods that have a signature definition but no implementation. Abstract classes can't be instantiated. They can only be used through derived classes that implement the abstract methods. By contrast, a [sealed](../../language-reference/keywords/sealed.md) class doesn't allow other classes to derive from it. For more information, see [Abstract and Sealed Classes and Class Members](../../programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members.md). +:::code language="csharp" source="snippets/classes/Program.cs" ID="Inheritance"::: -Class definitions can be split between different source files. For more information, see [Partial Classes and Methods](../../programming-guide/classes-and-structs/partial-classes-and-methods.md). +A class can inherit from one base class and implement multiple interfaces. Derived classes inherit all members of the base class except constructors. For more information, see [Inheritance](../object-oriented/inheritance.md) and [Interfaces](interfaces.md). -## C# Language Specification +## See also -[!INCLUDE[CSharplangspec](~/includes/csharplangspec-md.md)] +- [Type system overview](index.md) +- [Structs](structs.md) +- [Records](records.md) +- [Interfaces](interfaces.md) +- [Inheritance](../object-oriented/inheritance.md) diff --git a/docs/csharp/fundamentals/types/snippets/classes/Containers.cs b/docs/csharp/fundamentals/types/snippets/classes/Containers.cs index f9c41842709c2..480c9176baae9 100644 --- a/docs/csharp/fundamentals/types/snippets/classes/Containers.cs +++ b/docs/csharp/fundamentals/types/snippets/classes/Containers.cs @@ -1,15 +1,16 @@ -namespace Version1 +#pragma warning disable CS0414 // Field is assigned but never read (snippets demonstrate declaration patterns) + +namespace FieldInit { // public class Container { - // Initialize capacity field to a default value of 10: private int _capacity = 10; } // } -namespace Version2 +namespace ConstructorInit { // public class Container @@ -21,7 +22,7 @@ public class Container // } -namespace Version3 +namespace PrimaryInit { // public class Container(int capacity) diff --git a/docs/csharp/fundamentals/types/snippets/classes/Program.cs b/docs/csharp/fundamentals/types/snippets/classes/Program.cs index c56fe653fc561..fc617fb21891f 100644 --- a/docs/csharp/fundamentals/types/snippets/classes/Program.cs +++ b/docs/csharp/fundamentals/types/snippets/classes/Program.cs @@ -1,44 +1,102 @@ -// -Customer object1 = new(); -// +// --- Top-level statements --- -// -Customer object2; -// +// +var customer = new Customer("Allison"); +Console.WriteLine(customer.Name); // Allison +// +// +var c1 = new Customer("Grace"); +var c2 = c1; // both variables reference the same object -// -Customer object3 = new(); -Customer object4 = object3; -// +c2.Name = "Hopper"; +Console.WriteLine(c1.Name); // Hopper — c1 sees the change made through c2 +// -/* -var p1 = new Person(); // Error! Required properties not set -*/ -var p2 = new Person { FirstName = "Grace", LastName = "Hopper" }; +// +// var missing = new Person(); // Error: required properties not set +var person = new Person { FirstName = "Grace", LastName = "Hopper" }; +Console.WriteLine($"{person.FirstName} {person.LastName}"); // Grace Hopper +// + +// +double circumference = MathHelpers.CircleCircumference(5.0); +Console.WriteLine($"Circumference: {circumference:F2}"); // Circumference: 31.42 +// + +// +var options = new ConnectionOptions +{ + Host = "db.example.com", + Port = 5432, + UseSsl = true +}; +Console.WriteLine($"{options.Host}:{options.Port} (SSL: {options.UseSsl})"); +// db.example.com:5432 (SSL: True) +// + +// +// Traditional collection initializer: +List languages = ["C#", "F#", "Visual Basic"]; + +// Collection expression with spread: +List moreLangs = [.. languages, "Python", "TypeScript"]; +Console.WriteLine(string.Join(", ", moreLangs)); +// C#, F#, Visual Basic, Python, TypeScript +// + +// +var manager = new Manager("Satya", "Engineering"); +Console.WriteLine($"{manager.Name} manages {manager.Department}"); +// Satya manages Engineering +// + +// --- Type declarations --- // -//[access modifier] - [class] - [identifier] public class Customer { - // Fields, properties, methods and events go here... + public string Name { get; set; } + + public Customer(string name) => Name = name; } // // public class Person { - public required string LastName { get; set; } public required string FirstName { get; set; } + public required string LastName { get; set; } } // -public class Employee {} +// +static class MathHelpers +{ + public static double CircleCircumference(double radius) => + 2 * Math.PI * radius; +} +// -// -public class Manager : Employee +// +class ConnectionOptions { - // Employee fields, properties, methods and events are inherited - // New Manager fields, properties, methods and events go here... + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 80; + public bool UseSsl { get; set; } } -// \ No newline at end of file +// + +class Employee +{ + public string Name { get; set; } + public Employee(string name) => Name = name; +} + +class Manager : Employee +{ + public string Department { get; set; } + + public Manager(string name, string department) : base(name) => + Department = department; +} \ No newline at end of file diff --git a/docs/csharp/fundamentals/types/snippets/classes/classes.csproj b/docs/csharp/fundamentals/types/snippets/classes/classes.csproj index 2150e3797ba5e..ed9781c223ab9 100644 --- a/docs/csharp/fundamentals/types/snippets/classes/classes.csproj +++ b/docs/csharp/fundamentals/types/snippets/classes/classes.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable From 1e614bd549f0f55ed401cb32151dabd5c8d4a69b Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 25 Mar 2026 14:50:57 -0400 Subject: [PATCH 3/8] Revise record types article. --- docs/csharp/fundamentals/types/records.md | 109 +++++++++++------- .../types/snippets/records/EqualityTest.cs | 19 +-- .../types/snippets/records/FirstRecord.cs | 36 +++++- .../types/snippets/records/ImmutableRecord.cs | 32 ++--- .../types/snippets/records/Program.cs | 5 +- .../types/snippets/records/RecordStruct.cs | 35 ++++++ .../types/snippets/records/Records.csproj | 2 +- docs/csharp/toc.yml | 1 - 8 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index efb6c150df4fe..f6190508a2e8f 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -1,66 +1,97 @@ --- -title: "Record types" -description: Learn about C# record types and how to create them. A record is a class that provides value semantics. -ms.date: 08/15/2025 -helpviewer_keywords: - - "records [C#]" - - "C# language, records" +title: "C# record types" +description: Learn how to define and use record types in C#, including value equality, immutability, with expressions, record structs, and positional syntax. +ms.date: 03/25/2026 +ms.topic: concept-article +ai-usage: ai-assisted --- -# Introduction to record types in C\# +# C# record types -A [record](../../language-reference/builtin-types/record.md) in C# is a [class](../../language-reference/keywords/class.md) or [struct](../../language-reference/builtin-types/struct.md) that provides special syntax and behavior for working with data models. The `record` modifier instructs the compiler to synthesize members that are useful for types whose primary role is storing data. These members include an overload of and members that support value equality. +> [!TIP] +> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter records once you need concise data types with built-in equality. +> +> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala—they're types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [record structs](#record-structs) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. -## When to use records +A *record* is a class or struct that the compiler enhances with members useful for data-centric types. When you add the `record` modifier, the compiler generates value equality, a formatted `ToString`, and nondestructive mutation through `with` expressions. Use records when a type's primary role is storing data and two instances with the same values should be considered equal. + +## Declare a record + +Declare a record with the `record` keyword. The simplest form uses *positional parameters* that define both the constructor and the properties in a single line: + +:::code language="csharp" source="snippets/records/FirstRecord.cs" ID="DeclareRecord"::: + +The compiler generates a `FirstName` property and a `LastName` property from the positional parameters. For a `record class`, the properties are `init`-only (immutable after construction). You can also write records with standard property syntax when you need more control: + +:::code language="csharp" source="snippets/records/FirstRecord.cs" ID="RecordWithBody"::: + +Create and use record instances the same way you create any object: + +:::code language="csharp" source="snippets/records/FirstRecord.cs" ID="UsingRecord"::: -Consider using a record in place of a class or struct in the following scenarios: +## Value equality -* You want to define a data model that depends on [value equality](../../programming-guide/statements-expressions-operators/equality-comparisons.md#value-equality). -* You want to define a type for which objects are immutable. +For classes, `==` checks whether two variables point to the same object (*reference equality*). Records change that behavior: `==` checks whether the types match and all property values are equal (*value equality*). The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you: -### Value equality +:::code language="csharp" source="snippets/records/EqualityTest.cs" ID="EqualityTest"::: -For records, value equality means that two variables of a record type are equal if the types match and all property and field values compare equal. For other reference types such as classes, equality means [reference equality](../../programming-guide/statements-expressions-operators/equality-comparisons.md#reference-equality) by default, unless [value equality](../../programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type.md) was implemented. That is, two variables of a class type are equal if they refer to the same object. Methods and operators that determine equality of two record instances use value equality. +The two `Person` instances are different objects, but they're equal because all their property values match. Note that array properties compare by reference, not by contents—mutating the shared array is visible through both records because the array itself isn't copied. -Not all data models work well with value equality. For example, [Entity Framework Core](/ef/core/) depends on reference equality to ensure that it uses only one instance of an entity type for what is conceptually one entity. For this reason, record types aren't appropriate for use as entity types in Entity Framework Core. +## Nondestructive mutation with `with` expressions -### Immutability +Records are often immutable, so you can't change a property after creation. A `with` expression creates a copy with one or more properties changed, leaving the original intact: -An immutable type is one that prevents you from changing any property or field values of an object after it's instantiated. Immutability can be useful when you need a type to be thread-safe or you're depending on a hash code remaining the same in a hash table. Records provide concise syntax for creating and working with immutable types. +:::code language="csharp" source="snippets/records/ImmutableRecord.cs" ID="WithExpression"::: -Immutability isn't appropriate for all data scenarios. [Entity Framework Core](/ef/core/), for example, doesn't support updating with immutable entity types. +A `with` expression copies the existing instance, then applies the specified property changes. Computed properties should be calculated on access rather than stored, so they reflect the correct values in a copied instance. -## How records differ from classes and structs +## Record structs -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: +A `record struct` is a value type with the same compiler-generated members as a record class—equality, `ToString`, and `Deconstruct`. The key differences are: -* 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 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. -* A record can [inherit from another record](../../language-reference/builtin-types/record.md#inheritance). A record can't inherit from a class, and a class can't inherit from a record. +- A `record struct` is a value type. Assignment copies the data, not a reference. +- Positional properties in a `record struct` are read-write by default (not `init`-only like in a `record class`). +- Add `readonly` to make a `record struct` immutable: `readonly record struct`. -Record structs differ from structs in that the compiler synthesizes the methods for equality, and `ToString`. The compiler synthesizes a `Deconstruct` method for positional record structs. +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructDecl"::: -The compiler synthesizes a public init-only property for each primary constructor parameter in a `record class`. In a `record struct`, the compiler synthesizes a public read-write property. The compiler doesn't create properties for primary constructor parameters in `class` and `struct` types that don't include `record` modifier. +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="UsingRecordStruct"::: -## Examples +Record structs support `with` expressions, just like record classes: -The following example defines a public record that uses positional parameters to declare and instantiate a record. It then prints the type name and property values: +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructWith"::: -:::code language="csharp" source="./snippets/records/FirstRecord.cs" id="FirstRecord"::: +For more on value type semantics and when to choose a struct over a class, see [Structs](structs.md). -The following example demonstrates value equality in records: +## Positional records and deconstruction -:::code language="csharp" source="./snippets/records/EqualityTest.cs" id="EqualityTest"::: +Positional records generate a `Deconstruct` method that lets you extract property values into individual variables: + +:::code language="csharp" source="snippets/records/FirstRecord.cs" ID="Deconstruct"::: + +Deconstruction works with both `record class` and `record struct` types. You can use it in assignments, `foreach` loops, and pattern matching. + +## Record inheritance + +A `record class` can inherit from another `record class`. A record can't inherit from a regular class, and a class can't inherit from a record: + +:::code language="csharp" source="snippets/records/FirstRecord.cs" ID="RecordInheritance"::: + +Value equality checks include the run-time type, so a `Person` and a `Student` with the same `FirstName` and `LastName` aren't considered equal. Record structs don't support inheritance because structs can't inherit from other types. + +## When to use records -The following example demonstrates use of a `with` expression to copy an immutable object and change one of the properties: +Use a record when: -:::code language="csharp" source="./snippets/records/ImmutableRecord.cs" id="ImmutableRecord"::: +- The type's primary role is storing data. +- Two instances with the same values should be equal. +- You want immutability (especially for `record class` types). +- You want a readable `ToString` without writing one manually. -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. +Avoid records for entity types in [Entity Framework Core](/ef/core/), which depends on reference equality to track entities. For a broader comparison of type options, see [Choose which kind of type](index.md#choose-which-kind-of-type). -For more information, see [Records (C# reference)](../../language-reference/builtin-types/record.md). - -## C# Language Specification +## See also -[!INCLUDE[CSharplangspec](~/includes/csharplangspec-md.md)] +- [Type system overview](index.md) +- [Classes](classes.md) +- [Structs](structs.md) +- [Records (C# reference)](../../language-reference/builtin-types/record.md) diff --git a/docs/csharp/fundamentals/types/snippets/records/EqualityTest.cs b/docs/csharp/fundamentals/types/snippets/records/EqualityTest.cs index 93ec09057a239..536afc49b9e02 100644 --- a/docs/csharp/fundamentals/types/snippets/records/EqualityTest.cs +++ b/docs/csharp/fundamentals/types/snippets/records/EqualityTest.cs @@ -1,20 +1,21 @@ namespace EqualityTest; -// public record Person(string FirstName, string LastName, string[] PhoneNumbers); + public static class Program { public static void Main() { - var phoneNumbers = new string[2]; - Person person1 = new("Nancy", "Davolio", phoneNumbers); - Person person2 = new("Nancy", "Davolio", phoneNumbers); - Console.WriteLine(person1 == person2); // output: True + // + var phones = new string[] { "555-1234" }; + var person1 = new Person("Grace", "Hopper", phones); + var person2 = new Person("Grace", "Hopper", phones); - person1.PhoneNumbers[0] = "555-1234"; - Console.WriteLine(person1 == person2); // output: True + Console.WriteLine(person1 == person2); // True + Console.WriteLine(ReferenceEquals(person1, person2)); // False - Console.WriteLine(ReferenceEquals(person1, person2)); // output: False + person1.PhoneNumbers[0] = "555-9999"; + Console.WriteLine(person2.PhoneNumbers[0]); // 555-9999 — same array + // } } -// diff --git a/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs b/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs index 56156c626ae62..3b4e46fc42845 100644 --- a/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs +++ b/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs @@ -1,17 +1,41 @@ namespace FirstRecord; -// - +// public record Person(string FirstName, string LastName); +// + +// +public record Product +{ + public required string Name { get; init; } + public required decimal Price { get; init; } +} +// + +// +public record Student(string FirstName, string LastName, int GradeLevel) + : Person(FirstName, LastName); +// public static class Program { public static void Main() { - Person person = new("Nancy", "Davolio"); + // + var person = new Person("Grace", "Hopper"); Console.WriteLine(person); - // output: Person { FirstName = Nancy, LastName = Davolio } - } + // Person { FirstName = Grace, LastName = Hopper } + // + // + var (first, last) = person; + Console.WriteLine($"{first} {last}"); + // Grace Hopper + // + + var student = new Student("Grace", "Hopper", 12); + Console.WriteLine(student); + // Student { FirstName = Grace, LastName = Hopper, GradeLevel = 12 } + Console.WriteLine(person == student); // False — different runtime types + } } -// diff --git a/docs/csharp/fundamentals/types/snippets/records/ImmutableRecord.cs b/docs/csharp/fundamentals/types/snippets/records/ImmutableRecord.cs index 3b023a4c22230..8e3c526160667 100644 --- a/docs/csharp/fundamentals/types/snippets/records/ImmutableRecord.cs +++ b/docs/csharp/fundamentals/types/snippets/records/ImmutableRecord.cs @@ -1,31 +1,21 @@ namespace ImmutableRecord; -// -public record Person(string FirstName, string LastName) -{ - public required string[] PhoneNumbers { get; init; } -} +public record Person(string FirstName, string LastName); public class Program { public static void Main() { - Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] }; - Console.WriteLine(person1); - // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] } - - Person person2 = person1 with { FirstName = "John" }; - Console.WriteLine(person2); - // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] } - Console.WriteLine(person1 == person2); // output: False + // + var original = new Person("Grace", "Hopper"); + var modified = original with { FirstName = "Margaret" }; - person2 = person1 with { PhoneNumbers = new string[1] }; - Console.WriteLine(person2); - // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] } - Console.WriteLine(person1 == person2); // output: False + Console.WriteLine(original); // Person { FirstName = Grace, LastName = Hopper } + Console.WriteLine(modified); // Person { FirstName = Margaret, LastName = Hopper } + Console.WriteLine(original == modified); // False - person2 = person1 with { }; - Console.WriteLine(person1 == person2); // output: True + var copy = original with { }; + Console.WriteLine(original == copy); // True + // } -} -// \ No newline at end of file +} \ No newline at end of file diff --git a/docs/csharp/fundamentals/types/snippets/records/Program.cs b/docs/csharp/fundamentals/types/snippets/records/Program.cs index 9b3ad9f2b0413..bd1323dcd0cda 100644 --- a/docs/csharp/fundamentals/types/snippets/records/Program.cs +++ b/docs/csharp/fundamentals/types/snippets/records/Program.cs @@ -1,11 +1,10 @@ - -public static class Program +public static class Program { public static void Main() { FirstRecord.Program.Main(); EqualityTest.Program.Main(); ImmutableRecord.Program.Main(); + RecordStructDemo.Program.Main(); } - } \ No newline at end of file diff --git a/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs b/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs new file mode 100644 index 0000000000000..cfa9e7fecd625 --- /dev/null +++ b/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs @@ -0,0 +1,35 @@ +namespace RecordStructDemo; + +// +public record struct Coordinate(double Latitude, double Longitude); + +public readonly record struct Temperature(double Celsius) +{ + public double Fahrenheit => Celsius * 9.0 / 5.0 + 32.0; +} +// + +public static class Program +{ + public static void Main() + { + // + var home = new Coordinate(47.6062, -122.3321); + var copy = home; + + Console.WriteLine(home); // Coordinate { Latitude = 47.6062, Longitude = -122.3321 } + Console.WriteLine(home == copy); // True — value equality + // + + // + var shifted = home with { Longitude = -122.0 }; + Console.WriteLine(shifted); // Coordinate { Latitude = 47.6062, Longitude = -122 } + Console.WriteLine(home == shifted); // False + // + + var temp = new Temperature(100); + Console.WriteLine(temp); // Temperature { Celsius = 100 } + Console.WriteLine($"{temp.Celsius}°C = {temp.Fahrenheit}°F"); + // 100°C = 212°F + } +} diff --git a/docs/csharp/fundamentals/types/snippets/records/Records.csproj b/docs/csharp/fundamentals/types/snippets/records/Records.csproj index a150dcce740be..d384ae94f0cbd 100644 --- a/docs/csharp/fundamentals/types/snippets/records/Records.csproj +++ b/docs/csharp/fundamentals/types/snippets/records/Records.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable Program diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 9295be1d79d56..a70973541b072 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -56,7 +56,6 @@ items: # TODO: tuples - name: Classes href: fundamentals/types/classes.md - # TODO: Edit Records article so it doesn't depend on classes. - name: Records href: fundamentals/types/records.md - name: Structs From 1f490e5b3c8257789690673389f7163f7c839b55 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 25 Mar 2026 17:25:22 -0400 Subject: [PATCH 4/8] First review pass --- docs/csharp/fundamentals/types/classes.md | 28 ++++++++------- docs/csharp/fundamentals/types/records.md | 18 ++++++---- .../types/snippets/classes/Program.cs | 3 +- .../types/snippets/records/FirstRecord.cs | 2 +- .../types/snippets/records/RecordStruct.cs | 17 +++++++++ .../types/snippets/structs/Program.cs | 35 ++++++------------- docs/csharp/fundamentals/types/structs.md | 32 ++++------------- 7 files changed, 64 insertions(+), 71 deletions(-) diff --git a/docs/csharp/fundamentals/types/classes.md b/docs/csharp/fundamentals/types/classes.md index 68720e7381220..bdcad30e539f3 100644 --- a/docs/csharp/fundamentals/types/classes.md +++ b/docs/csharp/fundamentals/types/classes.md @@ -12,11 +12,11 @@ ai-usage: ai-assisted > > **Experienced in another language?** C# classes are similar to classes in Java or C++. Skim the [object initializers](#object-initializers) and [collection initializers](#collection-initializers) sections for C#-specific patterns, and see [Records](records.md) for a data-focused alternative. -A *class* is a reference type that defines a blueprint for objects. When you create a variable of a class type, the variable holds a *reference* to an object on the managed heap—not the object data itself. Assigning a class variable to another variable copies the reference, so both variables point to the same object. Classes are the most common way to define custom types in C#. Use them when you need complex behavior, inheritance, or shared identity between references. +A *class* is a reference type that defines a blueprint for objects. When you create a variable of a class type, the variable holds a *reference* to an object on the managed heap - not the object data itself. Assigning a class variable to another variable copies the reference, so both variables point to the same object. Classes are the most common way to define custom types in C#. Use them when you need complex behavior, inheritance, or shared identity between references. ## Declare a class -Define a class with the `class` keyword followed by the type name. An optional access modifier controls visibility—the default is `internal`: +Define a class with the `class` keyword followed by the type name. An optional access modifier controls visibility. The default is `internal`: :::code language="csharp" source="snippets/classes/Program.cs" ID="ClassDeclaration"::: @@ -28,7 +28,7 @@ A class defines a type, but it isn't an object itself. You create an object (an :::code language="csharp" source="snippets/classes/Program.cs" ID="CreateObject"::: -The variable `customer` holds a reference to the object, not the object itself. You can assign multiple variables to the same object—changes through one reference are visible through the other: +The variable `customer` holds a reference to the object, not the object itself. You can assign multiple variables to the same object. Changes through one reference are visible through the other: :::code language="csharp" source="snippets/classes/Program.cs" ID="ReferenceSemantics"::: @@ -38,19 +38,23 @@ This reference-sharing behavior is what distinguishes classes from [structs](str When you create an instance, you want its fields and properties initialized to useful values. C# offers several approaches: -**Field initializers** set a default value directly on the field declaration: +**[Field initializers](../../programming-guide/classes-and-structs/instance-constructors.md)** set a default value directly on the field declaration: :::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerFieldInitializer"::: +Field initializers define *internal* defaults. They don't give callers any way to choose the initial value. To let consumers of the class supply a value, use one of the following techniques. + **Constructor parameters** require callers to provide values: :::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerConstructor"::: -**Primary constructors** (C# 12) add parameters directly to the class declaration. Those parameters are available throughout the class body: +**[Primary constructors](../../whats-new/tutorials/primary-constructors.md)** (C# 12) add parameters directly to the class declaration. Those parameters are available throughout the class body: :::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerPrimaryConstructor"::: -**Required properties** enforce that callers set specific properties through an object initializer: +Notice how primary constructors and field initializers work together: the field initializer `_capacity = capacity` uses the primary-constructor parameter as its value. This pattern lets you capture constructor arguments in fields with a single, concise declaration. + +**[Required properties](../../language-reference/keywords/required.md)** enforce that callers set specific properties through an object initializer: :::code language="csharp" source="snippets/classes/Program.cs" ID="RequiredProperties"::: @@ -60,27 +64,27 @@ For a deeper look at constructor patterns, including parameter validation and co ## Static classes -A `static` class can't be instantiated and contains only static members. Use static classes as containers for utility methods that don't operate on instance data: +A `static` class can't be instantiated and contains only static members. Use static classes to organize utility methods that don't operate on instance data: :::code language="csharp" source="snippets/classes/Program.cs" ID="StaticClass"::: :::code language="csharp" source="snippets/classes/Program.cs" ID="UsingStaticClass"::: -The .NET class library includes many static classes, such as and . A static class is sealed implicitly—you can't derive from it or instantiate it. +The .NET class library includes many static classes, such as and . A static class is implicitly sealed. You can't derive from it or instantiate it. ## Object initializers -*Object initializers* let you set properties when you create an object, without writing a constructor for every combination of values: +*[Object initializers](../../programming-guide/classes-and-structs/object-and-collection-initializers.md)* let you set properties when you create an object, without writing a constructor for every combination of values: :::code language="csharp" source="snippets/classes/Program.cs" ID="ObjectInitializer"::: :::code language="csharp" source="snippets/classes/Program.cs" ID="UsingObjectInitializer"::: -Object initializers work with any accessible settable property. They combine naturally with `required` properties and with constructors that accept some parameters while letting the caller set others. +Object initializers work with any accessible property that has a `set` or [`init`](../../language-reference/keywords/init.md) accessor. They combine naturally with `required` properties and with constructors that accept some parameters while letting the caller set others. ## Collection initializers -*Collection initializers* let you populate a collection inline when you create it. You can use the traditional brace syntax or, starting with C# 12, *collection expressions* with bracket syntax: +*Collection expressions* (C# 12) let you populate a collection inline when you create it using bracket syntax: :::code language="csharp" source="snippets/classes/Program.cs" ID="CollectionInitializers"::: @@ -88,7 +92,7 @@ Collection expressions work with arrays, `List`, `Span`, and any type that ## Inheritance -Classes support *inheritance*—you can define a new class that reuses, extends, or modifies the behavior of an existing class. The class you inherit from is the *base class*, and the new class is the *derived class*: +Classes support *inheritance*. You can define a new class that reuses, extends, or modifies the behavior of an existing class. The class you inherit from is the *base class*, and the new class is the *derived class*: :::code language="csharp" source="snippets/classes/Program.cs" ID="Inheritance"::: diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index f6190508a2e8f..ca8d9d4712d30 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -10,17 +10,17 @@ ai-usage: ai-assisted > [!TIP] > **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter records once you need concise data types with built-in equality. > -> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala—they're types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [record structs](#record-structs) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. +> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala. They're types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [record structs](#record-structs) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. -A *record* is a class or struct that the compiler enhances with members useful for data-centric types. When you add the `record` modifier, the compiler generates value equality, a formatted `ToString`, and nondestructive mutation through `with` expressions. Use records when a type's primary role is storing data and two instances with the same values should be considered equal. +A *record* is a class or struct that the compiler enhances with members useful for data-centric types. When you add the [`record`](../../language-reference/builtin-types/record.md) modifier, the compiler generates value equality, a formatted `ToString`, and nondestructive mutation through [`with` expressions](../../language-reference/operators/with-expression.md). Use records when a type's primary role is storing data and two instances with the same values should be considered equal. ## Declare a record -Declare a record with the `record` keyword. The simplest form uses *positional parameters* that define both the constructor and the properties in a single line: +Declare a record with the `record` keyword. Writing `record` alone is shorthand for [`record class`](../../language-reference/builtin-types/record.md). The type is a reference type. (For value-type records, write [`record struct`](../../language-reference/builtin-types/record.md) explicitly; see [Record structs](#record-structs).) The simplest form uses *positional parameters* that define both the constructor and the properties in a single line: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="DeclareRecord"::: -The compiler generates a `FirstName` property and a `LastName` property from the positional parameters. For a `record class`, the properties are `init`-only (immutable after construction). You can also write records with standard property syntax when you need more control: +The compiler generates a `FirstName` property and a `LastName` property from the positional parameters. For a `record class`, the properties are `init`-only (immutable after construction). You can also write records with standard property syntax when you need more control. For example, to make a property read/write instead of `init`-only: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="RecordWithBody"::: @@ -30,7 +30,7 @@ Create and use record instances the same way you create any object: ## Value equality -For classes, `==` checks whether two variables point to the same object (*reference equality*). Records change that behavior: `==` checks whether the types match and all property values are equal (*value equality*). The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you: +Records use *value equality*: `==` checks whether the types match and all property values are equal. The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you, so you don't write any of that boilerplate. In contrast, classes use *reference equality* by default—`==` checks whether two variables point to the same object. Regular structs support value equality through `ValueType.Equals`, but that default implementation can be slower. Record structs get a compiler-generated, reflection-free equality check that's more efficient: :::code language="csharp" source="snippets/records/EqualityTest.cs" ID="EqualityTest"::: @@ -42,16 +42,20 @@ Records are often immutable, so you can't change a property after creation. A `w :::code language="csharp" source="snippets/records/ImmutableRecord.cs" ID="WithExpression"::: -A `with` expression copies the existing instance, then applies the specified property changes. Computed properties should be calculated on access rather than stored, so they reflect the correct values in a copied instance. +A `with` expression copies the existing instance, then applies the specified property changes. ## Record structs -A `record struct` is a value type with the same compiler-generated members as a record class—equality, `ToString`, and `Deconstruct`. The key differences are: +A `record struct` is a value type with the same compiler-generated members as a record class: equality, `ToString`, and `Deconstruct`. The key differences are: - A `record struct` is a value type. Assignment copies the data, not a reference. - Positional properties in a `record struct` are read-write by default (not `init`-only like in a `record class`). - Add `readonly` to make a `record struct` immutable: `readonly record struct`. +The following example shows the difference. Assigning a record class copies the reference. Both variables point to the same object. Assigning a record struct copies the data, so changes to one variable don't affect the other: + +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordClassVsStruct"::: + :::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructDecl"::: :::code language="csharp" source="snippets/records/RecordStruct.cs" ID="UsingRecordStruct"::: diff --git a/docs/csharp/fundamentals/types/snippets/classes/Program.cs b/docs/csharp/fundamentals/types/snippets/classes/Program.cs index fc617fb21891f..726cf02cb8f7d 100644 --- a/docs/csharp/fundamentals/types/snippets/classes/Program.cs +++ b/docs/csharp/fundamentals/types/snippets/classes/Program.cs @@ -36,10 +36,9 @@ // // -// Traditional collection initializer: List languages = ["C#", "F#", "Visual Basic"]; -// Collection expression with spread: +// The spread operator (..) composes collections from existing sequences: List moreLangs = [.. languages, "Python", "TypeScript"]; Console.WriteLine(string.Join(", ", moreLangs)); // C#, F#, Visual Basic, Python, TypeScript diff --git a/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs b/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs index 3b4e46fc42845..b3afd1cab985a 100644 --- a/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs +++ b/docs/csharp/fundamentals/types/snippets/records/FirstRecord.cs @@ -8,7 +8,7 @@ public record Person(string FirstName, string LastName); public record Product { public required string Name { get; init; } - public required decimal Price { get; init; } + public decimal Price { get; set; } } // diff --git a/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs b/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs index cfa9e7fecd625..4df3de5f01865 100644 --- a/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs +++ b/docs/csharp/fundamentals/types/snippets/records/RecordStruct.cs @@ -1,3 +1,5 @@ +using FirstRecord; + namespace RecordStructDemo; // @@ -13,6 +15,21 @@ public static class Program { public static void Main() { + // + // Record class — assignment copies the reference + var p1 = new Person("Grace", "Hopper"); + var p2 = p1; + // p1 and p2 point to the same object: + Console.WriteLine(ReferenceEquals(p1, p2)); // True + + // Record struct — assignment copies the data + var c1 = new Coordinate(47.6062, -122.3321); + var c2 = c1; + c2.Longitude = 0.0; // mutating c2 doesn't affect c1 + Console.WriteLine(c1.Longitude); // -122.3321 + Console.WriteLine(c2.Longitude); // 0 + // + // var home = new Coordinate(47.6062, -122.3321); var copy = home; diff --git a/docs/csharp/fundamentals/types/snippets/structs/Program.cs b/docs/csharp/fundamentals/types/snippets/structs/Program.cs index ab223d74f5677..fd7e50f26e9d5 100644 --- a/docs/csharp/fundamentals/types/snippets/structs/Program.cs +++ b/docs/csharp/fundamentals/types/snippets/structs/Program.cs @@ -39,24 +39,6 @@ Console.WriteLine(v.Speed); // 7.211... // -// -var home = new Coordinate(47.6062, -122.3321); -var copy = home; - -Console.WriteLine(home); // Coordinate { Latitude = 47.6062, Longitude = -122.3321 } -Console.WriteLine(home == copy); // True — value equality - -var shifted = home with { Longitude = -122.0 }; -Console.WriteLine(shifted); // Coordinate { Latitude = 47.6062, Longitude = -122 } -Console.WriteLine(home == shifted); // False -// - -// -var (lat, lon) = home; -Console.WriteLine($"Lat: {lat}, Lon: {lon}"); -// Lat: 47.6062, Lon: -122.3321 -// - // --- Type declarations --- // @@ -124,15 +106,20 @@ readonly struct Temperature // struct Velocity { - public double X { get; set; } - public double Y { get; set; } + public double X + { + readonly get; + set; + } + + public double Y + { + readonly get; + set; + } public readonly double Speed => Math.Sqrt(X * X + Y * Y); public readonly override string ToString() => $"({X}, {Y}) speed={Speed:F2}"; } // - -// -record struct Coordinate(double Latitude, double Longitude); -// diff --git a/docs/csharp/fundamentals/types/structs.md b/docs/csharp/fundamentals/types/structs.md index 7e0d3ab5f4625..c5fdb2b949f9f 100644 --- a/docs/csharp/fundamentals/types/structs.md +++ b/docs/csharp/fundamentals/types/structs.md @@ -10,9 +10,9 @@ ai-usage: ai-assisted > [!TIP] > **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter structs once you need lightweight value types in your code. > -> **Experienced in another language?** C# structs are value types similar to structs in C++ or Swift, but they live on the managed heap when boxed and support interfaces, constructors, and methods. Skim the [readonly structs](#readonly-structs-and-readonly-members) and [record structs](#record-structs) sections for C#-specific patterns. +> **Experienced in another language?** C# structs are value types similar to structs in C++ or Swift, but they live on the managed heap when boxed and support interfaces, constructors, and methods. Skim the [readonly structs](#readonly-structs-and-readonly-members) section for C#-specific patterns. For record structs, see [Records](records.md). -A *struct* is a value type that holds its data directly in the variable, rather than through a reference to an object on the heap. When you assign a struct to a new variable, the runtime copies the entire value. Changes to one variable don't affect the other. Use structs for small, lightweight data where identity isn't important—coordinates, colors, measurements, or configuration settings. +A *struct* is a value type that holds its data directly in the variable, rather than through a reference to an object on the heap. When you assign a struct to a new variable, the runtime copies the entire value. Changes to one variable don't affect the other. Use structs for small, lightweight data where identity isn't important: coordinates, colors, measurements, or configuration settings. ## Declare a struct @@ -20,7 +20,7 @@ Define a struct with the `struct` keyword. A struct can contain fields, properti :::code language="csharp" source="snippets/structs/Program.cs" ID="BasicStruct"::: -The `Point` struct stores two `double` values and provides a method to calculate the distance between two points. The `DistanceTo` method is marked `readonly` because it doesn't modify the struct's state—a pattern covered in [readonly members](#readonly-structs-and-readonly-members). +The `Point` struct stores two `double` values and provides a method to calculate the distance between two points. The `DistanceTo` method is marked `readonly` because it doesn't modify the struct's state. That pattern is covered in [readonly members](#readonly-structs-and-readonly-members). ## Value semantics @@ -32,7 +32,7 @@ This copy-on-assignment behavior differs from [classes](classes.md), where assig ## Struct constructors -You can define constructors in structs the same way you do in classes. Starting with C# 10, structs can have *parameterless constructors* that set custom default values: +You can define constructors in structs the same way you do in classes. Structs can have *parameterless constructors* that set custom default values: :::code language="csharp" source="snippets/structs/Program.cs" ID="ParameterlessConstructor"::: @@ -40,9 +40,7 @@ A parameterless constructor runs when you use `new` with no arguments. The `defa :::code language="csharp" source="snippets/structs/Program.cs" ID="DefaultVsConstructor"::: -## Auto-default structs - -Starting with C# 11, the compiler automatically initializes any fields you don't explicitly set in a constructor. Before C# 11, every field had to be assigned before the constructor returned. Now you can initialize only the fields that need non-default values: +The compiler automatically initializes any fields you don't explicitly set in a constructor. You can initialize only the fields that need non-default values: :::code language="csharp" source="snippets/structs/Program.cs" ID="AutoDefault"::: @@ -58,7 +56,7 @@ A `readonly struct` guarantees that no instance member modifies the struct's sta :::code language="csharp" source="snippets/structs/Program.cs" ID="UsingReadonly"::: -When you don't need the entire struct to be immutable, you can mark individual members as `readonly` instead. A `readonly` member can't modify the struct's state, and the compiler verifies that guarantee: +When you don't need the entire struct to be immutable, mark individual members as `readonly` instead. A `readonly` member can't modify the struct's state, and the compiler verifies that guarantee: :::code language="csharp" source="snippets/structs/Program.cs" ID="ReadonlyMembers"::: @@ -66,28 +64,12 @@ When you don't need the entire struct to be immutable, you can mark individual m Marking members `readonly` helps the compiler optimize defensive copies. When you pass a `readonly` struct to a method that accepts an `in` parameter, the compiler knows no copy is needed. -## Record structs - -A `record struct` combines value type semantics with the compiler-generated members that records provide—value equality, formatted `ToString`, and nondestructive mutation through `with` expressions. Declare a record struct with positional parameters for concise syntax: - -:::code language="csharp" source="snippets/structs/Program.cs" ID="RecordStruct"::: - -The compiler generates properties, `Equals`, `GetHashCode`, `ToString`, and a `Deconstruct` method from the positional parameters: - -:::code language="csharp" source="snippets/structs/Program.cs" ID="UsingRecordStruct"::: - -You can also deconstruct a positional record struct into individual variables: - -:::code language="csharp" source="snippets/structs/Program.cs" ID="RecordStructDeconstruct"::: - -Record structs are mutable by default, unlike `record class` types, which use `init`-only properties. Add the `readonly` modifier (`readonly record struct`) when you want immutability. For a deeper look at records, including record classes, inheritance, and customization, see [Records](records.md). - ## Choose structs or classes Use a struct when your type: - Represents a single value or a small group of related values (roughly 16 bytes or less). -- Has value semantics—two instances with the same data should be equal. +- Has value semantics: two instances with the same data should be equal. - Doesn't need inheritance from a base type (structs can't inherit from other structs or classes, but they can implement interfaces). Use a class when your type has complex behavior, needs inheritance, or when instances represent a shared identity rather than a copied value. For a broader comparison that includes records, tuples, and interfaces, see [Choose which kind of type](index.md#choose-which-kind-of-type). From e5be6dd36d85e6fca185c99b2ce9a8d65b12ef7b Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 26 Mar 2026 11:19:31 -0400 Subject: [PATCH 5/8] interim checkin Reviewed classes.md, and about to embark on a large body of work for records.md --- docs/csharp/fundamentals/types/classes.md | 17 ++++++++++++++--- docs/csharp/fundamentals/types/records.md | 4 ++-- .../types/snippets/classes/Program.cs | 6 +++--- docs/csharp/toc.yml | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/csharp/fundamentals/types/classes.md b/docs/csharp/fundamentals/types/classes.md index bdcad30e539f3..5ee8681a72770 100644 --- a/docs/csharp/fundamentals/types/classes.md +++ b/docs/csharp/fundamentals/types/classes.md @@ -32,7 +32,7 @@ The variable `customer` holds a reference to the object, not the object itself. :::code language="csharp" source="snippets/classes/Program.cs" ID="ReferenceSemantics"::: -This reference-sharing behavior is what distinguishes classes from [structs](structs.md), where assignment copies the data. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). +This reference-sharing behavior is one distinction between classes and [structs](structs.md), where assignment copies the data. More importantly, classes support [inheritance](#inheritance). You can build hierarchies where derived types reuse and specialize behavior from a base class. Structs can't participate in inheritance hierarchies. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). ## Constructors and initialization @@ -52,7 +52,7 @@ Field initializers define *internal* defaults. They don't give callers any way t :::code language="csharp" source="snippets/classes/Containers.cs" ID="ContainerPrimaryConstructor"::: -Notice how primary constructors and field initializers work together: the field initializer `_capacity = capacity` uses the primary-constructor parameter as its value. This pattern lets you capture constructor arguments in fields with a single, concise declaration. +Primary constructors and field initializers can work together: the field initializer `_capacity = capacity` uses the primary-constructor parameter as its value. This pattern lets you capture constructor arguments in fields with a single, concise declaration. **[Required properties](../../language-reference/keywords/required.md)** enforce that callers set specific properties through an object initializer: @@ -84,11 +84,13 @@ Object initializers work with any accessible property that has a `set` or [`init ## Collection initializers +A *collection* is a type that holds a group of related values—lists, sets, dictionaries, arrays, and spans are all common examples. The .NET class library provides general-purpose collection types such as , , and , alongside arrays and . + *Collection expressions* (C# 12) let you populate a collection inline when you create it using bracket syntax: :::code language="csharp" source="snippets/classes/Program.cs" ID="CollectionInitializers"::: -Collection expressions work with arrays, `List`, `Span`, and any type that supports collection initialization. The spread operator (`..`) lets you compose collections from existing sequences. For more information, see [Collection expressions (C# reference)](../../language-reference/operators/collection-expressions.md). +Collection expressions work with arrays, `List`, `Span`, and any type that supports collection initialization. The spread operator (`..`) adds all elements from its operand into the new collection. The operand doesn't have to be a full collection—it can be any expression that produces a sequence, such as a sub-range, a LINQ query, or a filtered subset. For more information, see [Collection expressions (C# reference)](../../language-reference/operators/collection-expressions.md). ## Inheritance @@ -98,6 +100,15 @@ Classes support *inheritance*. You can define a new class that reuses, extends, A class can inherit from one base class and implement multiple interfaces. Derived classes inherit all members of the base class except constructors. For more information, see [Inheritance](../object-oriented/inheritance.md) and [Interfaces](interfaces.md). +## When to use classes + +Use a class when: + +- The type has complex behavior or manages mutable state. +- You need inheritance to create a base class with derived specializations, or to create a derived type that extends an existing class. +- Instances represent a shared identity, not just a bundle of data (two references to the same object should stay in sync). +- The type is large or long-lived and benefits from heap allocation and reference semantics. + ## See also - [Type system overview](index.md) diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index ca8d9d4712d30..8d5d02da3d268 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -30,11 +30,11 @@ Create and use record instances the same way you create any object: ## Value equality -Records use *value equality*: `==` checks whether the types match and all property values are equal. The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you, so you don't write any of that boilerplate. In contrast, classes use *reference equality* by default—`==` checks whether two variables point to the same object. Regular structs support value equality through `ValueType.Equals`, but that default implementation can be slower. Record structs get a compiler-generated, reflection-free equality check that's more efficient: +Records use *value equality*: `==` checks whether the types match and all property values are equal. The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you, so you don't write any of that boilerplate. In contrast, classes use *reference equality* by default. The `==` operator checks whether two variables point to the same object. Regular structs support value equality through `ValueType.Equals`, but that default implementation can be slower. Record structs get a compiler-generated, reflection-free equality check that's more efficient: :::code language="csharp" source="snippets/records/EqualityTest.cs" ID="EqualityTest"::: -The two `Person` instances are different objects, but they're equal because all their property values match. Note that array properties compare by reference, not by contents—mutating the shared array is visible through both records because the array itself isn't copied. +The two `Person` instances are different objects, but they're equal because all their property values match. Array properties compare by reference, not by contents. Mutating the shared array is visible through both records because the array itself isn't copied. ## Nondestructive mutation with `with` expressions diff --git a/docs/csharp/fundamentals/types/snippets/classes/Program.cs b/docs/csharp/fundamentals/types/snippets/classes/Program.cs index 726cf02cb8f7d..25c8de0b4bc62 100644 --- a/docs/csharp/fundamentals/types/snippets/classes/Program.cs +++ b/docs/csharp/fundamentals/types/snippets/classes/Program.cs @@ -80,9 +80,9 @@ public static double CircleCircumference(double radius) => // class ConnectionOptions { - public string Host { get; set; } = "localhost"; - public int Port { get; set; } = 80; - public bool UseSsl { get; set; } + public string Host { get; init; } = "localhost"; + public int Port { get; init; } = 80; + public bool UseSsl { get; init; } } // diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index a70973541b072..96961225c0527 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -56,10 +56,10 @@ items: # TODO: tuples - name: Classes href: fundamentals/types/classes.md - - name: Records - href: fundamentals/types/records.md - name: Structs href: fundamentals/types/structs.md + - name: Records + href: fundamentals/types/records.md - name: Interfaces href: fundamentals/types/interfaces.md - name: Enumerations From 404d674283f6bb2023c1dba807098c895cdb1b27 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 26 Mar 2026 14:50:24 -0400 Subject: [PATCH 6/8] Next set of rework. --- docs/csharp/fundamentals/types/records.md | 61 +++++++++++++---------- docs/csharp/fundamentals/types/structs.md | 13 ++--- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index 8d5d02da3d268..c75ed4dacba79 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -1,6 +1,6 @@ --- title: "C# record types" -description: Learn how to define and use record types in C#, including value equality, immutability, with expressions, record structs, and positional syntax. +description: Learn how the record modifier enhances classes and structs with compiler-generated value equality, ToString, with expressions, and positional syntax. ms.date: 03/25/2026 ms.topic: concept-article ai-usage: ai-assisted @@ -10,17 +10,24 @@ ai-usage: ai-assisted > [!TIP] > **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter records once you need concise data types with built-in equality. > -> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala. They're types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [record structs](#record-structs) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. +> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala—types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [`record class` vs `record struct`](#record-class-vs-record-struct) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. -A *record* is a class or struct that the compiler enhances with members useful for data-centric types. When you add the [`record`](../../language-reference/builtin-types/record.md) modifier, the compiler generates value equality, a formatted `ToString`, and nondestructive mutation through [`with` expressions](../../language-reference/operators/with-expression.md). Use records when a type's primary role is storing data and two instances with the same values should be considered equal. +The [`record`](../../language-reference/builtin-types/record.md) keyword is a modifier you apply to either a `class` or a `struct`. It tells the compiler to generate value equality, a formatted `ToString`, and nondestructive mutation through [`with` expressions](../../language-reference/operators/with-expression.md). The underlying type—class or struct—still determines whether instances use reference or value semantics. The `record` modifier adds data-friendly behavior on top of those semantics. Use records when a type's primary role is storing data and two instances with the same values should be considered equal. ## Declare a record -Declare a record with the `record` keyword. Writing `record` alone is shorthand for [`record class`](../../language-reference/builtin-types/record.md). The type is a reference type. (For value-type records, write [`record struct`](../../language-reference/builtin-types/record.md) explicitly; see [Record structs](#record-structs).) The simplest form uses *positional parameters* that define both the constructor and the properties in a single line: +You can apply `record` to either a class or a struct. The simplest form uses *positional parameters* that define both the constructor and the properties in a single line: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="DeclareRecord"::: -The compiler generates a `FirstName` property and a `LastName` property from the positional parameters. For a `record class`, the properties are `init`-only (immutable after construction). You can also write records with standard property syntax when you need more control. For example, to make a property read/write instead of `init`-only: +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructDecl"::: + +Writing `record` alone is shorthand for [`record class`](../../language-reference/builtin-types/record.md)—a reference type. Writing [`record struct`](../../language-reference/builtin-types/record.md) creates a value type. The compiler generates properties from the positional parameters in both cases, but the defaults differ: + +- **`record class`**: Properties are `init`-only (immutable after construction). +- **`record struct`**: Properties are read-write by default. Add `readonly` (`readonly record struct`) to make them `init`-only. + +You can also write records with standard property syntax when you need more control—for example, to make a property read/write instead of `init`-only: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="RecordWithBody"::: @@ -28,43 +35,40 @@ Create and use record instances the same way you create any object: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="UsingRecord"::: -## Value equality - -Records use *value equality*: `==` checks whether the types match and all property values are equal. The compiler generates `Equals`, `GetHashCode`, and the `==`/`!=` operators for you, so you don't write any of that boilerplate. In contrast, classes use *reference equality* by default. The `==` operator checks whether two variables point to the same object. Regular structs support value equality through `ValueType.Equals`, but that default implementation can be slower. Record structs get a compiler-generated, reflection-free equality check that's more efficient: +## `record class` vs `record struct` -:::code language="csharp" source="snippets/records/EqualityTest.cs" ID="EqualityTest"::: +Because the `record` modifier preserves the underlying type's semantics, a `record class` and a `record struct` behave differently when you assign or compare references. Assigning a record class copies the reference—both variables point to the same object. Assigning a record struct copies the data, so changes to one variable don't affect the other: -The two `Person` instances are different objects, but they're equal because all their property values match. Array properties compare by reference, not by contents. Mutating the shared array is visible through both records because the array itself isn't copied. - -## Nondestructive mutation with `with` expressions +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordClassVsStruct"::: -Records are often immutable, so you can't change a property after creation. A `with` expression creates a copy with one or more properties changed, leaving the original intact: +:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="UsingRecordStruct"::: -:::code language="csharp" source="snippets/records/ImmutableRecord.cs" ID="WithExpression"::: +Choose `record class` when you need inheritance or when instances are large enough that copying would be expensive. Choose `record struct` for small, self-contained data where value-type copy semantics are appropriate. For more on value type semantics, see [Structs](structs.md). -A `with` expression copies the existing instance, then applies the specified property changes. +## Value equality -## Record structs +The `record` modifier gives both classes and structs compiler-generated, property-by-property equality. Here's how equality works across all four type kinds: -A `record struct` is a value type with the same compiler-generated members as a record class: equality, `ToString`, and `Deconstruct`. The key differences are: +- **Plain class**: Uses *reference equality* by default—`==` checks whether two variables point to the same object, not whether the data matches. +- **Plain struct**: Supports value equality through , but the default implementation uses reflection, which is slower and doesn't generate `==`/`!=` operators. +- **`record class`**: The compiler generates `Equals`, `GetHashCode`, and `==`/`!=` that compare every property value. Two distinct objects with the same data are equal. +- **`record struct`**: Same compiler-generated equality as a record class, but for a value type—no reflection, making it faster than plain struct equality. -- A `record struct` is a value type. Assignment copies the data, not a reference. -- Positional properties in a `record struct` are read-write by default (not `init`-only like in a `record class`). -- Add `readonly` to make a `record struct` immutable: `readonly record struct`. +The following example demonstrates record class equality: -The following example shows the difference. Assigning a record class copies the reference. Both variables point to the same object. Assigning a record struct copies the data, so changes to one variable don't affect the other: +:::code language="csharp" source="snippets/records/EqualityTest.cs" ID="EqualityTest"::: -:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordClassVsStruct"::: +The two `Person` instances are different objects, but they're equal because all their property values match. Array properties compare by reference, not by contents. Mutating the shared array is visible through both records because the array itself isn't copied. -:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructDecl"::: +## Nondestructive mutation with `with` expressions -:::code language="csharp" source="snippets/records/RecordStruct.cs" ID="UsingRecordStruct"::: +Records are often immutable, so you can't change a property after creation. A `with` expression creates a copy with one or more properties changed, leaving the original intact. This works for both `record class` and `record struct` types: -Record structs support `with` expressions, just like record classes: +:::code language="csharp" source="snippets/records/ImmutableRecord.cs" ID="WithExpression"::: :::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructWith"::: -For more on value type semantics and when to choose a struct over a class, see [Structs](structs.md). +A `with` expression copies the existing instance, then applies the specified property changes. ## Positional records and deconstruction @@ -91,6 +95,11 @@ Use a record when: - You want immutability (especially for `record class` types). - You want a readable `ToString` without writing one manually. +When choosing between `record class` and `record struct`: + +- Use `record class` when you need inheritance, or when the type is large enough that copying on every assignment would be expensive. +- Use `record struct` for small, self-contained values where copy semantics and stack allocation are beneficial. + Avoid records for entity types in [Entity Framework Core](/ef/core/), which depends on reference equality to track entities. For a broader comparison of type options, see [Choose which kind of type](index.md#choose-which-kind-of-type). ## See also diff --git a/docs/csharp/fundamentals/types/structs.md b/docs/csharp/fundamentals/types/structs.md index c5fdb2b949f9f..a2a60b7f19862 100644 --- a/docs/csharp/fundamentals/types/structs.md +++ b/docs/csharp/fundamentals/types/structs.md @@ -12,7 +12,7 @@ ai-usage: ai-assisted > > **Experienced in another language?** C# structs are value types similar to structs in C++ or Swift, but they live on the managed heap when boxed and support interfaces, constructors, and methods. Skim the [readonly structs](#readonly-structs-and-readonly-members) section for C#-specific patterns. For record structs, see [Records](records.md). -A *struct* is a value type that holds its data directly in the variable, rather than through a reference to an object on the heap. When you assign a struct to a new variable, the runtime copies the entire value. Changes to one variable don't affect the other. Use structs for small, lightweight data where identity isn't important: coordinates, colors, measurements, or configuration settings. +A *struct* is a value type that holds its data directly in the instance, rather than through a reference to an object on the heap. When you assign a struct to a new variable, the runtime copies the entire instance. Changes to one variable don't affect the other because each variable represents a different instance. Use structs for small, lightweight types whose primary role is storing data rather than modeling behavior. Examples include coordinates, colors, measurements, or configuration settings. ## Declare a struct @@ -28,11 +28,11 @@ Structs are *value types*. Assignment copies the data, so each variable holds it :::code language="csharp" source="snippets/structs/Program.cs" ID="ValueSemantics"::: -This copy-on-assignment behavior differs from [classes](classes.md), where assignment copies only the reference. Both variables then point to the same object. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). +Because structs are data containers, assignment copies every data member into a new, independent instance. Each copy is distinct. Modifying one doesn't affect the other. This behavior differs from [classes](classes.md), where assignment copies only the reference and both variables share the same object. For more on the distinction, see [Value types and reference types](index.md#value-types-and-reference-types). ## Struct constructors -You can define constructors in structs the same way you do in classes. Structs can have *parameterless constructors* that set custom default values: +You can define constructors in structs the same way you do in classes. Structs can have *parameterless constructors* that set custom default values. The term "parameterless constructor" distinguishes an instance created with `new` (which runs your constructor logic) from a *default* instance created with the `default` expression (which zero-initializes all fields): :::code language="csharp" source="snippets/structs/Program.cs" ID="ParameterlessConstructor"::: @@ -64,15 +64,16 @@ When you don't need the entire struct to be immutable, mark individual members a Marking members `readonly` helps the compiler optimize defensive copies. When you pass a `readonly` struct to a method that accepts an `in` parameter, the compiler knows no copy is needed. -## Choose structs or classes +## When to use structs Use a struct when your type: - Represents a single value or a small group of related values (roughly 16 bytes or less). -- Has value semantics: two instances with the same data should be equal. +- Has value semantics—two instances with the same data should be equal. +- Is primarily a data container rather than a model of behavior. - Doesn't need inheritance from a base type (structs can't inherit from other structs or classes, but they can implement interfaces). -Use a class when your type has complex behavior, needs inheritance, or when instances represent a shared identity rather than a copied value. For a broader comparison that includes records, tuples, and interfaces, see [Choose which kind of type](index.md#choose-which-kind-of-type). +For a broader comparison that includes classes, records, tuples, and interfaces, see [Choose which kind of type](index.md#choose-which-kind-of-type). ## See also From 3e783448c1636508e0f8625630a48cf35ab34988 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 26 Mar 2026 14:59:24 -0400 Subject: [PATCH 7/8] YAEP: Yet Another Edit pass Reorder and make a number of content changes. --- docs/csharp/fundamentals/types/records.md | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/csharp/fundamentals/types/records.md b/docs/csharp/fundamentals/types/records.md index c75ed4dacba79..59d321779a9b6 100644 --- a/docs/csharp/fundamentals/types/records.md +++ b/docs/csharp/fundamentals/types/records.md @@ -8,11 +8,11 @@ ai-usage: ai-assisted # C# record types > [!TIP] -> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter records once you need concise data types with built-in equality. +> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll encounter records when you need concise data types with built-in equality. > -> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala—types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [`record class` vs `record struct`](#record-class-vs-record-struct) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. +> **Experienced in another language?** C# records are similar to data classes in Kotlin or case classes in Scala. They're types optimized for storing data, with compiler-generated equality, `ToString`, and copy semantics. Skim the [`record class` vs `record struct`](#record-class-vs-record-struct) and [`with` expressions](#nondestructive-mutation-with-with-expressions) sections for C#-specific patterns. -The [`record`](../../language-reference/builtin-types/record.md) keyword is a modifier you apply to either a `class` or a `struct`. It tells the compiler to generate value equality, a formatted `ToString`, and nondestructive mutation through [`with` expressions](../../language-reference/operators/with-expression.md). The underlying type—class or struct—still determines whether instances use reference or value semantics. The `record` modifier adds data-friendly behavior on top of those semantics. Use records when a type's primary role is storing data and two instances with the same values should be considered equal. +The [`record`](../../language-reference/builtin-types/record.md) keyword is a modifier you apply to either a `class` or a `struct`. It tells the compiler to generate value equality, a formatted `ToString`, and nondestructive mutation through [`with` expressions](../../language-reference/operators/with-expression.md). The underlying type, a class or a struct, still determines whether instances use reference or value semantics. The `record` modifier adds data-friendly behavior on top of those semantics. Use records when a type's primary role is storing data and two instances with the same values should be considered equal. ## Declare a record @@ -22,12 +22,12 @@ You can apply `record` to either a class or a struct. The simplest form uses *po :::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordStructDecl"::: -Writing `record` alone is shorthand for [`record class`](../../language-reference/builtin-types/record.md)—a reference type. Writing [`record struct`](../../language-reference/builtin-types/record.md) creates a value type. The compiler generates properties from the positional parameters in both cases, but the defaults differ: +Writing `record` alone is shorthand for [`record class`](../../language-reference/builtin-types/record.md) which is a reference type. Writing [`record struct`](../../language-reference/builtin-types/record.md) creates a value type. The compiler generates properties from the positional parameters in both cases, but the defaults differ: - **`record class`**: Properties are `init`-only (immutable after construction). - **`record struct`**: Properties are read-write by default. Add `readonly` (`readonly record struct`) to make them `init`-only. -You can also write records with standard property syntax when you need more control—for example, to make a property read/write instead of `init`-only: +You can also write records with standard property syntax when you need more control. For example, to make a property read/write instead of `init`-only: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="RecordWithBody"::: @@ -35,9 +35,9 @@ Create and use record instances the same way you create any object: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="UsingRecord"::: -## `record class` vs `record struct` +## `record class` vs. `record struct` -Because the `record` modifier preserves the underlying type's semantics, a `record class` and a `record struct` behave differently when you assign or compare references. Assigning a record class copies the reference—both variables point to the same object. Assigning a record struct copies the data, so changes to one variable don't affect the other: +Because the `record` modifier preserves the underlying type's semantics, a `record class` and a `record struct` behave differently when you assign or compare references. Assigning a record class copies the reference. Both variables point to the same object. Assigning a record struct copies the data, so changes to one variable don't affect the other: :::code language="csharp" source="snippets/records/RecordStruct.cs" ID="RecordClassVsStruct"::: @@ -49,10 +49,10 @@ Choose `record class` when you need inheritance or when instances are large enou The `record` modifier gives both classes and structs compiler-generated, property-by-property equality. Here's how equality works across all four type kinds: -- **Plain class**: Uses *reference equality* by default—`==` checks whether two variables point to the same object, not whether the data matches. +- **Plain class**: Uses *reference equality* by default. The `==` operator checks whether two variables point to the same object, not whether the data matches. - **Plain struct**: Supports value equality through , but the default implementation uses reflection, which is slower and doesn't generate `==`/`!=` operators. - **`record class`**: The compiler generates `Equals`, `GetHashCode`, and `==`/`!=` that compare every property value. Two distinct objects with the same data are equal. -- **`record struct`**: Same compiler-generated equality as a record class, but for a value type—no reflection, making it faster than plain struct equality. +- **`record struct`**: Same compiler-generated equality as a record class, but for this record type without using reflection, making it faster than plain struct equality. The following example demonstrates record class equality: @@ -62,7 +62,7 @@ The two `Person` instances are different objects, but they're equal because all ## Nondestructive mutation with `with` expressions -Records are often immutable, so you can't change a property after creation. A `with` expression creates a copy with one or more properties changed, leaving the original intact. This works for both `record class` and `record struct` types: +Records are often immutable, so you can't change a property after creation. A `with` expression creates a copy with one or more properties changed, leaving the original record unchanged. This approach works for both `record class` and `record struct` types: :::code language="csharp" source="snippets/records/ImmutableRecord.cs" ID="WithExpression"::: @@ -72,7 +72,7 @@ A `with` expression copies the existing instance, then applies the specified pro ## Positional records and deconstruction -Positional records generate a `Deconstruct` method that lets you extract property values into individual variables: +Positional records generate a `Deconstruct` method that you use to extract property values into individual variables: :::code language="csharp" source="snippets/records/FirstRecord.cs" ID="Deconstruct"::: @@ -88,7 +88,7 @@ Value equality checks include the run-time type, so a `Person` and a `Student` w ## When to use records -Use a record when: +Use a record when all of the following conditions are true: - The type's primary role is storing data. - Two instances with the same values should be equal. From c2744469fa0b65317edadee14b404b73909bb1ad Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 26 Mar 2026 15:34:49 -0400 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/csharp/fundamentals/types/classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/fundamentals/types/classes.md b/docs/csharp/fundamentals/types/classes.md index 5ee8681a72770..e72fc5085875d 100644 --- a/docs/csharp/fundamentals/types/classes.md +++ b/docs/csharp/fundamentals/types/classes.md @@ -12,7 +12,7 @@ ai-usage: ai-assisted > > **Experienced in another language?** C# classes are similar to classes in Java or C++. Skim the [object initializers](#object-initializers) and [collection initializers](#collection-initializers) sections for C#-specific patterns, and see [Records](records.md) for a data-focused alternative. -A *class* is a reference type that defines a blueprint for objects. When you create a variable of a class type, the variable holds a *reference* to an object on the managed heap - not the object data itself. Assigning a class variable to another variable copies the reference, so both variables point to the same object. Classes are the most common way to define custom types in C#. Use them when you need complex behavior, inheritance, or shared identity between references. +A *class* is a reference type that defines a blueprint for objects. When you create a variable of a class type, the variable holds a *reference* to an object on the managed heap. The variable doesn't hold the object data itself. Assigning a class variable to another variable copies the reference, so both variables point to the same object. Classes are the most common way to define custom types in C#. Use them when you need complex behavior, inheritance, or shared identity between references. ## Declare a class @@ -60,7 +60,7 @@ Primary constructors and field initializers can work together: the field initial :::code language="csharp" source="snippets/classes/Program.cs" ID="UsingRequired"::: -For a deeper look at constructor patterns, including parameter validation and constructor chaining, see [Constructors](../object-oriented/index.md). +For a deeper look at constructor patterns, including parameter validation and constructor chaining, see [Constructors](../../programming-guide/classes-and-structs/constructors.md). ## Static classes