Enum types

Learn how to define enumerations in Slice.

An enumeration type or enum type is a user-defined type that can take two forms:

A basic enum holds a set of named constants called enumerators, and is similar to C++ and C# enums.

A basic enum always specifies an underlying type. This underlying type can be any integral type: both fixed size and variable size integral types are accepted. The following example shows how to define a basic enum named Fruit with three enumerators: Apple, Pear, and Orange:

slice
enum Fruit : uint8 { Apple, Pear, Orange }

An enum with no underlying type is called a variant enum, and its members are called variants. A variant may define one or more fields (no field is also acceptable). A variant enum is a discriminated union, just like enums in Rust and enums with associated values in Swift.

For example:

slice
enum Shape { // no underlying type
Circle(radius: uint32)
Rectangle(width: uint32, length: uint32)
Dot // a variant without any fields
}

The enumerators of a basic enum are integral constants. The default behavior of the Slice compiler is to assign values to these enumerators automatically. The first enumerator is assigned a value of 0, and subsequent enumerators are assigned increasing values. The range of these enumerator values is the range of the underlying type.

The following code example illustrates how you can explicitly assign values for the enumerators in your basic enum type. In this example, the Apple enumerator is assigned a value of 1, the Pear enumerator is assigned a value of 5, and the Orange enumerator gets automatically a value of 6 (the value of the preceding enumerator plus 1).

slice
enum Fruit : uint8 {
Apple = 1
Pear = 5
Orange
}

The variants of a variant enum are not constants; you can think of them as instances of nested structs.

And just like the fields of a struct, you can include tagged fields unless the enclosing enum is marked compact. For example:

slice
// Fields can be tagged unless the enum if marked compact
enum FlagColor {
Red(tag(1) code: uint16?)
White
Blue(shade: string, tag(1) code: uint16?)
}
// A compact enum does not accept tagged fields
compact enum LaunchResult {
Success(speed: float32)
Failure(message: string, errorCode: int32)
}

You can also assign a numeric value to each variant. These numeric values are called discriminants and are used to encode and decode the variant enum.

For example:

slice
enum Shape {
Circle(radius: uint32) // discriminant is 0
Rectangle(width: uint32, length: uint32) = 3 // discriminant is 3
Dot // discriminant is 4
}

The range of these discriminants is 0 to 2,147,483,647 (int32 max).

By default, when the code generated by the Slice code generator decodes an instance of an enum, it makes sure this instance corresponds to a known enumerator or variant. This is the "checked" behavior: the decoding fails for a value with no matching enumerator or variant.

You can also get the opposite behavior—unchecked—by prepending unchecked to your enum definition. For example:

slice
unchecked enum ErrorCode : varuint62 {
NotFound
NotAuthorized
}

Since ErrorCode is marked unchecked, the generated code will successfully decode an integral value without a matching enumerator.

A checked enum must have at least one enumerator, while an unchecked enum may have no enumerator at all. For example, the following basic enum type is valid:

slice
// Same range as int16
unchecked enum MyInt16 : int16 {}

When the generated code decodes a variant enum and receives an unknown variant, it returns a special variant that holds the undecodable bytes received from the peer.

An unchecked variant enum cannot be marked compact. As a result, you can always use tagged fields in the variants of an unchecked variant enum.

A basic enum maps to an internal C# enumeration with the same name, and each Slice enumerator maps to a C# enumerator with the same name. For example:

slice
enum Fruit : uint8 { Apple, Pear, Orange }
C#
internal enum Fruit : byte
{
Apple = 0,
Pear = 1,
Orange = 2
}

The underlying type of the mapped enum type is always the mapped type for the Slice underlying type. For example, a Slice uint8 corresponds to a C# byte.

C# does not provide a native discriminated union type. This mapping relies on an emulation based on record classes proposed a few years ago. Briefly, a Slice variant enum maps to a C# partial record class with the same name, and each variant maps to a public nested record class with the same name as the variant. This nested record class derives from the enclosing "enum type" record class.

The Slice code generator adds the attribute [Dunet.Union] to the base record class, which instructs the Dunet source generator to generate additional helper code for these partial records.

For example:

slice
enum Shape {
Circle(radius: uint32)
Rectangle(width: uint32, length: uint32)
Dot
}

maps to:

C#
[Dunet.Union]
internal abstract partial record class Shape
{
public partial record Circle(uint Radius) : Shape;
public partial record Rectangle(uint Width, uint Length) : Shape;
public partial record Dot : Shape;
}

ZeroC.Slice.Codec has a dependency on the Dunet package; as a result, you don't need an explicit reference to Dunet in your project.

The Slice code generator generates extension methods to encode and decode instances of each enum:

  • EncodeName to encode an enum instance
  • DecodeName to decode an enum instance

With our Fruit example, we get:

C#
internal static class FruitSliceEncoderExtensions
{
internal static void EncodeFruit(this ref SliceEncoder encoder, Fruit value) => ...
}
internal static class FruitSliceDecoderExtensions
{
internal static Fruit DecodeFruit(this ref SliceDecoder decoder) => ...
}

For basic enums, the C# code generator also generates an extension method AsName to convert a value of the underlying type into an enumerator.

With our Fruit example:

C#
internal static class FruitByteExtensions
{
internal static Fruit AsFruit(this byte value) => ...
}

This conversion fails and throws InvalidDataException when the value does not correspond to any enumerator of the (checked) enum.

The mapped C# API for checked and unchecked enums is the same, with one exception: the mapping for an unchecked variant enum has an additional nested record class named Unknown that holds unknown variant values. For example:

slice
unchecked enum Shape {
Circle(radius: uint32)
Dot
}

maps to:

C#
[Dunet.Union]
internal abstract partial record class Shape
{
public partial record Circle(uint Radius) : Shape;
public partial record Dot : Shape;
// Extra variant when mapping an unchecked variant enum
public partial record Unknown(int Discriminant, ReadOnlyMemory<byte> Fields) : Shape;
}

This Unknown variant can be re-encoded later without losing any information.

The cs::attribute attribute adds the specified C# attribute to the mapped C# enum. You typically use it to add the FlagsAttribute to the mapped C# enum. For example:

slice
[cs::attribute("Flags")]
enum MultiHue : uint8 {
None = 0,
Black = 1,
Red = 2,
Green = 4,
Blue = 8
}
C#
[Flags]
internal enum MultiHue : byte
{
None = 0,
Black = 1,
Red = 2,
Green = 4,
Blue = 8,
}

You can also apply cs::attribute to an enumerator or variant to get the specified C# attribute on the mapped C# enumerator or variant.

Was this page helpful?

CookiesYour privacy
This website uses cookies to analyze traffic and improve your experience.
By clicking "Accept," you consent to the use of these cookies. You can learn more about our cookies policy in our Privacy Policy.