21

I have a program that is interfacing with an external library that, among other things, has an unsigned 12-bit value packed in a larger struct.

This used to be 8 bits, so we simply marshaled it as a byte.

Now that it's 12 bits... I can use a ushort, but that opens up issues of (a) range checking and (b) marshaling.

Is there a simple way of implementing a constrained numeric type like this, where I don't have to override every assignment and comparison method?

Joe
  • 41,484
  • 20
  • 104
  • 125
  • Can you check the range just at the marshalling point? – David Heffernan Sep 30 '11 at 19:27
  • Unfortunately no; the c# side of these will be potentially creating them as well, so other parts of the code are assigning values to this member. – Joe Sep 30 '11 at 19:34
  • Why does that matter? Is it crucial that the exception is raised at the point of assignment? If so then you clearly need to write a wrapper class or struct. – David Heffernan Sep 30 '11 at 19:36
  • Well, yes. I was just hoping there was a simple wrapper that I could drop in as a datatype replacement for an integral type without overriding every operator, etc. – Joe Sep 30 '11 at 19:41

2 Answers2

31

Try this (this example shows a custom Int64 type)

public class MyCustomInt64 : CustomValueType<MyCustomInt64, Int64>
{
    private MyCustomInt64(long value) : base(value) {}        
    public static implicit operator MyCustomInt64(long value) { return new MyCustomInt64(value); }
    public static implicit operator long(MyCustomInt64 custom) { return custom._value; }
}

Implement what you like in the constructor to apply constriants.

Usage

MyCustomInt64 myInt = 27; //use as like any other value type

And here is the reusable base custom value type (add more operators if needed)

public class CustomValueType<TCustom, TValue>
{        
    protected readonly TValue _value;

    public CustomValueType(TValue value)
    {
        _value = value;
    }

    public override string ToString()
    {
        return _value.ToString();
    }

    public static bool operator <(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return Comparer<TValue>.Default.Compare(a._value, b._value) < 0;
    }

    public static bool operator >(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return !(a < b);
    }

    public static bool operator <=(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return (a < b) || (a == b);
    }

    public static bool operator >=(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return (a > b) || (a == b);
    }

    public static bool operator ==(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return a.Equals((object)b);
    }

    public static bool operator !=(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return !(a == b);
    }

    public static TCustom operator +(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return (dynamic) a._value + b._value;
    }

    public static TCustom operator -(CustomValueType<TCustom, TValue> a, CustomValueType<TCustom, TValue> b)
    {
        return ((dynamic) a._value - b._value);
    }

    protected bool Equals(CustomValueType<TCustom, TValue> other)
    {            
        return EqualityComparer<TValue>.Default.Equals(_value, other._value);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((CustomValueType<TCustom, TValue>)obj);
    }

    public override int GetHashCode()
    {
        return EqualityComparer<TValue>.Default.GetHashCode(_value);
    }
}
Jay Byford-Rew
  • 5,736
  • 1
  • 35
  • 36
  • 2
    This is fantastic! Like finding a hidden treasure, seriously good stuff! – Piotr Kula Nov 05 '15 at 22:19
  • 1
    I have not read it entire but the way you handle generic reusable solution is gold - just the thought of it – Demetris Leptos Nov 05 '17 at 14:54
  • since you've reached this far - you can add a step further with global static assignable logic (delegates) - I was planning on something of this sort and it would blend awesomely with your solution – Demetris Leptos Nov 05 '17 at 14:56
  • 3
    but make all this struct-ish - because we want the value type nature – Demetris Leptos Nov 05 '17 at 14:57
  • 1
    This is brilliant! Im yet to test it. Im also thinking about converting it into struct. One internet cookie for you. – W0lfw00ds Mar 20 '19 at 18:58
  • How does _value get initialised? I'm using this to replace ints - and am getting lots of "System.NullReferenceException" when using an array of MyCustomInt64. – Clare Macrae Feb 25 '21 at 15:42
18

You should create a struct that overrides the implicit conversion operator:

struct PackedValue {
    private PackedValue(ushort val) {
         if(val >= (1<<12)) throw new ArgumentOutOfRangeException("val");
         this._value = val;
    }
    private ushort _value;
    public static explicit operator PackedValue(ushort value) {
        return new PackedValue(value);
    }

    public static implicit operator ushort(PackedValue me) {
        return me._value;
    }
}
Jacob Krall
  • 28,341
  • 6
  • 66
  • 76
  • Thanks, those two operators were what I was looking for. – Joe Sep 30 '11 at 19:53
  • 1
    I think you meant `if (val >= 1 << 12)` ... `2 << 12` is equal to `2 * 2 ^ 12`, or `2 ^ 13`. – phoog Mar 16 '13 at 00:59
  • 1
    In my case this worked this way just as a class. For struct I had to add " : this()" for the constructor. – mikiqex May 09 '14 at 08:10
  • @mikiqex: I removed the autoproperty, as its backing field was causing the compilation error you saw, and it is not necessary for solving the problem. – Jacob Krall May 09 '14 at 14:49