C# 12
Rainer Stropek | @rstropek@fosstodon.org | @rstropek
C# 12 Status
- C# 12 is under heavy development!
- Currently still quite unstable
- Many features only in proposal state
- Unsure what will make it into C# 12
- Interesting reads
Semi-Auto
Properties
var p = new Properties
{
ManualProp = "",
AutoProp = "",
AutoInitOnlyProp = "",
RequiredProp = "",
};
class Properties
{
private string? ManualPropValue;
public string? ManualProp
{
get => ManualPropValue;
set => ManualPropValue = value;
}
public string? AutoProp { get; set; } = "";
public string AutoReadOnlyProp { get; } = ""; // Can be assigned in ctor
public string? AutoInitOnlyProp { get; init; }
public required string RequiredProp { get; set; }
public string ExpressionBodiedProp => "";
}
Recap: Property Types
Semi-Auto Properties
- Extended auto-implemented properties
- Still have an automatically generated backing field
- Bodies for accessors can be provided
- Use field keyword to reference backing field
class Vector2d
{
public float X
{
get => field;
set => field = value;
}
public float Y
{
get => field;
set => field = value;
}
static Vector2d() { Zero = new() { X = 0f, Y = 0f }; }
public static Vector2d Zero { get => field; }
}
var p = new Person { FirstName = "Foo", LastName = "Bar" };
p.PropertyChanged += (s, ea) => Console.WriteLine($"{ea.PropertyName} changed");
p.LastName = "Baz";
class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string FullName => $"{LastName}, {FirstName}";
public string FirstName
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(FirstName)));
PropertyChanged?.Invoke(this, new(nameof(FullName)));
}
}
}
public string LastName
{
// ... (like FirstName above)
}
}
Primary
Constructors
Primary Constructors
- Today, record types offer primary constructors
- Much shorter than declaring a regular constructor
var pr = new PersonRecord("Foo", "Bar");
var pc = new PersonClass("Foo", "Bar");
class PersonClass
{
public PersonClass(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; init; }
public string LastName { get; init; }
}
record PersonRecord(string FirstName, string LastName);
- Goal: Offer similar functionality for classes
Primary Constructors
var pc = new PersonClass("Foo", "Bar");
System.Console.WriteLine(pc.FullName);
class PersonClass(string firstName, string lastName)
{
public string FullName => $"{lastName}, {firstName}";
}
- Generates private fields, not public props (as in records)
- Field not generated if not referenced
- Fields do not have the same name as the ctor params
- Any other constructor must call the primary constructor
using static System.Console;
Hero h = new("Wonder Woman", "Princess Diana of Themyscira", true);
WriteLine(h.Description);
class Hero(string name, string realName, bool canFly)
{
// All other constructors must use primary constructor
public Hero(string name, bool canFly) : this(name, "", canFly) { }
public Hero(string name) : this(name, "", false) { }
public Hero() : this("") { }
public string Description => $"{name} is {realName} and can{(canFly ? "" : "not")} fly";
// Note that you cannot use "this.name" as "name"
// is a parameter name in the constructor, not the
// name of the field.
// public string Name { get; } = name; // Beware of that!
public string Name => name;
}
Span<T> for variadic functions
Problem: Array Allocation
using System;
using static System.Console;
Printer.Write("Hello", "World", 42);
static class Printer
{
public static void Write(string message, params Object[] arg)
{
WriteLine(message);
foreach (var a in arg)
{
WriteLine(a.ToString());
}
}
}
Solution so far: Overrides
Solution in C# 12: Spans, stackalloc
using System;
using static System.Console;
// This will try to allocate param array on the stack
// in many cases -> no heap allocation -> less GC.
Printer.Write("Hello", "World", 42);
static class Printer
{
public static void Write(string message, params ReadOnlySpan<object?> arg)
{
WriteLine(message);
foreach (var a in arg)
{
WriteLine(a.ToString());
}
}
}
nameof enhancements
nameof on Members
public class Other
{
public Class Class;
}
public class Class
{
[Attr(nameof(Other.Class))] // Today: error CS0120 An object reference is required
public Other Other;
}
public class Attr : System.Attribute
{
public Attr(string s) {}
}
Default Parameters in Lambdas
Allow Default Values in Lambdas
var add = (int addTo) => addTo + 1;
var addWithDefault = (int addTo = 2) => addTo + 1;
add(2); // 3
addWithDefault(); // 3
addWithDefault(5); // 6
- Why? ASP.NET Core Minimal API
var app = WebApplication.Create(args);
app.MapPost("/todos/{id}", (int id, string task = "foo", TodoService todoService) => {
var todo = todoService.Create(id, task);
return Results.Created(todo);
});
Deconstructing default
Deconstructing default
using System;
using static System.Console;
var aTuple = (number: 42, text: "Fourtytwo");
var (number, text) = aTuple;
WriteLine($"{number}; {text}");
int defaultNumber = default;
DateTime defaultDateTime = default;
string? defaultText = default;
WriteLine($"{defaultNumber}; {defaultDateTime}; {defaultText ?? "null"}");
(int, string?) tupleWithDefaults = (default, default);
(number, text) = tupleWithDefaults;
WriteLine($"{number}; {text ?? "null"}");
// And new is:
(int anotherNumber, string? anotherText) = default;
WriteLine($"{anotherNumber}; {anotherText ?? "null"}");
(number, text) = default;
WriteLine($"{number}; {text ?? "null"}");
Deconstructing default
- Why?
- Example: Go-like error handling
(int result, ErrorKind err) = default;
(result, err) = DoSomethingThatMightFail(41);
if (err != ErrorKind.NoError)
{
System.Console.WriteLine(err);
}
(int result, ErrorKind err) DoSomethingThatMightFail(int parameter)
{
// Note use of default in the implementation
if (parameter >= 42) { return (42, default); };
return (default, ErrorKind.NotImplemented);
}
enum ErrorKind
{
NoError,
GeneralError,
NotImplemented,
InvalidState,
}
Type Aliases
Enhancements
using static System.Console;
using MagicNumbers = System.Collections.Generic.List<(string, int)>;
using MagicNumber = (string, int); // This did not work prior C# 12
MagicNumbers numbers = new() { ( "Answer", 42 ) };
numbers.ForEach(n => WriteLine(n));
MagicNumber number = ("Answer", 42);
WriteLine(number);
Collection
Literals
Sooo many collection initializers
var numbers1 = new int[] { 1, 2, 3, 4, 5 };
var numbers2= new [] { 1, 2, 3, 4, 5 };
int[] numbers3 = { 1, 2, 3, 4, 5 };
Span<int> numbers4 = stackalloc [] { 1, 2, 3, 4, 5 };
var numbers5 = new List<int> { 1, 2, 3, 4, 5 };
List<int> numbers6 = new() { 1, 2, 3, 4, 5 };
// Will become a series of calls to List.Add
var numbers7 = ImmutableArray.Create<int>()
.Add(1)
.AddRange(2, 3, 4, 5);
var numbers8Builder = ImmutableArray.CreateBuilder<int>(5);
numbers8Builder.Add(1);
numbers8Builder.AddRange(2, 3, 4, 5);
var numbers8 = numbers8Builder.ToImmutableArray();
Sooo many collection initializers
var dict1 = new Dictionary<int, string>
{
{ 1, "One" },
{ 2, "Two" },
{ 3, "Three" }
};
Dictionary<int, string> dict2 = new()
{
{ 1, "One" },
{ 2, "Two" },
{ 3, "Three" }
};
var dict3 = new Dictionary<int, string>()
{
[1] = "One",
[2] = "One",
[3] = "One"
};
Inconsistency with List Patterns
// Creation
var numbers1 = new int[] { 1, 2, 3, 4, 5 };
// List Pattern
if (numbers1 is [ 1, 2, 3, 4, 5 ]) { Console.WriteLine("This is my list!"); }
Goals
- C# has so many different collection initialization methods today
- Some of these forms lead to inefficient code (e.g. multiple Add calls when constructing a list, even if the size is known)
- People are used to collection literals from other languages
- Goal for C#: Simplify the creation of collections and dictionaries by introducing collection literals
# Python list literals
numbers = [1, 2, 3, 4, 5]
// JavaScript/TypeScript array literal notation
const numbers = [1, 2, 3, 4, 5];
C# Collection Literals
using System;
using System.Collections.Generic;
int[] numbersArray = [ 1, 2, 3, 4, 5 ];
List<int> numbersList = [ 1, 2, 3, 4, 5 ];
// var numbersList = [ 1, 2, 3, 4, 5 ];
// Should result in a stack allocation (not yet)
Span<int> numbersSpan = [ 1, 2, 3, 4, 5 ];
// Spread operator does not work (yet)
// int[] moreNumbers = [ ..numbers, 6, 7, 8 ];
// Dictionary initializers do not work (yet)
// Dictionary<int, string> dict = [ 1: "One", 2: "Two", 3: "Three" ];
// var moreDict = [ ..dict, 4: "Four" ];
// Implement Construct method to support collection literals for other types
Extensions
Glimpse into future...
Extensions
public class DataObject
{
public DataObject this[string member] { get; set; }
public string AsString() { }
public IEnumerable<DataObject> AsEnumerable();
public int ID { get; }
}
// Old
public static class DataObjectExtensions
{
public string ToJson(this DataObject) { ... }
...
}
// New
public implicit extension JsonDataObject for DataObject
{
public string ToJson() { … this … }
public static DataObject FromJson(string json) { … } // Note: static method
}
- Enhanced syntax for extension methods
Explicit Extensions
public explicit extension Order for DataObject
{
public Customer Customer => this["Customer"];
public string Description => this["Description"].AsString();
}
public explicit extension Customer for DataObject
{
public string Name => this["Name"].AsString();
public string Address => this["Address"].AsString();
public IEnumerable<Order> Orders => this["Orders"].AsEnumerable();
}
- Selectively augment individual members
- Underlying representation is identical
- I.e. conversions are free at runtime
Explicit Extensions
IEnumerable<Customer> customers = LoadCustomers();
foreach (var customer in customers)
{
WriteLine($"{customer.Name}:");
foreach (var order in customer.Orders)
{
WriteLine($" {order.Description}");
}
}
Future: Implement interfaces
public interface IPerson
{
public int ID { get; }
public string FullName { get; }
}
public explicit Customer for DataObject : IPerson
{
string IPerson.FullName => Name;
// ...
}
- Roles and extensions can add interface implementation to existing type
C# 12 🤘
Rainer Stropek | @rstropek
C# 12
By Rainer Stropek
C# 12
- 638