Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .openpublishing.redirection.csharp.json
Original file line number Diff line number Diff line change
Expand Up @@ -5304,6 +5304,10 @@
"source_path_from_root": "/docs/csharp/tutorials/pattern-matching.md",
"redirect_url": "/dotnet/csharp/fundamentals/tutorials/pattern-matching"
},
{
"source_path_from_root": "/docs/csharp/tutorials/records.md",
"redirect_url": "/dotnet/csharp/fundamentals/tutorials/records"
},
{
"source_path_from_root": "/docs/csharp/tutorials/upgrade-to-nullable-references.md",
"redirect_url": "/dotnet/csharp/nullable-migration-strategies"
Expand Down
87 changes: 87 additions & 0 deletions docs/csharp/fundamentals/tutorials/records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: Use record types tutorial
description: Build a small app that models temperature data with records, compares record behavior, and uses with expressions for nondestructive mutation.
ms.date: 04/10/2026
ms.topic: tutorial
ai-usage: ai-assisted
---

# Use record types

> [!TIP]
> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You get comfortable with classes, methods, and loops there.
>
> **Experienced in another language?** This tutorial focuses on C# record features you use every day: value equality, positional syntax, and `with` expressions.

In this tutorial, you build a console app that models daily temperatures by using records and record structs.

In this tutorial, you learn how to:

- Declare positional records and record structs.
- Build a small record hierarchy.
- Use compiler-generated equality and formatting.
- Use `with` expressions for nondestructive mutation.

## Prerequisites

[!INCLUDE [Prerequisites](../../../../includes/prerequisites-basic-winget.md)]

## Create the app and your first record

Create a folder for your app, run `dotnet new console`, and open the generated project.

Add a file named *InterimSteps.cs*, and add a positional `readonly record struct` for temperature values:

:::code language="csharp" source="./snippets/records/InterimSteps.cs" ID="DailyRecord":::

Add a file named *Program.cs*, and create sample temperature data:

:::code language="csharp" source="./snippets/records/Program.cs" ID="DeclareData":::

This syntax gives you concise data modeling with immutable value semantics.

## Add behavior to the record struct

Create a file named *DailyTemperature.cs* and add a computed `Mean` property:

:::code language="csharp" source="./snippets/records/DailyTemperature.cs" ID="TemperatureRecord":::

A record struct works well here because each value is small and self-contained.

## Build record types for degree-day calculations

Add a hierarchy for heating and cooling degree-day calculations:

:::code language="csharp" source="./snippets/records/InterimSteps.cs" ID="DegreeDaysRecords":::

Now calculate totals from your `Main` method:

:::code language="csharp" source="./snippets/records/Program.cs" ID="HeatingAndCooling":::

The generated `ToString` output is useful for quick diagnostics while you iterate.

## Customize output by overriding PrintMembers

When default output includes too much noise, override `PrintMembers` in the base record:

:::code language="csharp" source="./snippets/records/DegreeDays.cs" ID="AddPrintMembers":::

This override keeps output focused on the information you need.

## Use with expressions for nondestructive mutation

Use `with` to create modified copies without mutating the original record:

:::code language="csharp" source="./snippets/records/Program.cs" ID="GrowingDegreeDays":::

You can extend that idea to compute rolling totals from slices of your input data:

:::code language="csharp" source="./snippets/records/Program.cs" ID="RunningFiveDayTotal":::

This approach is useful when you need transformations while preserving original values.

## Next steps

- Review [C# record types](../types/records.md) for deeper guidance.
- Continue with [Object-oriented C#](oop.md) for broader design patterns.
- Explore [Converting types](safely-cast-using-pattern-matching-is-and-as-operators.md) to combine records with safe conversion patterns.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
82 changes: 82 additions & 0 deletions docs/csharp/fundamentals/types/conversions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: "Type conversions, casting, and boxing"
description: Learn how to convert between C# types by using implicit and explicit conversions, safe casting patterns, boxing and unboxing, and Parse and TryParse APIs.
ms.date: 04/10/2026
ms.topic: concept-article
ai-usage: ai-assisted
---

# Type conversions, casting, and boxing

> [!TIP]
> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You practice basic types there before you make conversion choices.
>
> **Experienced in another language?** C# conversions work like most statically typed languages: widening conversions are implicit, narrowing conversions need explicit casts, and parsing text should favor `TryParse` in user-facing code.

When you write C# code, you often move values from one type to another. For example, you might convert from `int` to `long`, read text and convert it to a number, or cast a base type to a derived type.

Choose the conversion style based on risk:

- Use an implicit conversion when the compiler proves the conversion is safe.
- Use an explicit cast when data might be lost or the conversion might fail.
- Use pattern matching or `as` when you need safe reference-type conversions.
- Use parsing APIs when your source value is text.

## Use implicit and explicit numeric conversions

Use implicit conversions when the destination type can represent the full source range. Use explicit casts when the destination type has a smaller range or less precision.

:::code language="csharp" source="snippets/conversions/Program.cs" ID="ImplicitAndExplicitNumeric":::

An explicit cast tells readers that the conversion might lose information. In the sample, the `double` value is truncated when converted to `int`.

For full conversion tables, see [Built-in numeric conversions](../../language-reference/builtin-types/numeric-conversions.md).

## Convert references safely

For reference types, you often start with a base type and need to access members from a derived type. Prefer pattern matching so the test and assignment happen together.

:::code language="csharp" source="snippets/conversions/Program.cs" ID="ReferenceConversions":::

Pattern matching keeps the successful cast variable in the smallest practical scope, which improves readability.

If you need a nullable result instead of a conditional branch, use `as`:

:::code language="csharp" source="snippets/conversions/Program.cs" ID="AsOperator":::

Use `as` only with reference types and nullable value types. It returns `null` when conversion fails.

## Understand boxing and unboxing

Boxing converts a value type to `object` or to an implemented interface type. Unboxing extracts the value type from that object reference.

:::code language="csharp" source="snippets/conversions/Program.cs" ID="BoxingAndUnboxing":::

Boxing allocates memory on the managed heap, and unboxing requires a type check. In hot paths, avoid unnecessary boxing because it adds allocations and extra work.

## Parse text by using Parse and TryParse

When you convert user input or file content, start with `TryParse`. It avoids exceptions for expected invalid input and makes failure handling explicit.

:::code language="csharp" source="snippets/conversions/Program.cs" ID="ParseAndTryParse":::

Use `Parse` when input is guaranteed to be valid, such as controlled test data. Use `TryParse` for user input, network payloads, and file data.

## Core conversion APIs

Use these APIs most often in everyday code:

- <xref:System.Int32.Parse(System.String)?displayProperty=nameWithType>
- <xref:System.Int32.TryParse*?displayProperty=nameWithType>
- <xref:System.Double.TryParse*?displayProperty=nameWithType>
- <xref:System.Decimal.TryParse*?displayProperty=nameWithType>
- <xref:System.IConvertible?displayProperty=nameWithType>

For advanced conversion behavior and all overloads, review the API reference for the specific target type.

## See also

- [Type system overview](index.md)
- [Built-in types and literals](built-in-types.md)
- [Pattern matching](../functional/pattern-matching.md)
- [How to safely cast by using pattern matching and the is and as operators](../tutorials/safely-cast-using-pattern-matching-is-and-as-operators.md)
69 changes: 69 additions & 0 deletions docs/csharp/fundamentals/types/delegates-lambdas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: "Delegates, lambdas, and events"
description: Learn how to use Func and Action delegates, write lambda expressions, use static lambdas and discard parameters, and understand the basic event subscription model.
ms.date: 04/10/2026
ms.topic: concept-article
ai-usage: ai-assisted
---

# Delegates, lambdas, and events

> [!TIP]
> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You build core type and method skills there before using delegates.
>
> **Experienced in another language?** Think of delegates as strongly typed function variables. In modern C#, you usually write them with lambda expressions and `Func` or `Action` types.

Delegates let you pass behavior as data. You use delegates when code needs a callback, a rule, or a transformation that the caller supplies.

In everyday C# code, you most often use:

- `Func<T...>` when a delegate returns a value.
- `Action<T...>` when a delegate returns `void`.
- Lambda expressions to create delegate instances.

## Start with Func and Action

`Func` and `Action` cover most delegate scenarios without creating a custom delegate type.

:::code language="csharp" source="snippets/delegates-lambdas/Program.cs" ID="FuncAndAction":::

Use descriptive parameter names in lambdas so readers can understand intent without scanning the full method body.

## Pass lambdas to methods

A common pattern is to accept a `Func<T, bool>` or similar delegate parameter and apply it to a sequence of values.

:::code language="csharp" source="snippets/delegates-lambdas/Program.cs" ID="LambdaAsArgument":::

This pattern appears throughout LINQ and many .NET APIs.

## Use static lambdas when capture is unnecessary

A static lambda can't capture local variables. Use it when the logic depends only on parameters.

:::code language="csharp" source="snippets/delegates-lambdas/Program.cs" ID="StaticLambda":::

Static lambdas make intent clear and prevent accidental captures.

## Use discard parameters when inputs are irrelevant

Sometimes a delegate signature includes parameters you don't need. Use discards to signal that choice clearly.

:::code language="csharp" source="snippets/delegates-lambdas/Program.cs" ID="DiscardParameters":::

Discards improve readability because they show which parameters matter.

## Event basics: subscribe with a lambda

Events expose notifications. You subscribe with a delegate, often a lambda expression.

:::code language="csharp" source="snippets/delegates-lambdas/Program.cs" ID="MinimalEventIntro":::

This model lets publishers raise notifications without knowing subscriber implementation details.

## See also

- [Type system overview](index.md)
- [Methods](../../methods.md)
- [Pattern matching](../functional/pattern-matching.md)
- [Events (C# programming guide)](../../programming-guide/events/index.md)
101 changes: 101 additions & 0 deletions docs/csharp/fundamentals/types/snippets/conversions/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
namespace ConversionsSample;

public class Animal;
public class Mammal : Animal
{
public string Name { get; init; } = "Mammal";
}

public class Reptile : Animal;

public interface ILabelled
{
string Label { get; }
}

public readonly struct Packet(int id) : ILabelled
{
public int Id { get; } = id;
public string Label => $"P-{Id}";
}

public static class Program
{
public static void Main()
{
ShowNumericConversions();
ShowReferenceConversions();
ShowAsOperator();
ShowBoxingAndUnboxing();
ShowParseAndTryParse();
}

private static void ShowNumericConversions()
{
// <ImplicitAndExplicitNumeric>
int itemCount = 42;
long widened = itemCount; // Implicit conversion.

double average = 19.75;
int truncated = (int)average; // Explicit cast.

Console.WriteLine($"widened: {widened}, truncated: {truncated}");
// </ImplicitAndExplicitNumeric>
}

private static void ShowReferenceConversions()
{
// <ReferenceConversions>
Animal knownAnimal = new Mammal { Name = "River otter" };

if (knownAnimal is Mammal mammal)
{
Console.WriteLine($"Pattern match succeeded: {mammal.Name}");
}

Animal unknownAnimal = new Reptile();
Console.WriteLine($"Can treat as mammal: {unknownAnimal is Mammal}");
// </ReferenceConversions>
}

private static void ShowAsOperator()
{
// <AsOperator>
object boxedMammal = new Mammal { Name = "Sea lion" };
Mammal? maybeMammal = boxedMammal as Mammal;
Console.WriteLine(maybeMammal is null ? "Not a mammal" : maybeMammal.Name);

object boxedReptile = new Reptile();
Mammal? noMammal = boxedReptile as Mammal;
Console.WriteLine(noMammal is null ? "Safe null result" : noMammal.Name);
// </AsOperator>
}

private static void ShowBoxingAndUnboxing()
{
// <BoxingAndUnboxing>
int temperature = 72;
object boxedTemperature = temperature; // Boxing.
int unboxedTemperature = (int)boxedTemperature; // Unboxing.

Packet packet = new(7);
ILabelled labelledPacket = packet; // Boxing through an interface reference.

Console.WriteLine($"Unboxed: {unboxedTemperature}, Label: {labelledPacket.Label}");
// </BoxingAndUnboxing>
}

private static void ShowParseAndTryParse()
{
// <ParseAndTryParse>
string textValue = "512";
int parsed = int.Parse(textValue);

string userInput = "12x";
bool parsedSuccessfully = int.TryParse(userInput, out int safeValue);

Console.WriteLine($"parsed: {parsed}");
Console.WriteLine(parsedSuccessfully ? $"safe value: {safeValue}" : "Input is not a valid number.");
// </ParseAndTryParse>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Loading
Loading