Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ValueUnion

C# 15 has finally added discrimminated unions! However, using a struct as a union case type results in boxing, with manual implementations requiring a lot of boilerplate.

This library autogenerates that boilerplate so it's easier to use unions without allocating or errors. Simply add the [Union<>] attribute and mark your union struct as partial.

using ValueUnion;

CustomerId id = Guid.NewGuid();

string formatted = id switch
{
    int value => $"legacy:{value}",
    Guid value => $"modern:{value:N}"
};

[Union<int, Guid>]
partial struct CustomerId;
Source generated code
[Union]
[StructLayout(LayoutKind.Auto)]
partial struct CustomerId : IUnion
{
    private readonly byte _tag;
    private readonly Impl _inner;
    
    [StructLayout(LayoutKind.Explicit)]
    private struct Impl
    {
        [FieldOffset(0)]
        internal int Value1;
        [FieldOffset(0)]
        internal Guid Value2;
    }
    
    public CustomerId(int? value)
    {
        if (value is int v)
        {
            _inner.Value1 = v;
            _tag = 1;
        }
    }

    public CustomerId(Guid? value)
    {
        if (value is Guid v)
        {
            _inner.Value2 = v;
            _tag = 2;
        }
    }
    
    public readonly bool HasValue => _tag != 0;
    
    public readonly object? Value => _tag switch
    {
        1 => _inner.Value1,
        2 => _inner.Value2,
        _ => null,
    };
    
    public readonly bool TryGetValue(out int value)
    {
        value = _inner.Value1;
        return _tag == 1;
    }

    public readonly bool TryGetValue(out Guid value)
    {
        value = _inner.Value2;
        return _tag == 2;
    }
}

Installation

Warning

Unions are still in preview and may still change.

dotnet add package ValueUnion

Usage

Simply apply the [Union<>] attribute to a partial struct declaration. You can add up to 8 generic parameters for up to 8 possible types within a union. Types can be struct, primitives, classes, anything e.g. [Union<double, string, int, long>]. When all union cases are unmanaged, the union's cases are overlapped to save memory (this cannot be done if case types are managed due to the GC).

By default, a union also has a null case. Using the union example above, a default(CustomerId) has no active case and may warn if the null case is unhandled. You can get around this by setting a type to use in the default case with the Default property. If [Union<int, Guid>(Default = typeof(int))] was used instead, a default(CustomerId) would have case int with value 0.

CustomerId defaultId = default;

Debug.Assert(defaultId switch
{
    // case is int, as it is default case
    int => true,
    Guid => false,
});

[Union<int, Guid>(Default = typeof(int))]
partial struct CustomerId;

About

Union source generator for C#.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages