Steve Love
@IAmSteveLove@mastodon.social
@IAmSteveLove
C# is a simple […] programming language…
Yeah…
….right
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));
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));
var result = Enumerable.Range(0, 4)
.Select(start => Task.Factory.StartNew(() => start * start))
.ToList();
Assert.That(result[0].Result, Is.EqualTo(0));
Long-standing hazards and anomalies
New language features bring new pitfalls to avoid…
…and some that make things easier too
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);
}
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);
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;
Color? red = new Color();
Color? blue = new Color { Red = 0, Green = 0, Blue = 0xFF };
Assert.True(red != blue);
// 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) => // ...
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();
public readonly struct LogEntry : IEquatable<LogEntry>
{
public LogEntry()
=> (Message, TimeStamp) = (string.Empty, default);
public string Message { get; init; }
public DateTime TimeStamp { get; init; }
// ...
}
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();
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"));
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();
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
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;
}
source = new Source(sourceAddress);
try
{
sink = new Sink(sinkAddress);
}
catch
{
source.Dispose();
throw;
}
Error prone and ugly
using
public Broker(string sourceAddress, string sinkAddress)
{
using var temp = new Source(sourceAddress);
sink = new Sink(sinkAddress);
source = temp;
}
using
?public Broker(string sourceAddress, string sinkAddress)
{
using var temp = new Source(sourceAddress);
sink = new Sink(sinkAddress);
source = temp;
temp = null;
}
using var owned = new Owner<Source>(new Source(sourceAddress));
sink = new Sink(sinkAddress);
source = owned.Acquire();
Owner
Classpublic 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
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;
}
if(owned is null)
throw new ObjectDisposedException(nameof(owned));
Suppose T
is a value type…
What does owned is null
mean?
(hint - is
means ==
)
In addition to normal applicability rules (§12.6.4.2), the predefined reference type equality operators require one of the following in order to be applicable:
…snip…
One operand is the literal null, and the other operand is a value of type T where T is a type_parameter that is not known to be a value type, and does not have the value type constraint.
If at runtime T is a non-nullable value type, the result of == is false and the result of != is true.
If at runtime T is a nullable value type, the result is computed from the HasValue property of the operand, as described in (§11.11.10).
If at runtime T is a reference type, the result is true if the operand is null, and false otherwise.
public double CumulativeAverage(double mean, int count, double next)
=> mean + (next - mean) / (count + 1);
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);
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);
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/
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);
}
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;
}
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>
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
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);
}
Steve Love
@IAmSteveLove@mastodon.social
@IAmSteveLove
Equality operators in generics:
https://github.com/dotnet/csharpstandard/blob/draft-v7/standard/expressions.md#11117-reference-type-equality-operators
Default interface implementations:
https://learn.microsoft.com/en-gb/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods
Static abstract interface methods (generic operators):
https://learn.microsoft.com/en-gb/dotnet/csharp/language-reference/proposals/csharp-11.0/static-abstracts-in-interfaces