C# 12

Rainer Stropek | @rstropek@fosstodon.org | @rstropek

C# 12 Status

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