C# is a Doddle

Steve Love


@IAmSteveLove@mastodon.social
@IAmSteveLove


Essential Complexity

C# is a simple […​] programming language…​

Yeah…​
…​.right

Captured Loop Variables

        var tasks = new List<Task<int>>();
        
        foreach(var start in Enumerable.Range(0, 4))
        {
            tasks.Add(Task.Factory.StartNew(() => start * start));
        }

        Assert.That(tasks[0].Result, Is.EqualTo(0));

Spot The Difference

        var tasks = new List<Task<int>>();
        
        for(var start = 0; start != 4; ++start)
        {
            tasks.Add(Task.Factory.StartNew(() => start * start));
        }

        Assert.That(tasks[0].Result, Is.EqualTo(0));

Without Loops

        var result = Enumerable.Range(0, 4)
            .Select(start => Task.Factory.StartNew(() => start * start))
            .ToList();
        
        Assert.That(result[0].Result, Is.EqualTo(0));

The Evolution of C#

  • Long-standing hazards and anomalies

  • New language features bring new pitfalls to avoid…​

  • …​and some that make things easier too

Operators & Overloading

public readonly struct Color : IEquatable<Color>
{
    public int Red { get; init; }
    public int Green { get; init; }
    public int Blue { get; init; }
    
    public bool Equals(Color other) => Red == other.Red && 
                                       Green == other.Green && 
                                       Blue == other.Blue;
    
    public override bool Equals(object? obj) => obj is Color other && Equals(other);

    public override int GetHashCode() => HashCode.Combine(Red, Green, Blue);
}

Equality Operators

public readonly struct Color : IEquatable<Color>
{
    // ...
    
    public bool Equals(Color other) => Red == other.Red && 
                                       Green == other.Green && 
                                       Blue == other.Blue;
    
    public static bool operator==(Color left, Color right) => left.Equals(right);
    public static bool operator!=(Color left, Color right) => left.Equals(right);
}

…​ooops, that should probably be
!left.Equals(right);

Checking for null

public sealed class Color : IEquatable<Color>
{
    public static bool operator==(Color? left, Color? right) 
        => left == null ? right == null : left.Equals(right);
    
    // ...
}

That should be:

=> left is null ? right is null : left.Equals(right);

Or, better:

=> left?.Equals(right) ?? right is null;

Nullable Values

Color? red = new Color();

Color? blue = new Color { Red = 0, Green = 0, Blue = 0xFF };


Assert.True(red != blue);

Lifting an Operator

// Color? red = new Color();
Nullable<Color> red = new Color();

// Color? blue = new Color { Red = 0, Green = 0, Blue = 0xFF };
Nullable<Color> blue = new Color { Red = 0, Green = 0, Blue = 0xFF };

// Assert.That(red != blue, Is.True);
Assert.True(red.HasValue != blue.HasValue ||
            red.HasValue && red.GetValueOrDefault() != blue.GetValueOrDefault());

public static bool operator==(Color? left, Color? right) => // ...

Construction & Initialization

public readonly struct LogEntry : IEquatable<LogEntry>
{
    public string Message { get; init; }
    public DateTime TimeStamp { get; init; }

    // ...
}

var log = new LogEntry();
// ...
var compact = log.Message.Trim();

Parameterless Constructors

public readonly struct LogEntry : IEquatable<LogEntry>
{
    public LogEntry() 
        => (Message, TimeStamp) = (string.Empty, default);
    
    public string Message { get; init; }
    public DateTime TimeStamp { get; init; }

    // ...
}

Property Initializers

public readonly struct LogEntry : IEquatable<LogEntry>
{
    public LogEntry()
    {
    }

    public string Message { get; init; } = string.Empty;
    public DateTime TimeStamp { get; init; } = default;

    // ...
}

var log = default(LogEntry);
// ...
var compact = log.Message.Trim();

Record Structs

public readonly record struct LogEntry(string Message, DateTime TimeStamp);

var log = new LogEntry(Message: "Created new entry", 
                       TimeStamp: DateTime.Now);

Assert.That(log.Message, Is.EqualTo("Created new entry"));

Properties of Record Structs

public readonly record struct LogEntry(string Message, DateTime TimeStamp)
{
    public string Message { get; init; } = string.Empty;
    public DateTime TimeStamp { get; init; } = default;
}

var log = new LogEntry();
// ...
var compact = log.Message.Trim();

Positional Properties

public readonly record struct LogEntry(string Message, DateTime TimeStamp)
{
    public string Message { get; init; } = string.Empty;
    public DateTime TimeStamp { get; init; } = default;
}
var log = new LogEntry("message", DateTime.Now);

Assert.That(log.Message, Is.EqualTo("message"));

Don’t mix positional and manual properties

Exception Safety

public class Broker : IDisposable
{
    public Broker(string sourceAddress, string sinkAddress)
    {
        source = new Source(sourceAddress);
        sink = new Sink(sinkAddress);
    }

    public void Dispose()
    {
        source.Dispose();
        sink.Dispose();
    }
    
    private readonly Source source;
    private readonly Sink sink;
}

Try and Try Again

source = new Source(sourceAddress);
try
{
    sink = new Sink(sinkAddress);
}
catch
{
    source.Dispose();
    throw;
}

Error prone and ugly

Using using

public Broker(string sourceAddress, string sinkAddress)
{
    using var temp = new Source(sourceAddress);
    
    sink = new Sink(sinkAddress);
    source = temp;
}

Disabling using?

public Broker(string sourceAddress, string sinkAddress)
{
    using var temp = new Source(sourceAddress);
    
    sink = new Sink(sinkAddress);
    source = temp;
    
    temp = null;
}

Acquiring a Disposable

using var owned = new Owner<Source>(new Source(sourceAddress));

sink = new Sink(sinkAddress);
source = owned.Acquire();

The Owner Class

public sealed class Owner<T> : IDisposable where T : IDisposable
{
    public Owner(T owned) => this.owned = owned;
    public void Dispose() => this.owned?.Dispose();
    
    public T Acquire()
    {
        (T temp, owned) = (owned!, default);
        return temp;
    }
    
    private T? owned;
}

A similar approach is useful for a sequence of disposable objects, too

Not only but also…​

What if owned is null?

E.g. multiple calls to Acquire

public sealed class Owner<T> : IDisposable where T : IDisposable
{
    // ...

    public T Acquire()
    {
        if(owned is null)
            throw new ObjectDisposedException(nameof(owned));
        
        (T temp, owned) = (owned!, default);
        return temp;
    }
    
    private T? owned;
}

A Special Case

if(owned is null)
    throw new ObjectDisposedException(nameof(owned));

Suppose T is a value type…​

What does owned is null mean?


(hint - is means ==)

The Weasel Words

Generics & Operators

public double CumulativeAverage(double mean, int count, double next)
    => mean + (next - mean) / (count + 1);

Without Generics

public readonly struct Temperature : IEquatable<Temperature>
{
    private Temperature(double amount) => celsius = amount;

    public static Temperature FromCelsius(double celsius) => new(celsius);
    public double InCelsius => celsius;
    
    // ...
    
    private readonly double celsius;
}

var recordedTemperature = ReadFromSensor();

var avg = CumulativeAverage(averageTemp.InCelsius, count, 
                            recordedTemperature.InCelsius);

averageTemp = Temperature.FromCelsius(avg);

Target State

var averageTemp = CumulativeAverage(averageTemp, count, recordedTemperature);

Could be achieved with generics:

public T CumulativeAverage<T>(T mean, int count, T next)
    => mean + (next - mean) / (count + 1);

Red Herrings

C# v8.0 Default Interface Implementation

public interface IAddition<T> where T : IAddition<T>
{
    static T operator+(T left, T right) 
        => left.Add(right);
        
    T Add(T other);
}

Besides, needs operator- and operator/

Generic Arithmetic

public T CumulativeAverage<T>(T mean, int count, T next)
    where T : IAdditionOperators<T, T, T>, 
    ISubtractionOperators<T, T, T>,
    IDivisionOperators<T, double, T>
{
    return mean + (next - mean) / (count + 1);
}

Implementing Constraints

public readonly struct Temperature : IEquatable<Temperature>, 
    IAdditionOperators<Temperature, Temperature, Temperature>,
    ISubtractionOperators<Temperature, Temperature, Temperature>,
    IDivisionOperators<Temperature, double, Temperature>
{
    // ...
    public static Temperature operator+(Temperature left, Temperature right) 
        => new(left.celsius + right.celsius);
    
    public static Temperature operator-(Temperature left, Temperature right) 
        => new(left.celsius - right.celsius);
    
    public static Temperature operator/(Temperature left, double right) 
        => new(left.celsius / right);
    // ...
    
    private readonly double celsius;
}

Generic Comparisons

public sealed class Owner<T> : IDisposable 
    where T : IDisposable, IEqualityOperators<T, T, bool>
{
    // ...

    public void Assign(T other)
    {
        if(other == owned!)
            return;
        
        owned = other;
    }

    private T? owned;
}

A value-like type might need to implement IEqualityOperators<T, T, bool>
and IEquatable<T>

Lessons Learned

  • Defining equality is still easy to get wrong

  • Exception safety is still hard

  • The compiler will generate lots of stuff but

    • Initialization is more complicated than ever

    • Records & record structs are no silver bullet

…​And

  • Generic arithmetic is here! Yay!

But

  • There’s still no way to constrain a parameterized constructor

public T AddItem<T>(string msg, DateTime stamp)
    where T : new(string, DateTime)
{
    // ...
    return new T(msg, stamp);
}

That’s All Folks