-2

How can I enforce that the variable of a class can be set within a particular numerical range? For example, in my sample code below, I would like to enforce that the user can only be able to set a value between 0.0f to 1.0f for the variable DownsamplingScale.

public sealed class DownsampleData
{
    public float DownsamplingScale = 0.0f;
}

I am not looking for the Clamping type solution because I really need to tell the class user that he/she must set a value within the range of 0.0f to 1.0f at the time they are using my class (writing code i.e. before compilation).

There are several ways to deal with the situation during runtime. I need something that informs during/before compilation about the expected range.

skm
  • 5,015
  • 8
  • 43
  • 104
  • How would you expect this to work at compile-time if the code that sets the field/property receives a `double` value as its *own* method parameter? Fundamentally, no, there's no way of doing this. – Jon Skeet Mar 05 '22 at 19:08
  • 1
    As long as you're fine with only being able to do this for constants, you can write your own custom Roslyn analyzer. But of course this will quickly fail for non-trivial scenarios like `downsampleData.DownsamplingScale = 1f; downsampleData.DownsamplingScale *= 2`. That sort of thing can't be detected at compile time and must be taken care of with properties. – Jeroen Mostert Mar 05 '22 at 19:10
  • 1
    Is it an option to use an integer type argument instead, which is scaled to the ranges you want? – Progman Mar 05 '22 at 19:14
  • For my interest, why at compile time, I mean the property setter check solution will stop their app once they try to run it – pm100 Mar 05 '22 at 19:17
  • @Progman: Any datatype will work (including custom struct/class) as long as the values are in the range of `0.0f` to `1.0f`. – skm Mar 05 '22 at 19:18
  • @pm100: If I already know the expected range, why shouldn't I stop someone beforehand instead of waiting for the app to throw error during runtime and then someone has to change code. – skm Mar 05 '22 at 19:21
  • Some languages allow you to define custom numeric types by applying ranges to existing types. C# does not (along with most other languages ultimately derived from C). You *can* define your own custom type that implements arithmetic through operator overloads and offers implicit conversions to/from `float` -- this would not prevent people from "doing it wrong" but it would be at least clearly signal to the user what is expected, and is more amenable to static analysis. A poor man's approach to this is a local alias (`using NormalizedValue = System.Single;`), but that's at best a hint. – Jeroen Mostert Mar 05 '22 at 19:25
  • https://github.com/Microsoft/CodeContracts has compile time checks, you could also add fxcop rules maybe – pm100 Mar 05 '22 at 19:50
  • @pm100: Code Contracts is effectively dead (the chief architect moved on to greener pastures and nobody picked up the slack), there is not and likely never will be a version compatible with .NET Core, for example. FxCop is effectively also dead, but because it's been superseded by Roslyn analyzers (which builds on the same basic idea and rules with a new architecture), already suggested in earlier comments. – Jeroen Mostert Mar 05 '22 at 20:26

2 Answers2

1

I need something that informs during/before compilation about the expected range.

It's not possible for the compiler to achieve that. The value might be read from a file and that file may not even exist at compile time, e.g.

var d = new DownsampleData();
d.DownsamplingScale(float.Parse(File.ReadAllText("file_on_customer_pc_only.dat")));
Thomas Weller
  • 55,411
  • 20
  • 125
  • 222
  • Don't we have the similar situation in case of `enums`. The values can be set from the provided options only. I basically need something like enum of values between 0.0f to 1.0f and looking for some smart way to do that. – skm Mar 05 '22 at 19:04
  • 1
    @skm: "The values can be set from the provided options only." - nope, you can cast any value of the underlying base type to the enum type. For example: `FileMode fm = (FileMode) 1123456;`. So yes, we have a similar situation - in that you can't rely on an enum value actually only being one of the named values. – Jon Skeet Mar 05 '22 at 19:08
  • @JonSkeet: Sure, I am just trying to clarify my requirement. For example, if we have an enum like this for color `enum Color {Red, Green}`, at least the user will come to know that he/she is expected to choose one of these two values at the time of writing the code. Similarly, I want to inform the person at the time of writing the code (i.e. before/during compilation). If someone really wants to break, its ok! By the way, the variable MUST NOT be of float type. For me, only the values (between `0.0f` to `1.0f`) matters . – skm Mar 05 '22 at 19:16
  • @skm: What if the user receives a parameter of type `Color`? They still don't know that it will be Red or Green, without additional validation. It sounds like you probably want to introduce your own immutable `struct` consisting of a `float` field that's validated on construction. But fundamentally if the value is read from disk somehow (as per Thomas's example) I still don't see how you'd expect that to be handled at compilation-time. *Only* handling compile-time constants is of very limited benefit IMO. – Jon Skeet Mar 05 '22 at 19:19
  • @JonSkeet For sure, I must put a runtime check as well and I will do that. During runtime, I can throw exception etc, I already know how to do that and therefore did not ask question about it. I don't know how can I "also" inform before/during compilation if someone is setting the hard-coded value (e.g. initialization). – skm Mar 05 '22 at 19:25
  • @skm: Your question doesn't say anything about "if someone is setting the hard-coded value". If you *really* only care about constants, you could write a Roslyn analyzer for that. But it will be *really* limited. – Jon Skeet Mar 06 '22 at 07:50
0

You can create a property (or method) where the argument is of an unsigned integer type. You pick the the actual type based on how much precision you need. If you use

public byte DownsamplingScaleFactor {get; set;}

You will have 256 values to choose from. From the documentation you make it clear that the value 0 means "0%" and 255 means "100%". It will not be possible to use any lower or higher value than that since the chosen type only supports these ranges.

With the type byte you will have a precision of 1/256 (ignoring off-by-one issues). If you need higher precision, use any other unsigned integer type like ushort, uint or ulong.

Progman
  • 16,827
  • 6
  • 33
  • 48
  • To play devil's advocate here: you could have much more precision if you used `float`, and indicated that the value `float.MinValue` means 0% and `float.MaxValue` means 100%. Problem solved! ...for some values of "solved". (Of course `float` is even *less* suitable than other types because precision decreases at the outer ends, but you get my point.) – Jeroen Mostert Mar 05 '22 at 19:35