C
C#3y ago
Thinker

Best way to do a discriminated union struct

I have a struct + enum that looks essentially like this.
public readonly struct Value
{
public string? StringValue { get; } = null;
public double? NumberValue { get; } = null;

public ValueKind Kind { get; }

public bool IsUndefined => Kind == ValueKind.Undefined;
[MemberNotNullWhen(true, nameof(StringValue))]
public bool IsString => Kind == ValueKind.String;
[MemberNotNulWhen(true, nameof(NumberValue))]
public bool IsNumber => Kind == ValueKind.Number;

public Value()
{
Kind = ValueKind.Undefined;
}
public Value(string value)
{
StringValue = value;
Kind = ValueKind.String;
}
public Value(double value)
{
NumberValue = value;
Kind = ValueKind.Number;
}
}

public enum ValueKind
{
Undefined,
String,
Number
}
public readonly struct Value
{
public string? StringValue { get; } = null;
public double? NumberValue { get; } = null;

public ValueKind Kind { get; }

public bool IsUndefined => Kind == ValueKind.Undefined;
[MemberNotNullWhen(true, nameof(StringValue))]
public bool IsString => Kind == ValueKind.String;
[MemberNotNulWhen(true, nameof(NumberValue))]
public bool IsNumber => Kind == ValueKind.Number;

public Value()
{
Kind = ValueKind.Undefined;
}
public Value(string value)
{
StringValue = value;
Kind = ValueKind.String;
}
public Value(double value)
{
NumberValue = value;
Kind = ValueKind.Number;
}
}

public enum ValueKind
{
Undefined,
String,
Number
}
Is there a better way to do this? Unfortunately I don't think it's possible to do the MemberNotNullWhen thing but with just the enum.
23 Replies
TheBoxyBear
TheBoxyBear3y ago
The enum value can't be null anyway
Thinker
ThinkerOP3y ago
I mean it would be ideal if something like [MemberNotNullWhen(ValueKind.String, nameof(StringValue))] was possible
ero
ero3y ago
That attribute is missing some crucial overloads
TheBoxyBear
TheBoxyBear3y ago
You could also consider throwing an exception instead and making the values non nullable Would avoid storing value types as references
Thinker
ThinkerOP3y ago
True wdym?
TheBoxyBear
TheBoxyBear3y ago
Like decimal?
Thinker
ThinkerOP3y ago
Where is it a reference?
TheBoxyBear
TheBoxyBear3y ago
You could have it decimal with the default value and throw an exception on get if the type doesn't match My bad Nullable is a struct Keep getting that confused
ero
ero3y ago
Doesn't making it nullable make it a reference type by default
Thinker
ThinkerOP3y ago
yes no?
ero
ero3y ago
Because it becomes Nullable<double>
TheBoxyBear
TheBoxyBear3y ago
Nullable is a struct that spoofs nullability
ero
ero3y ago
Oh it's a struct? Hm
TheBoxyBear
TheBoxyBear3y ago
If it were truly null, you couldn't use HasValue
ero
ero3y ago
Mh
TheBoxyBear
TheBoxyBear3y ago
It just stores its null state internally and spoofs all checks But Nullable does the same thing of throwing an exception when you get the value if it's meant to be null
Thinker
ThinkerOP3y ago
Yeah I guess I could do that Doesn't really make the entire thing any prettier but eh
TheBoxyBear
TheBoxyBear3y ago
Won't have null warnings Could also recommend a way of getting the value as object. Sure you have boxxing so not ideal but is say you need to pass it to a method that takes object anyway then no loss and you avoid some boiletplate
Thinker
ThinkerOP3y ago
I would like to avoid boxing
Anton
Anton3y ago
Don't use nullables for the fields, make a bunch of TryGet for those instead. You could make a match method, if you are ok with allocating a few delegates for a nicer ux. For each of the things it can be, I say you should do getters that either assert or throw if the kind doesn't match. don't use nullables for fields, you already have that info in the ValueKind, plus it's one extra bool for value types. You can do a fixed buffer if you really need to conserve memory, or explicit struct layouts. I personally don't see why the struct should be readonly, I'd just mark the getters readonly The thing with disciminated unions in C# is that they are clunky if done generically, and Match would need allocations for the delegates. Code generation with these is a good idea Or use a package
Anton
Anton3y ago
GitHub
GitHub - mcintyre321/OneOf: Easy to use F#-like ~discriminated~ uni...
Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time matching - GitHub - mcintyre321/OneOf: Easy to use F#-like ~discriminated~ unions for C# with exhaustive compile time ...
Anton
Anton3y ago
(although I think their type is a class, which is a total letdown)
Thinker
ThinkerOP3y ago
I was planning on using explicit struct layout anyway, so all the fields for the values wouldn't take up additional space. Asserting that the kind is correct seems like a good option. For context I'm making a runtime for a dumb interpreted language, and it doesn't need to be super efficient, but I still want to be weary of memory and speed.

Did you find this page helpful?