- Open
Program.cs
. - Build and run (
Ctrl+F5
) to show the program output.
Record structs have the same features and very similar semantics as record classes. They provide the full machinery for value equality, including an implementation of IEquatable<T>
and ==
/!=
operators. They also support with
expressions.
- Add the
class
keyword afterrecord
forPerson
. It should now read asrecord class Person
. - Change
class
tostruct
forPerson
. It should now read asrecord struct Person
. - Build and run (
Ctrl+F5
) to show that the program output is unchanged.
with
expression support has been added for vanilla non-record structs. This works seamlessly because structs are copy-by-value.
- Remove the
record
keyword fromPerson
. It should now read asstruct Person
.
Semantically, anonymous types are really "anonymous records". So, we've made with
expressions work with them as well.
- Remove
Person
fromvar person = new Person
on line 4. It should now read asvar person = new
. - Hover over
var
to show thatperson
is now an anonymous type.
We've added several other features to vanilla non-record structs. First, it is now possible to declare public, parameterless constructor.
-
Add a public, parameterless constructor to
Person
that initializes both theFirstName
andLastName
properties.struct Person { public Person() { FirstName = "John"; LastName = "Doe"; } public string FirstName { get; init; } public string LastName { get; init; } public void WriteToFile(string filePath) => File.WriteAllText(filePath, ToString()); }
This represents a change in philosophy for C# with regard to structs. Previously, we chose not allow public, parameterless constructors because it means that default(SomeStruct)
may result in a different value than new SomeString()
. So, the following code results in different Person
values.
-
Add the following lines above the declaration of
Person
structPerson p1 = default; Person p2 = new();
In addition, it is now possible to use field and property initializers in structs, which weren't previously allowed for the same reason.
-
Remove the constructor that was just added and use initializers for the properties.
struct Person { public string FirstName { get; init; } = "John"; public string LastName { get; init; } = "Doe"; }
Our project is starting to grow, so let's move Person
into a separate file. Fortunately, the IDE has several refactorings that can help with that.
- Move editor caret to
Person
and pressCtrl+.
to bring up the list of available refactorings. - Choose "Move type to Person.cs" to move the
Person
struct to a new file. - Click on a reference to
Person
and pressF12
to go to the definition ofPerson
inPerson.cs
. - Move the editor caret to
Person
and pressCtrl+.
to bring up the list of available refactorings. - Choose "Move to namespace..." and type "Model" to move
Person
into a namespace namedModel
.
It's unfortunate that namespaces have forced nearly every C# class to be indented. With file-scoped namespaces, that's no longer a concern.
- Remove the curly braces for the
Model
namespace and add a;
afterModel
.
using System.IO;
namespace Model;
struct Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; init; } = "John";
public string LastName { get; init; } = "Doe";
public void WriteToFile(string filePath)
=> File.WriteAllText(filePath, ToString());
}
Another bit of C# clutter that has "infected" every C# file is the block of using directives. In C# 10, this can be cleaned up with global using directives, which are essentially using directives that apply throughout the project.
- Open
Usings.cs
. - Move the editor caret before the first
using
and pressShift+Alt+Down
until there is a vertical selection in front of all the using directives. - Type
global
before the using directives to transform all of them into global usings.
We should go ahead and add a global using directive for Model
, since we expect to use that namespace throughout our project.
-
Immediately before the last global using, and one one more:
global using Model;
.global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Text; global using System.Threading.Tasks; global using Model; global using static System.Console;
And now we can clean up the using directives in our other files, since they're declared globally here.
- Use the "Remove Unnecessary Usings" code fix at the top of both
Person.cs
andProgram.cs
.
For .NET 6, we've built a tooling feature that allows global using directives to be generated from information in the project file. We call this feature "implicit usings". This feature is enabled by default for new projects created with .NET 6 SDK, and it's easy to enable it for existing projects.
-
Open the project file (
CSharpTen.csproj
) and add<ImplicitUsings>enable</ImplicitUsings>
.<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> </Project>
We can easily add our Model
namespace here as well.
-
Add a new
<ItemGroup>
containing<Using Include="Model"/>
.<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <Using Include="Model" /> </ItemGroup> </Project>
-
Open
Using.cs
and delete all of the using directives except forglobal using static System.Console;
.
Let's take a look at a few other features of C# 10, such as improvements to lambda expressions and method groups.
- Open
OtherStuff.cs
.
At long last, we've defined natural types for lambda expressions, which are the Func
and Action
delegates that were first introduced in C# 3.0.
Now, we can can correctly infer a delegate type for a lambda expression if there is enough type information.
-
On line 9, change
Func<string, int>
tovar
.var parse = (string s) => int.Parse(s);
Of course, if there isn't enough information to infer a delegate type, the C# compiler will produce an error.
-
Change
(string s) =>
tos
.var parse = s => int.Parse(s);
It's also legal to assign a lambda to a type that is convertible from the inferred delegate type, such as object
or Delegate
.
-
Change
var
toobject
.object parse = (string s) => int.Parse(s);
-
Change
object
toDelegate
.Delegate parse = (string s) => int.Parse(s);
Similarly, it is possible to assign a lambda to a legal Expression
type.
-
Change
Delegate
toExpression
and pressCtrl+.
to add a using directive forSystem.Linq.Expressions
.Expression parse = (string s) => int.Parse(s);
-
Change
Expression
toLambdaExpression
.LambdaExpression parse = (string s) => int.Parse(s);
In this case, not enough information is provided by the lambda expression to infer a return type. So, the compiler produces an error.
-
On line 11, change
Func<bool, object>
tovar
.var choose = (bool b) => b ? 1 : "two";
In C# 10, it's possible to add return types for lambda expressions, which allows Func<bool, object>
to be inferred.
-
Add
object
after=
to give the lambda expression a return type.var choose = object (bool b) => b ? 1 : "two";
Finally, C# 10 will infer delegate types for method groups if possible.
-
On line 20, change
Func<int>
tovar
.var read = Console.Read;
In this case, there are multiple overloads, so a delegate type can't be inferred.
-
On line 21, change
Action<string>
tovar
.var write = Console.Write;
Notice that this will cause an error.
- Open
OtherStuff.cs
.
C# 6 introduced an incredibly powerful and useful feature: interpolated strings. At a high level, interpolated strings are highly readable string.Format(...)
calls. Unfortunately, they bring a lot of costs in the form of hidden allocations. In addition, interpolated strings are built eagerly,
In C# 10, we've introduced a new library pattern that allows APIs to be written that work directly with interpolated strings and can make the code that we're already writing far more efficient. Several of APIs that take advantage of this pattern have been added in .NET 6.
- Show the interpolated string usage on line 32.
public string BuildString(object[] args)
{
var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");
return sb.ToString();
}
In C# 6, this interpolated string would result a string.Format(...)
call which would use a different StringBuilder
to produce a string. Then the string would be added to sb
. In C# 10, the interpolated string is handled specially and is added directly to sb
without requiring another StringBuilder
. So, the code that we were already writing is just faster.
Hovering over the StringBuilder.Append
call on line 32 reveals that the overload of Append
that is being called takes a StringBuilder.AppendInterpolatedStringHandler
rather than a string
. This allows the API to perform custom processing of the interpolated string arguments.
- Show the interpolated string usage on line 39.
public void DebugAssert(bool condition)
{
Debug.Assert(condition, $"{DateTime.Now} - {ExpensiveCalculation()}");
}
In C# 6, the interpolated string passed to Debug.Assert
is always created, even if condition
is true
. In C# 10, the arguments to the interpolated string won't be evaluated unless condition
is false
.