Smart enums, inheritance and "siblings" behaving weirdly and I don't understand why.

So, I'm using Ardalis.SmartEnum to create some smart enums with data members. I found myself having a lot of boilerplate, so I came up with the following code:
using Ardalis.SmartEnum;

namespace AetherLogger.Models;

public interface IModeInterface : ISmartEnum
{
string ReadableName { get; }
}

public abstract class ModeBase<TEnum> : SmartEnum<TEnum, int>, IModeInterface where TEnum : SmartEnum<TEnum, int>
{
protected ModeBase(string name, string? readableName = null) : base(name, ++_counter)
{
ReadableName = readableName ?? name;
}

private static int _counter;
public string ReadableName { get; }
}

public sealed class Mode : ModeBase<Mode>
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);

public IEnumerable<IModeInterface>? Submodes { get; }

private Mode(string name, string? readableName = null, IEnumerable<IModeInterface>? submodes = null) : base(name, readableName)
{
Submodes = submodes;
}

public Mode(string name, IEnumerable<IModeInterface> submodes) : this(name, name, submodes)
{
}
}


public sealed class ChipSubMode(string name, string? readableName = null) : ModeBase<ChipSubMode>(name, readableName)
{
public static readonly ChipSubMode CHIP64 = new(nameof(CHIP64), "CHIP 64");
public static readonly ChipSubMode CHIP128 = new(nameof(CHIP128), "CHIP 128");
}

public sealed class CwSubMode(string name, string? readableName = null) : ModeBase<CwSubMode>(name, readableName)
{
public static readonly CwSubMode PCW = new(nameof(PCW), "Precision CW");
}
using Ardalis.SmartEnum;

namespace AetherLogger.Models;

public interface IModeInterface : ISmartEnum
{
string ReadableName { get; }
}

public abstract class ModeBase<TEnum> : SmartEnum<TEnum, int>, IModeInterface where TEnum : SmartEnum<TEnum, int>
{
protected ModeBase(string name, string? readableName = null) : base(name, ++_counter)
{
ReadableName = readableName ?? name;
}

private static int _counter;
public string ReadableName { get; }
}

public sealed class Mode : ModeBase<Mode>
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);

public IEnumerable<IModeInterface>? Submodes { get; }

private Mode(string name, string? readableName = null, IEnumerable<IModeInterface>? submodes = null) : base(name, readableName)
{
Submodes = submodes;
}

public Mode(string name, IEnumerable<IModeInterface> submodes) : this(name, name, submodes)
{
}
}


public sealed class ChipSubMode(string name, string? readableName = null) : ModeBase<ChipSubMode>(name, readableName)
{
public static readonly ChipSubMode CHIP64 = new(nameof(CHIP64), "CHIP 64");
public static readonly ChipSubMode CHIP128 = new(nameof(CHIP128), "CHIP 128");
}

public sealed class CwSubMode(string name, string? readableName = null) : ModeBase<CwSubMode>(name, readableName)
{
public static readonly CwSubMode PCW = new(nameof(PCW), "Precision CW");
}
This code works. However, note ModeBase<TEnum> : SmartEnum<TEnum, int>. What I found is that this code doesn't work if I don't template ModelBase and I don't know why. [continues in the comments]
5 Replies
LordKalma (CT7ALW)
LordKalma (CT7ALW)OP12mo ago
This does not work:
using Ardalis.SmartEnum;

namespace AetherLogger.Models;

public interface IModeInterface : ISmartEnum
{
string ReadableName { get; }
}

public abstract class ModeBase : SmartEnum<ModeBase>, IModeInterface
{
protected ModeBase(string name, string? readableName = null) : base(name, ++_counter)
{
ReadableName = readableName ?? name;
}

private static int _counter;
public string ReadableName { get; }
}

public sealed class Mode : ModeBase
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);

public IEnumerable<IModeInterface>? Submodes { get; }

private Mode(string name, string? readableName = null, IEnumerable<IModeInterface>? submodes = null) : base(name, readableName)
{
Submodes = submodes;
}

public Mode(string name, IEnumerable<IModeInterface> submodes) : this(name, name, submodes)
{
}
}


public sealed class ChipSubMode(string name, string? readableName = null) : ModeBase(name, readableName)
{
public static readonly ChipSubMode CHIP64 = new(nameof(CHIP64), "CHIP 64");
public static readonly ChipSubMode CHIP128 = new(nameof(CHIP128), "CHIP 128");
}

public sealed class CwSubMode(string name, string? readableName = null) : ModeBase(name, readableName)
{
public static readonly CwSubMode PCW = new(nameof(PCW), "Precision CW");
}
using Ardalis.SmartEnum;

namespace AetherLogger.Models;

public interface IModeInterface : ISmartEnum
{
string ReadableName { get; }
}

public abstract class ModeBase : SmartEnum<ModeBase>, IModeInterface
{
protected ModeBase(string name, string? readableName = null) : base(name, ++_counter)
{
ReadableName = readableName ?? name;
}

private static int _counter;
public string ReadableName { get; }
}

public sealed class Mode : ModeBase
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);

public IEnumerable<IModeInterface>? Submodes { get; }

private Mode(string name, string? readableName = null, IEnumerable<IModeInterface>? submodes = null) : base(name, readableName)
{
Submodes = submodes;
}

public Mode(string name, IEnumerable<IModeInterface> submodes) : this(name, name, submodes)
{
}
}


public sealed class ChipSubMode(string name, string? readableName = null) : ModeBase(name, readableName)
{
public static readonly ChipSubMode CHIP64 = new(nameof(CHIP64), "CHIP 64");
public static readonly ChipSubMode CHIP128 = new(nameof(CHIP128), "CHIP 128");
}

public sealed class CwSubMode(string name, string? readableName = null) : ModeBase(name, readableName)
{
public static readonly CwSubMode PCW = new(nameof(PCW), "Precision CW");
}
(note that now we have ModeBase : SmartEnum<ModeBase>, IModeInterface)
LordKalma (CT7ALW)
LordKalma (CT7ALW)OP12mo ago
Even Visual Studio shows me a weird hint that ChipSubMode. and CwSubMode. are not needed before List.
No description
LordKalma (CT7ALW)
LordKalma (CT7ALW)OP12mo ago
Doing this the following test is completly broken:
[TestFixture]
public class ModeTests
{
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes?.First().Name;
var pcw = Mode.CW.Submodes?.First().Name;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.EqualTo("CHIP64"));
Assert.That(pcw, Is.EqualTo("PCW"));
});
}
}
[TestFixture]
public class ModeTests
{
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes?.First().Name;
var pcw = Mode.CW.Submodes?.First().Name;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.EqualTo("CHIP64"));
Assert.That(pcw, Is.EqualTo("PCW"));
});
}
}
With the error:
System.TypeInitializationException : The type initializer for 'AetherLogger.Models.Mode' threw an exception. ----> System.NullReferenceException : Object reference not set to an instance of an object.
And I don't know which bit of the language I don't understand that causes this error. I would like to learn. Thank you.
Message:  System.TypeInitializationException : The type initializer for 'AetherLogger.Models.Mode' threw an exception. ----> System.NullReferenceException : Object reference not set to an instance of an object. Stack Trace:  ModeTests.DifferentModesHaveDifferentSubModes() line 9 RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) --NullReferenceException <>c.<GetAllOptions>b4_2(TEnum t) EnumerableSorter2.ComputeKeys(TElement[] elements, Int32 count) EnumerableSorter1.ComputeMap(TElement[] elements, Int32 count) EnumerableSorter1.Sort(TElement[] elements, Int32 count) OrderedEnumerable1.Fill(Buffer1 buffer, Span1 destination) OrderedEnumerable1.ToArray() SmartEnum2.GetAllOptions() Lazy1.ViaFactory(LazyThreadSafetyMode mode) Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) Lazy`1.CreateValue() <.cctor>b36_0() Lazy1.ViaFactory(LazyThreadSafetyMode mode) Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) Lazy1.CreateValue() SmartEnum2.get_List() Mode.cctor() line 23
With
public sealed class Mode : ModeBase
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
// public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);
public sealed class Mode : ModeBase
{
public static readonly Mode CHIP = new(nameof(CHIP), ChipSubMode.List);
// public static readonly Mode CW = new(nameof(CW), "Morse (CW)", CwSubMode.List);
and
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes?.First().Name;
// var pcw = Mode.CW.Submodes?.First().Name;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.EqualTo("CHIP64"));
// Assert.That(pcw, Is.EqualTo("PCW"));
});
}
}
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes?.First().Name;
// var pcw = Mode.CW.Submodes?.First().Name;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.EqualTo("CHIP64"));
// Assert.That(pcw, Is.EqualTo("PCW"));
});
}
}
The test still fails
Message:  System.TypeInitializationException : The type initializer for 'AetherLogger.Models.Mode' threw an exception. ----> System.NullReferenceException : Object reference not set to an instance of an object. Stack Trace:  ModeTests.DifferentModesHaveDifferentSubModes() line 9 RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) --NullReferenceException <>c.<GetAllOptions>b4_2(TEnum t) EnumerableSorter2.ComputeKeys(TElement[] elements, Int32 count) EnumerableSorter1.ComputeMap(TElement[] elements, Int32 count) EnumerableSorter1.Sort(TElement[] elements, Int32 count) OrderedEnumerable1.Fill(Buffer1 buffer, Span1 destination) OrderedEnumerable1.ToArray() SmartEnum2.GetAllOptions() Lazy1.ViaFactory(LazyThreadSafetyMode mode) Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) Lazy`1.CreateValue() <.cctor>b36_0() Lazy1.ViaFactory(LazyThreadSafetyMode mode) Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) Lazy1.CreateValue() SmartEnum2.get_List() Mode.cctor() line 23
I also thought that probably a single member it should work, but apparently not?
CHIP = new(nameof(CHIP), null);
CHIP = new(nameof(CHIP), null);
and
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.Null);
});
}
[Test]
public void DifferentModesHaveDifferentSubModes()
{
var chip64 = Mode.CHIP.Submodes;
Assert.Multiple(() =>
{
Assert.That(chip64, Is.Null);
});
}
Does solve it, yeah Thanks for the deep dive! Wow! What amazes me the most after all of this is that the static analyser picked up that the .List` would be the same This looks like it makes some use of reflection, so having all of that in the static analyser is kind of amazing
MODiX
MODiX12mo ago
Zeth
REPL Result: Success
class L<T> { public static int K; }

L<int>.K = 3;

(L<int>.K, L<string>.K)
class L<T> { public static int K; }

L<int>.K = 3;

(L<int>.K, L<string>.K)
Result: ValueTuple<int, int>
{
"item1": 3,
"item2": 0
}
{
"item1": 3,
"item2": 0
}
Compile: 394.234ms | Execution: 39.682ms | React with ❌ to remove this embed.
LordKalma (CT7ALW)
LordKalma (CT7ALW)OP12mo ago
ah because List is a static member and that is shared when they aren't generic, got it VS recommended to just put List in there, no prefix yes that, sorry yes yes, that's why I'm saying, it was just "hey, this symbol is available from here" but yeah, seems that Ardalis.SmartEnum is a smartely crafted library pun totally intended thanks for the time for the deep dive!
Want results from more Discord servers?
Add your server