-
Notifications
You must be signed in to change notification settings - Fork 6k
Ref updates for 7.2 #3667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Ref updates for 7.2 #3667
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
fd3b22b
first draft ready
BillWagner ea738fb
add samples
BillWagner cd8d451
give it a proofread
BillWagner 5b34265
Fix the build errors.
BillWagner 04207f8
respoond to feedback
BillWagner f545d5f
rename file
BillWagner 5575823
fix a build error
BillWagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
--- | ||
title: Reference semantics with value types | ||
description: Understand the language features that minimize copying structures safely | ||
author: billwagner | ||
ms.author: wiwagn | ||
ms.date: 11/10/2017 | ||
ms.topic: article | ||
ms.prod: .net | ||
ms.technology: devlang-csharp | ||
ms.devlang: csharp | ||
ms.custom: mvc | ||
--- | ||
|
||
# Reference semantics with value types | ||
|
||
An advantage to using value types is that they often avoid heap allocations. | ||
The corresponding disadvantage is that they are copied by value. This tradeoff | ||
makes it harder to optimize algorithms that operate on large amounts of | ||
data. New language features in C# 7.2 provide mechanisms that enable pass-by-reference | ||
semantics with value types. If you use these features wisely you can minimize both allocations | ||
and copy operations. This article explores those new features. | ||
|
||
Much of the sample code in this article demonstrates features added in C# 7.2. In order to | ||
use those features, you have to configure your project to use C# 7.2 or later in your | ||
project. You can use Visual Studio to select it. For each project, select **Project** from | ||
the menu, then **Properties**. Select the **Build** tab and click **Advanced**. From there, | ||
you can configure the language version. Choose either "7.2", or "latest". Or you can | ||
edit the *csproj* file and add the following node: | ||
|
||
```XML | ||
<PropertyGroup> | ||
<LangVersion>7.2</LangVersion> | ||
</PropertyGroup> | ||
``` | ||
|
||
You can use either "7.2" or "latest" for the value. | ||
|
||
## Specifying `in` parameters | ||
|
||
C# 7.2 adds the `in` keyword to complement the existing `ref` | ||
and `out` keywords when you write a method that passes arguments | ||
by reference. The `in` keyword specifies that you are passing | ||
the parameter by reference and the called method does not modify | ||
the value passed to it. | ||
|
||
This addition provides a full vocabulary to express your design intent. | ||
Value types are copied when passed to a called method when you do not | ||
specify any of the following modifiers. Each of these modifiers specify | ||
that a value type is passed by reference, avoiding the copy. Each modifier | ||
expresses a different intent: | ||
|
||
- `out`: This method sets the value of the argument used as this parameter. | ||
- `ref`: This method may set the value of the argument used as this parameter. | ||
- `in`: This method does not modify the value of the argument used as this parameter. | ||
|
||
When you add the `in` modify to pass an argument by reference, you declare | ||
your design intent is to pass arguments by reference to | ||
avoid unnecessary copying. You do not intend to modify the object used | ||
as that argument. The following code shows an example of a method | ||
that calculates the distance between two points in 3D space. | ||
|
||
[!code-csharp[InArgument](../../samples/csharp/reference-semantics/Program.cs#InArgument "Specifying an In argument")] | ||
|
||
The arguments are two structures that each contain three doubles. A double is 8 bytes, | ||
so each argument is 24 bytes. By specifying the `in` modifier, you pass 4-byte | ||
or 8-byte reference to those arguments, depending on the | ||
architecture of the machine. The difference in size is small, but it can quickly add | ||
up when your application calls this method in a tight loop using many different | ||
values. | ||
|
||
The `in` modifier complements `out` and `ref` in other ways as well. You | ||
cannot create overloads of a method that differ only in the presence of | ||
`in`, `out` or `ref`. These new rules extend the same behavior that had always been | ||
defined for `out` and `ref` parameters. | ||
|
||
The `in` modifier may be applied to any member that takes parameters: | ||
methods, delegates, lambdas, local functions, indexers, operators. | ||
|
||
Unlike `ref` and `out` arguments, you may use literal values or constants | ||
for the argument to an `in` parameter. Also, unlike a `ref` or `out` parameter, | ||
you don't need to apply the `in` modifier at the call site. The following | ||
code shows you two examples of calling the `CalculateDistance` method. The | ||
first uses two local variables passed by reference. The second includes a | ||
temporary variable created as part of the method call. | ||
|
||
[!code-csharp[UseInArgument](../../samples/csharp/reference-semantics/Program.cs#UseInArgument "Specifying an In argument")] | ||
|
||
There are several ways in which the compiler ensures that the read-only | ||
nature of an `in` argument is enforced. First of all, the called method | ||
can't directly assign to an `in` parameter. It can't directly assign | ||
to any field of an `in` parameter. In addition, you cannot pass | ||
an `in` parameter to any method demanding the `ref` or `out` modifier. | ||
The compiler enforces that the `in` argument is a readonly variable. You | ||
can call any instance method that uses pass-by-value semantics. In | ||
those instances, a copy of the `in` parameter is created. Because the compiler | ||
can create a temporary variable for any `in` parameter, you can also specify default | ||
values for any `in` parameter. The follow code uses that to specify the origin | ||
(point 0,0) as the default value for the second point: | ||
|
||
[!code-csharp[InArgumentDefault](../../samples/csharp/reference-semantics/Program.cs#InArgumentDefault "Specifying defaults for an in parameter")] | ||
|
||
The `in` parameter designation can also be used with reference types or built in | ||
numeric values. However, the benefits inn both cases are minimal, if any. | ||
|
||
## `ref readonly` returns | ||
|
||
You may also want to return a value type by reference, but disallow the | ||
caller from modifying that value. Use the `ref readonly` modifier to | ||
express that design intent. It notifies readers | ||
that you are returning a reference to existing data, but not allowing | ||
modification. | ||
|
||
The compiler enforces that the caller cannot modify the reference. Attempts | ||
to assign to the value directly generate a compile-time error. However, the compiler cannot know if any member method modifies the state of the struct. | ||
To ensure that the object is not modified, the compiler creates a copy and | ||
calls member references using that copy. Any modifications are to that | ||
defensive copy. | ||
|
||
It's likely that the library using `Point3D` would often use the origin | ||
throughout the code. Every instance creates a new object on the stack. It may | ||
be advantageous to create a constant and return it by reference. But, if you | ||
return a reference to internal storage, you may want to enforce that | ||
the caller cannot modify the referenced storage. The following code | ||
defines a read-only property that returns a `readonly ref` to a `Point3D` | ||
that specifies the origin. | ||
|
||
[!code-csharp[OriginReference](../../samples/csharp/reference-semantics/Point3D.cs#OriginReference "Creating a readonly Origin reference")] | ||
|
||
Creating a copy of a ref readonly return is easy: Just assign it to a variable | ||
not declared with the `ref readonly` modifier. The compiler generates code | ||
to copy the object as part of the assignment. | ||
|
||
When you assign a variable to a `ref readonly return`, you can specify either a `ref readonly` | ||
variable, or a by-value copy of the readonly reference: | ||
|
||
[!code-csharp[AssignRefReadonly](../../samples/csharp/reference-semantics/Program.cs#AssignRefReadonly "Assigning a ref readonly")] | ||
|
||
The first assignment in the preceding code makes a copy of the `Origin` constant and assigns | ||
that copy. The second assigns a reference. Notice that the `readonly` modifier | ||
must be part of the declaration of the variable. The reference to which it refers | ||
cannot be modified. Attempts to do so result in a compile-time error. | ||
|
||
## `readonly struct` type | ||
|
||
Applying `ref readonly` to high-traffic uses of a struct may be sufficient. | ||
Other times, you may want to create an immutable struct. Then you can | ||
always pass by readonly reference. That practice | ||
removes the defensive copies | ||
that take place when you access methods of a struct used as an `in` parameter. | ||
|
||
You can do that by creating a `readonly struct` type. You can add the `readonly` | ||
modifier to a struct declaration. The compiler enforces that all members of | ||
the struct are `readonly`; the `struct` must be immutable. | ||
|
||
There are other optimizations for a `readonly struct`. You can | ||
use the `in` modifier at every location where a `readonly struct` is an | ||
argument. In addition, you can return a `readonly struct` as a `ref return` when | ||
you are returning an object whose lifetime extends beyond the scope of the method | ||
returning the object. | ||
|
||
Finally, the compiler generates more efficient code when you call members of a | ||
`readonly struct`: The `this` reference, instead of a copy of the receiver, | ||
is always an `in` parameter passed by reference to the member method. This optimization | ||
saves more copying when you use a `readonly struct`. The `Point3D` is a great | ||
candidate for this change. The following code shows an updated `ReadonlyPoint3D` structure: | ||
|
||
[!code-csharp[ReadonlyOnlyPoint3D](../../samples/csharp/reference-semantics/Point3D.cs#ReadonlyOnlyPoint3D "Defining an immutable structure")] | ||
|
||
## `ref struct` type | ||
|
||
Another related language feature is the ability to declare a value type that | ||
must be stack allocated. In other words, these types can never be created on the | ||
heap as a member of another class. The primary motivation for this feature | ||
was `Span<T>` and related structures. `Span<T>` may contain a managed pointer as one of its members, the other being | ||
the length of the span. It's actually implemented a bit differently because C# | ||
doesn't support pointers to managed memory outside of an unsafe context. Any | ||
write that changes the pointer and the length is not atomic. That means a | ||
`Span<T>` would be subject to out of range errors or other type safety violations | ||
were it not constrained to a single stack frame. In addition, putting a | ||
managed pointer on the GC heap typically crashes at JIT time. | ||
|
||
You may have similar requirements working with memory created | ||
using [`stackalloc`](language-reference/keywords/stackalloc.md) or | ||
when using memory from interop APIs. You can define your own `ref struct` types | ||
for those needs. In this article, you see examples | ||
using `Span<T>` for simplicity. | ||
|
||
The `ref struct` declaration declares that a struct of this type must be | ||
on the stack. The language rules ensure the safe use of these types. Other | ||
types declared as `ref struct` include `ReadOnlySpan<T>`, `Memory<T>`, and | ||
`ReadOnlyMemory<T>`. | ||
|
||
The goal of keeping a `ref struct` type as a stack-allocated variable | ||
introduces several rules that the compiler enforces for all `ref struct` | ||
types. | ||
|
||
- You can't box a `ref struct`. You cannot assign a `ref struct` type to a variable of type `object`, `dynamic`, or any interface type. | ||
- You can't declare a `ref struct` as a member of a class or a normal struct. | ||
- You cannot declare local variables that are `ref struct` types in async methods. You can declare them in synchronous methods that return `Task`, `Task<T>` or Task-like types. | ||
- You cannot declare `ref struct` local variables in iterators. | ||
- You cannot capture `ref struct` variables in lambda expressions or local functions. | ||
|
||
These restrictions ensure that you do not accidentally use a `ref struct` | ||
in a manner that could promote it to the managed heap. | ||
|
||
## Conclusions | ||
|
||
These enhancements to the C# language are designed for performance critical | ||
algorithms where memory allocations can be critical to achieving the | ||
necessary performance. You may find that you don't often use these features in | ||
the code you write. However, these enhancements have been adopted in many locations | ||
in the .NET Framework. As more and more APIs make use of these features, you'll | ||
see the performance of your own applications improve. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using System; | ||
|
||
namespace reference_semantics | ||
{ | ||
public struct Point3D | ||
{ | ||
public double X; | ||
public double Y; | ||
public double Z; | ||
|
||
#region OriginReference | ||
private static Point3D origin = new Point3D(); | ||
public static ref readonly Point3D Origin => ref origin; | ||
#endregion | ||
} | ||
|
||
#region ReadonlyOnlyPoint3D | ||
readonly public struct ReadonlyPoint3D | ||
{ | ||
public ReadonlyPoint3D(double x, double y, double z) | ||
{ | ||
this.X = x; | ||
this.Y = y; | ||
this.Z = z; | ||
} | ||
|
||
public double X { get; } | ||
public double Y { get; } | ||
public double Z { get; } | ||
|
||
private static ReadonlyPoint3D origin = new ReadonlyPoint3D(); | ||
public static ref readonly ReadonlyPoint3D Origin => ref origin; | ||
} | ||
#endregion | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
using System; | ||
|
||
namespace reference_semantics | ||
{ | ||
class Program | ||
{ | ||
static void Main(string[] args) | ||
{ | ||
var pt1 = new Point3D{X = 3, Y = 4, Z = 5}; | ||
var pt2 = new Point3D{X = 10, Y = 8, Z = 5}; | ||
|
||
#region UseInArgument | ||
var distance = CalculateDistance(pt1, pt2); | ||
var fromOrigin = CalculateDistance(pt1, new Point3D()); | ||
#endregion | ||
fromOrigin = CalculateDistance2(pt1); | ||
|
||
|
||
#region AssignRefReadonly | ||
var originValue = Point3D.Origin; | ||
ref readonly var originReference = ref Point3D.Origin; | ||
#endregion | ||
} | ||
|
||
#region InArgument | ||
private static double CalculateDistance(in Point3D point1, in Point3D point2) | ||
{ | ||
double xDifference = point1.X - point2.X; | ||
double yDifference = point1.Y - point2.Y; | ||
double zDifference = point1.Z - point2.Z; | ||
|
||
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); | ||
} | ||
#endregion | ||
|
||
#region InArgumentDefault | ||
private static double CalculateDistance2(in Point3D point1, in Point3D point2 = default) | ||
{ | ||
double xDifference = point1.X - point2.X; | ||
double yDifference = point1.Y - point2.Y; | ||
double zDifference = point1.Z - point2.Z; | ||
|
||
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); | ||
} | ||
#endregion | ||
|
||
|
||
private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default) | ||
{ | ||
double xDifference = point1.X - point2.X; | ||
double yDifference = point1.Y - point2.Y; | ||
double zDifference = point1.Z - point2.Z; | ||
|
||
return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference); | ||
} | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
samples/csharp/reference-semantics/reference-semantics.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp2.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<LangVersion>7.2</LangVersion> | ||
</PropertyGroup> | ||
|
||
</Project> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that there are multiple ways of updating a project (UI settings, editing csproj, code fix), consider factoring this section to a dedicated page, and referenced from here.
Then you can just have one line: "In order to use those features, you have to configure your project to use C# 7.2 or later in your project, by setting the language version to "7.2" or "latest" per these instructions".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd agree.
I want to take a bit more time to do that, so I made an issue that can get addressed in this or the next sprint: #3685