Description
Background and motivation
In many use cases, negative TimeSpan
values are invalid, so you will want to make them an error during parameter validation.
However, Timeout.Infinite
is an exceptional case.
If you want to perform parameter validation like this, you will need to write boilerplate code like the following.
void Foo(TimeSpan value)
{
if (value != Timeout.Infinite)
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
}
}
You should not write this as follows.
This is because there are negative values that are larger than Timeout.InfiniteTimeSpan
.
void Foo(TimeSpan value)
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, Timeout.InfiniteTimeSpan);
// Foo(TimeSpan.FromTicks(-1)) prints "-00:00:00.0000001: Valid".
Console.WriteLine($"{value}: Valid");
}
Also, ArgumentOutOfRangeException.ThrowIfNegative
cannot be used when comparing with Timeout.Zero
because TimeSpan
does not implement INumberBase<T>
.
But it is not a good idea to add a method specific to TimeSpan
validation to the ArgumentOutOfRangeException
class.
And, .NET timers support timeout values in milliseconds, and the valid range is from 1
to UInt32.MaxValue - 1
(0xfffffffe
). 0xffffffff
is not valid because it is Timeout.Infinite
.
It may be useful to have a method that checks whether or not the specified value is within this valid range.
For example, it can be used by someone implementing a class that uses timers internally and wants to check the range of values in the constructor.
So, we propose adding a validation methods to the TimeSpan
class.
Using these methods will make the meaning of the code clearer and also help you avoid errors in value validation.
It may also be a good idea to provide an analyzer that recommends specifications for methods like this.
API Proposal
namespace System;
public struct TimeSpan
{
// Throws `ArgumentOutOfRangeException` if the value is negative (even if it is `Timeout.InifiniteTimeSpan`).
public static void ThrowIfNegative(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws `ArgumentOutOfRangeException` if the value is negative (except `Timeout.InifiniteTimeSpan`).
public static void ThrowIfNegativeExceptInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws `ArgumentOutOfRangeException` if the value is negative or `TimeSpan.Zero` (even if it is `Timeout.InifiniteTimeSpan`).
public static void ThrowIfNegativeOrZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws `ArgumentOutOfRangeException` if the value is negative or `TimeSpan.Zero` (except `Timeout.InifiniteTimeSpan`).
public static void ThrowIfNegativeOrZeroExceptInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967294ms.
public static void ThrowIfOutOfTimeoutRange(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967294ms.
public static void ThrowIfOutOfTimeoutRangeAllowsZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967295ms.
public static void ThrowIfOutOfTimeoutRangeAllowsInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967295ms.
public static void ThrowIfOutOfTimeoutRangeAllowsZeroAndInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
}
API Usage
void Foo(TimeSpan value)
{
// `Foo(TimeSpan.FromTicks(-1))` will result in an error.
TimeSpan.ThrowIfNegativeExceptInfinite(value);
}
Alternative Designs
The timer itself can also have a method to check whether the value is within the valid range of the timer.
Since some of the timer implementations in .NET eventually end up with the internal TimerQueueTimer
class that implements ITimer
interface, this method could also be a static method of ITimer
.
The upper limit supported by the timer-related methods we have proposed is currently defined in the MaxSupportedTimeout
field of the System.Threading.Timer
class. The ITimer
interface was introduced later, along with the TimerProvider
.
public interface ITimer
{
// Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967294ms.
public static void ThrowIfOutOfTimeoutRange(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967294ms.
public static void ThrowIfOutOfTimeoutRangeAllowsZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967295ms.
public static void ThrowIfOutOfTimeoutRangeAllowsInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
// Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967295ms.
public static void ThrowIfOutOfTimeoutRangeAllowsZeroAndInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
}
System.Windows.Forms.Timer
and System.Windows.Threading.DispatcherTimer
are implemented using the Win32 API SetTimer
rather than TimerQueueTimer
, and in this case the upper limit is different.
Risks
No response