Custom structures as a means of validating values

Ah, if only users always understood the subject area and passed only valid data to our wonderful algorithms… But reality is merciless, and argument checks are a must. In this article, we will see how defining your own meaningful type can help with this.


I have already published this before big material concerning various aspects of .NET library development. I would like to open a series of notes on architecture with this article. In the mentioned text, this topic was devoted to literally several paragraphs, and I still believe that I have no right to give advice or teach the “correct” organization of code. However, there is an irresistible desire to touch upon some things that are interesting in my opinion.

There will be no further attempts to explain SOLID on your fingers or tales about the miracle of microservices. I will try to draw attention to less popular techniques and, ultimately, just interesting topics.

Let's start with validating values.

Table of contents

Problem

Close your eyes and imagine: in front of you is a variable, for example, like int or byteyou pass it to the method, the algorithm gently rustles its gears, creating a miracle, but… Then you wake up and remember that not always any number from the acceptable range of values ​​makes sense in a specific program logic, and therefore it needs to be validated.

In order not to rack my brains over an example, I will turn to my real experience for help. As you may know from my previous articles, the source of thoughts and materials for them is the library I am developing DryWetMIDIproviding a wide range of possibilities on working with MIDI on the .NET platform. Keeping in mind, as before, MIDI 1.0, I will note that in a huge number of places the standard implies the use of numbers from 0 before 127for example, to indicate the note number or the speed at which it is pressed (velocity).

There is no suitable type among the built-in types in .NET. The closest candidate is byteBut the range of acceptable values ​​for it is from 0 before 255. It turns out that we will have to check the number passed by the user to our API.

A simple solution

Without thinking twice, let's write at the beginning of an incomprehensible (but undoubtedly very useful) method HandleNote such code:

private static void HandleNote(byte noteNumber, byte velocity)
{
    if (noteNumber > 127)
        throw new ArgumentOutOfRangeException(nameof(noteNumber), noteNumber, "Invalid note number.");

    if (velocity > 127)
        throw new ArgumentOutOfRangeException(nameof(velocity), velocity, "Invalid velocity.");

    // ...
}

As stated earlier, such checks are needed wherever numbers in the range are expected 0-127. So when writing code you will have to crank up your attentiveness and patience to the max.

Yes, validation instructions can be packaged into utility methods, you can even include a third-party library with ready-made methods or one that uses the approach of adding special attributes to parameters. Will we write less code? Probably. Will this free us from having to remember to insert special instructions every time we deal with questionable data? No.

Custom structure

But what if, instead of checking arguments everywhere, you change the type of the input data to your own, within which the value will be checked?

public struct SevenBitNumber
{
    private byte _value;

    public SevenBitNumber(byte value)
    {
        if (value > 127)
            throw new ArgumentOutOfRangeException(
                nameof(value),
                value,
                "Value is out of range for seven-bit number.");

        _value = value;
    }

    public static implicit operator byte(SevenBitNumber number) =>
        number._value;

    public static explicit operator SevenBitNumber(byte number) =>
        new SevenBitNumber(number);
}

And then the old method takes on new forms:

private static void HandleNote(SevenBitNumber noteNumber, SevenBitNumber velocity)
{
    // ...
}

This approach solves two important problems:

  1. concentrate validation logic in one place;

  2. explicitly show the allowed values ​​via the type name.

What's interesting is that in all my time in the profession I've never encountered such a technique. Even when preparing the article I didn't find much information on the topic:

In the question on the first link, the person had another goal: to prevent semantic confusion when data circulates through the program. That is, with the same acceptable values, to prevent, for example, the area from being transferred where the length is expected. For me, such a bonus is a dubious thing, and if you seriously think about it, the code will be cluttered with types that only the author understands.

Returning to the tasks outlined above, it is worth admitting without unnecessary modesty that the solution looks good. There is no longer a need to check seven-bit numbers coming from the user everywhere, the new structure will do it for us.

But is everything so wonderful?

Minuses

If we received an instance at some stage of the program SevenBitNumberthen thanks to the implementation of the implicit conversion operator in byte this number will work:

var sevenBitNumber = GetSevenBitNumber();
int x = sevenBitNumber;

And here there will be a compilation error:

SevenBitNumber y = 100;

Transformation from byte V SevenBitNumber should be explicit (due to potential data loss and exception throwing):

SevenBitNumber y = (SevenBitNumber)100;

It turns out that values ​​always need to be explicitly cast to a custom structure. The price to pay for the ability to get rid of argument checks is more verbose code. It's unpleasant, but the advantages of the approach outweigh it, in my opinion.

Performance

What about performance? — you might ask. Common sense tells you that it shouldn't suffer noticeably. After all, .NET's built-in primitive types are structures too. Well, okay, byte inside SevenBitNumber it's a bit more than just structure Byte. So, it's time to create a clean project, install it there BenchmarkDotNet and take measurements.

Let's define the test subject:

public struct ByteWrapper
{
    private byte _value;

    public ByteWrapper(byte value)
    {
        if (value > 254)
            value = 254;

        _value = value;
    }

    public static implicit operator ByteWrapper(byte b) =>
        new ByteWrapper(b);

    public static implicit operator byte(ByteWrapper wrapper) =>
        wrapper._value;
}

Here everything is similar to the structure discussed above. SevenBitNumber: constructor, implicit cast operator to byte and obvious to ByteWrapper. Unless an exception is thrown in the body of the conditional operator in the constructor (it’s nice to break sometimes, but not your own program).

And, actually, the benchmarks:

public class Benchmarks
{
    private const int IterationsCount = 100000000;
    private const int OperationsPerInvoke = 10000;

    private static readonly Random _random = new();
            
    private byte _randomByte;
    private ByteWrapper _randomByteWrapper;

    [IterationSetup]
    public void PrepareRandomByte()
    {
        _randomByte = (byte)(_random.Next() & 0xFF);
        _randomByteWrapper = _randomByte;
    }

    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
    public byte CreateByte()
    {
        for (var i = 0; i < IterationsCount; i++)
            _randomByte += _randomByte;

        return _randomByte;
    }

    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
    public ByteWrapper CreateByteWrapper()
    {
        for (var i = 0; i < IterationsCount; i++)
            _randomByteWrapper += _randomByteWrapper;

        return _randomByteWrapper;
    }

    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
    public byte PassByteToMethod()
    {
        for (var i = 0; i < IterationsCount; i++)
            DoSomethingWithByte(_randomByte);

        return DoSomethingWithByte(_randomByte);
    }

    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
    public ByteWrapper PassByteWrapperToMethod()
    {
        for (var i = 0; i < IterationsCount; i++)
            DoSomethingWithByteWrapper(_randomByteWrapper);

        return DoSomethingWithByteWrapper(_randomByteWrapper);
    }

    private static byte DoSomethingWithByte(byte b) =>
        b++;

    private static ByteWrapper DoSomethingWithByteWrapper(ByteWrapper wrapper) =>
        wrapper++;
}

Here are applied Jedi techniques to eliminate compiler optimizations and zero results. Here's what I found:

Method

Average time (µs)

CreateByte

17.397

CreateByteWrapper

16.889

PassByteToMethod

3.421

PassByteWrapperToMethod

3.392

No performance drop was observed.

And although each of us can guess what the results will be, let's take measurements with this type:

public class ByteWrapperClass
{
    private byte _value;

    public ByteWrapperClass(byte value)
    {
        if (value > 254)
            value = 254;

        _value = value;
    }

    public static implicit operator ByteWrapperClass(byte b) =>
        new ByteWrapperClass(b);

    public static implicit operator byte(ByteWrapperClass wrapper) =>
        wrapper._value;
}

Yes, a class instead of a structure. I won't provide the benchmark code, everything is the same there, except that we'll include MemoryDiagnoser for more “beauty”:

Method

Average time (µs)

Memory allocated (B)

CreateByte

17.397

CreateByteWrapper

16.889

CreateByteWrapperClass

84.212

240000

PassByteToMethod

3.421

PassByteWrapperToMethod

3.392

PassByteWrapperClassToMethod

43.476

240000

Time has increased many times, and the memory in the heap has also started to be allocated.

By the way, at the beginning of the section there was this statement:

byte inside SevenBitNumber it's a bit more than just structure Byte

Or more?

var unmanagedSize = Marshal.SizeOf(typeof(ByteWrapper));
Console.WriteLine($"Unmanaged size = {unmanagedSize} byte(s)");

var managedSize = Unsafe.SizeOf<ByteWrapper>();
Console.WriteLine($"Managed size = {managedSize} byte(s)");

Console output:

Unmanaged size = 1 byte(s)
Managed size = 1 byte(s)

It turns out that the size of an instance of our new type, which is a wrapper over bytethe size of a byte and is equal.

Conclusion

At the end of this note, I would like to draw attention to the fact that the real structure will, of course, contain more code than just a field, a constructor, and a couple of type casting operators.

You will probably want to use such custom numbers in methods OrderBy() or List.Sort(). Then you will need to implement the interface IComparable. Also, methods from the class come in handy sometimes. Convert. In order to be able to pass a new type to them, you will have to implement the interface IConvertible. But there is no need to be afraid of this: in the implementations of the methods of the specified interfaces, you will simply need to call the same methods on the field around which the wrapper is built.

Also remember that for value types you always need to override the method Equalsif you don't want to have performance problems out of the blue (let me remind you that by default structures are compared via reflection, see How to define value equality for a class or struct).

Of course, wrapping all numbers in your code in your own types is a bad idea and can cause headaches. As with any architectural decisions, you need to know the limits and apply them with awareness of the consequences. DryWetMIDI has only two such structures – FourBitNumber And SevenBitNumberThey are used frequently, and therefore their presence is fully justified.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *