Puzzling C#

Steve Love


@IAmSteveLove@mastodon.social
@stevelove.bsky.social


More puzzles

Something for everyone

Who are you?

New to to C#?

I hope you learn something interesting!

C# veteran?

I want to mess with your mind! (and I hope you still learn something interesting)

The on ramp

Basic arithmetic

Every problem becomes very childish when once it is explained to you.

The Adventure of the Dancing Men
— Sherlock Holmes

Relative importance

double price = 200.0;
double percent = 20.0;

price *= 1 + percent / 100.0;

What does this do?

The canon

For a binary operator op, a compound assignment expression of the form

x op= y

is equivalent to

x = x op y

except that x is only evaluated once.

This

double a = 200.0;
double b = 0.2;

a = a * b;

Assert.That(a, Is.EqualTo(40));

Can be replaced by

double a = 200.0;
double b = 0.2;

a *= b;

Assert.That(a, Is.EqualTo(40));

Possibilities

double price = 200.0;
double percent = 20.0;

price *= 1 + percent / 100.0;

(a)

price = price * (1 + percent / 100);

240.0

(b)

price = (price * 1) + percent / 100;

200.2
(ish)

The answer is…​

double price = 200.0;
double percent = 20.0;

price *= 1 + percent / 100.0;

Assert.That(price, Is.EqualTo(240));

(a)

price = price * (1 + percent / 100);

240.0

But…​why?

In order of precedence

Operators

Category or name

blah…​

blah…​

x * y, x / y, x % y

Multiplicative

x + y, x – y

Additive

blah…​

blah…​

x = y, x += y, x -= y, x *= y, x /= y, x %= y, x &= y, x |= y, x ^= y, x <⇐ y, x >>= y, x >>>= y, x ??= y,

Assignment and lambda declaration

Minutiae

double price = 200.0;
double percent = 20.0;

price = price * 1 + percent / 100.0;
double price = 200.0;
double percent = 20.0;

price *= 1 + percent / 100.0;

200.2

240.0

It matters!

Initialization

It is a capital mistake to theorize before one has data.

A Scandal in Bohemia
— Sherlock Holmes

Intricate inheritance

public abstract class Base
{
    protected Base(string name)
    {
        Name = name;
    }
    
    public virtual string Name 
            { get; set; } = "base";
}
public class Child : Base
{
    public Child() 
        : base("default")
    {
    }

    public override string Name 
            { get; set; } = "child";
}
var child = new Child();
Console.WriteLine(child.Name);

a) base

b) child

c) default

The answer is…​

public abstract class Base
{
    protected Base(string name)
    {
        Name = name;
    }
    
    public virtual string Name 
            { get; set; } = "base";
}
public class Child : Base
{
    public Child() 
        : base("default")
    {
    }

    public override string Name 
            { get; set; } = "child";
}
var child = new Child();
Console.WriteLine(child.Name);

a) base

b) child

c) default

Virtual call

callvirtual

Initialization order

constructorder

What about this?

getonly
public class Child : Base
{
    public Child() 
        : base("default")
    {
    }

    public override string Name 
            { get; } = "child";
}
var child = new Child();
Console.WriteLine(child.Name);

a) base

b) child

c) default

The answer is…​

getonly
public class Child : Base
{
    public Child() 
        : base("default")
    {
    }

    public override string Name 
            { get; } = "child";
}
var child = new Child();
Console.WriteLine(child.Name);

a) base

b) child

c) default

Virtual property

With a (virtual) set accessor:

IL_0013: ldarg.0      // this
IL_0014: ldarg.1      // name
IL_0015: callvirt     instance void code.Test_auto_mutable_prop/Base::set_Name(string)
IL_001a: nop        

Without a set accessor:

IL_0013: ldarg.0      // this
IL_0014: ldarg.1      // name
IL_0015: stfld        string code.Test_auto_immutable_prop/Base::'<Name>k__BackingField'

but it’s just minutiae, right?

Asychronicity

I never make exceptions. An exception disproves the rule.

The Sign of Four
— Sherlock Holmes

We don’t need no…​

…​return value

public async void PersistState()
{
    await Task.Run(() =>
    {
        // long-running operation
        
        // ... that may fail
        throw new Exception("An arbitrary error");
    });
}
public void Flush(object? state)
{
    try
    {
        PersistState();
        Console.WriteLine("Done");
    }
    catch // anything
    {
        Console.WriteLine("Error");
    }
}

The answer is…​

bang Your program might exit unconditionally

i.e. it’ll crash

probably

A bit of terminology

Warning: high-level hand waving here!

Promise

A function that returns a future

Future

A a result that may be obtained on a different thread asynchronously, and made available to the current thread by a promise

Of note

  1. asynchronous exceptions are…​tricksy

  2. An exception is a result!

  3. See 1

async doesn’t make a method asynchronous

actually, await is where any asynchrony happens

To be continued…​

await 1

To be continued…​

await 2

To be continued…​

await 3

The (more detailed) answer is…​

The Synchronization Context decides

You’re covered

The future is Task

public async Task PersistState()    // <-- async Task
{
    await Task.Run(() =>
    {
        // long-running operation
        
        // ... that may fail
        throw new Exception("An arbitrary error");
    });
}

But…​

the exception is still not caught in the caller

async all the way down

public async void Flush(object? state)  // <-- async
{
    try
    {
        await PersistState();           // <-- await
        Console.WriteLine("Done");
    }
    catch // anything
    {
        Console.WriteLine("Error");
    }
}

But…​we’re back to async void!

Initialization (redux)

There is nothing more deceptive than an obvious fact.

The Boscomble Valley Mystery
— Sherlock Holmes

Not actually required

public readonly struct Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public DateOnly DOB { get; init; }
}    
var person = new Person
{
    FirstName = "John",
    LastName = "Doe"
};

Push left

public readonly struct Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateOnly DOB { get; init; }        
}    
var person = new Person
{
    FirstName = "John",
    LastName = "Doe"
};
Error CS9035 : Required member 'Person.DOB' must be set in the 
                object initializer or attribute constructor.

Nullable references

warn
var person = new Person
{
    FirstName = "John",
    LastName = default,
    DOB = default
};
[CS8625] Cannot convert null literal to non-nullable reference type.

…​but silent on the default DOB

More default

Person person = default;
Consider making this a sin!

Constructors

public readonly struct Person
{
    public Person(string firstName, string lastName, DateOnly dob)
        => (FirstName, LastName, DOB) = (firstName, lastName, dob);
    
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateOnly DOB { get; init; }
}
error
var person = new Person("John", "Doe", new DateOnly(2010, 12, 31));
[CS9035] Required member 'Person.FirstName' must be set 
        in the object initializer or attribute constructor.

Trust me…​

public readonly struct Person
{
    [SetsRequiredMembers]
    public Person(string firstName, string lastName, DateOnly dob)
        => (FirstName, LastName, DOB) = (firstName, lastName, dob);
    
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateOnly DOB { get; init; }
    public required string Pronoun { get; init; }
}
pass
var person = new Person("John", "Doe", new DateOnly(2010, 12, 31));

Should this be ok?

Wrong problem?

public readonly struct Person
{
    public Person(string firstName, string lastName, DateOnly dob, string pronoun)
        => (FirstName, LastName, DOB, Pronoun) = (firstName, lastName, dob, pronoun);
    
    public string FirstName { get; }
    public string LastName { get; }
    public DateOnly DOB { get; }
    public string Pronoun { get; }
}
error
var person = new Person { FirstName = "John", LastName = "Doe" };
[CS0200] Property or indexer 'Test_construct.Person.FirstName' 
            cannot be assigned to -- it is read only

Type safety

We must look for consistency. Where there is a want of it we must suspect deception.

The Problem of Thor Bridge
— Sherlock Holmes

Unrelated value

public class Converter(char from)
{
    public static explicit operator Converter(char value) 
        => new(value);
}
decimal money = 24.5m + 11.5m;
var result = (Converter)money;

Does this compile?

Should it?

Operators are methods too

decimal money = 24.5m + 11.5m;
var result = (Converter)money;

is equivalent to this:

var result = Converter.op_Explicit(money);

But user-defined operators are special

public class Converter(char from)
{
    public static explicit operator Converter(char value) 
        => new(value);

    public static Converter From(char value) 
        => new(value);
}
error
var result = Converter.From(money);
error
var result = Converter.From((char)money);

More minutiae

decimal money = 24.5m + 11.5m;
var result = (Converter)money;
IL_0012: call char System.Decimal::op_Explicit(valuetype System.Decimal)
IL_0017: call class Converter Converter::op_Explicit(char)

Yet more canon

  • First, if required, performing a standard conversion from the source expression to the operand type of the user-defined or lifted conversion operator.

  • Next, invoking the user-defined or lifted conversion operator to perform the conversion.

  • Finally, if required, performing a standard conversion from the result type of the user-defined conversion operator to the target type.

Language legalese

It is, of course, a trifle, but there is nothing so important as trifles.

The Man with the Twisted Lip
— Sherlock Holmes

Thank you for listening!