Search code examples
c#typedef

How to create ranges and synonym constants in C#?


I've just started working in C#, and I would like to define a new type (Priority) as an integer, going from 1 to 9. In top of this, I would like to create three new constants:

pri_Low,     which has value 1
pri_Default, which has value 5
pri_High,    which has value 9

I though of doing something like this: (C-style)

typedef TPriority = 1..9;
Const TPriority pri_Low     1;
Const TPriority pri_Default 5;
Const TPriority pri_High    9;

But when I look for this on the internet, I get answers like "You need to create a class, and you need to declare it static, and ...".

My first reaction is "Hold your horses. I just want to create a simple range of numbers and give a meaning to three of them. No classes, no constructors, no static, public, private, friend or whatever fancy things, just simple basics.", is this even possible or is C# that into "Everything is a class" that such simple things are not even allowed?


Solution

  • You need to create a ... No, you don't need to do anything.

    You can, however, implement a relatively simple type. I say relatively because on first glance it will look like a lot of code, but the code itself is really simple.

    And yes, coming from Object Pascal / Delphi, which has the 1..9 types, I really miss those types, but sadly they don't exist in C# or .NET.

    In C# 8, we got "ranges", but they're not types, they're just values.

    Now, on the simple and naive end you can use an enum:

    public enum Priority
    {
        Low = 1,
        Default = 5,
        High = 9
    }
    

    However, you now don't have a priority for 4, and if you want to ask .NET to tell you if a value is valid or not, you need to have names for all valid values, so you would need:

    public enum Priority
    {
        P1 = 1, P2, P3, P4, P5, P6, P7, P8, P9,
        
        Low = P1,
        Default = P5,
        High = P9
    }
    

    Unfortunately, enums doesn't prevent you from storing invalid values, this is fine:

    Priority p = (Priority)-5;
    

    and any fields in a type that you don't explicitly assign a value will have the value (Priority)0 as default, not 5.

    So...

    If you want a type that actually doesn't accept values less than 1 or higher than 9, you have no recurse than to create one yourself, so here's a simple Priority type for the range 1..9:

    public struct Priority : IEquatable<Priority>, IEquatable<int>,
        IComparable<Priority>, IComparable<int>, IFormattable
    {
        private const int _lowPriority = 1;
        private const int _defaultPriority = 5;
        private const int _highPriority = 9;
        
        private readonly int _value;
        
        public Priority(int value)
        {
            if (value < _lowPriority || value > _highPriority)
                throw new ArgumentOutOfRangeException(nameof(value),
                    $"value must be in the range {_lowPriority}..{_highPriority}");
                
            _value = value - _defaultPriority;
        }
    
        // the trick with `+/- _defaultPriority` is to make sure
        // new Priority() is the same as new Priority(5)
        public int Value => _value + _defaultPriority;
    
        public static Priority Parse(string s)
            => Parse(s, NumberStyles.Integer, NumberFormatInfo.CurrentInfo);
        public static Priority Parse(string s, NumberStyles style,
            IFormatProvider provider)
        {
            if (TryParse(s, style, provider, out var priority))
                return priority;
            throw new FormatException($"Unable to parse priority '{s}'");
        }
        
        public static bool TryParse(string s, out Priority result)
            => TryParse(s, NumberStyles.Integer, NumberFormatInfo.CurrentInfo,
                   out result);
        public static bool TryParse(string s, NumberStyles style,
            IFormatProvider provider, out Priority result)
        {
            result = default;
            if (s is null)
                return false;
                
            var span = s.AsSpan();
            if (span.Length == 0 || span[0] != 'P')
                return false;
            span = span[1..];
            
            if (!int.TryParse(span, style, provider, out int value))
                return false;
                
            if (value < _lowPriority || value > _highPriority)
                return false;
                
            result = new Priority(value);
            return true;
        }
        
        public static readonly Priority Low = new Priority(_lowPriority);
        public static readonly Priority Default = new Priority(_defaultPriority);
        public static readonly Priority High = new Priority(_highPriority);
        
        public static implicit operator int(Priority priority) => priority.Value;
        public static explicit operator Priority(int value) => new Priority(value);
        
        public static bool operator ==(Priority a, int b) => a.Value == b;
        public static bool operator !=(Priority a, int b) => a.Value != b;
        public static bool operator <(Priority a, int b) => a.Value < b;
        public static bool operator >(Priority a, int b) => a.Value > b;
        public static bool operator <=(Priority a, int b) => a.Value <= b;
        public static bool operator >=(Priority a, int b) => a.Value >= b;
    
        public static bool operator ==(int a, Priority b) => a == b.Value;
        public static bool operator !=(int a, Priority b) => a != b.Value;
        public static bool operator <(int a, Priority b) => a < b.Value;
        public static bool operator >(int a, Priority b) => a > b.Value;
        public static bool operator <=(int a, Priority b) => a <= b.Value;
        public static bool operator >=(int a, Priority b) => a >= b.Value;
        
        public bool Equals(Priority other) => Value == other.Value;
        public bool Equals(int other) => Value == other;
        public override bool Equals(object obj) => obj is Priority other
            && Equals(other);
        public override int GetHashCode() => _value.GetHashCode();
        
        public int CompareTo(Priority other) => Value.CompareTo(other.Value);
        public int CompareTo(int other) => Value.CompareTo(other);
    
        public override string ToString() => $"P{Value}";
        public string ToString(string format, IFormatProvider formatProvider)
            => $"P{Value.ToString(format, formatProvider)}";
    }