Narrow-If-Necessary and Clip-If-Necessary syntax and/or attributes

Topics: C# Language Design, VB Language Design
May 6, 2014 at 4:42 PM
Edited May 6, 2014 at 4:46 PM
It is not uncommon in many projects for types which use float and int to be replaced with ones that use double and long. Consequently, it would be desirable to write code in such a way as to work effectively with either kind of type or, at worst, in such a way that it could easily and reliably be written to work with either type. Unfortunately, C# and VB.NET presently require the same syntax in three different scenarios, two of which are common:
  1. Code needs to pass a value of one type to code that what the programmer believes to be a narrower type which can precisely hold the value.
  2. Code needs to pass a value of one type to code that the programmer believes to be a narrower type which is not expected be capable of precisely holding the value, but whose best representation would be acceptable substitute.
  3. Code needs to find the closest representation to a value in a particular type, even though the value may be given to something expecting a wider type.
I would expect that #1 is probably the most common usage of long to int typecasts, and #2 is probably the most common usage of double to float typecasts. Usage #3 is much rarer than #1 or #2.

I would suggest adding distinct syntax for #1 and #2, possibly with a couple of slight integer-specific variations. For C#, the syntax would be
  • (var)expr -- If expr would be coerced to a type for which an explicit cast exists but an implicit one does not, use the explicit cast; otherwise equivalent to expr.
  • (var clip)expr -- Require that expr and the destination type be numeric primitives; if expr is coerced to a numeric type that cannot hold its exact value, substitute the numerically-closest value (or infinity or NaN). Otherwise equivalent to (var)expr.
  • (var match)expr -- Require that expr and the destination type be numeric primitives; if the destination type cannot hold the exact value, throw an exception. Otherwise equivalent to (var)expr.
  • (var wrap) expr -- Require that expr be an integer type coerced to another; if the destination type cannot hold the exact value, use wrapping-integer semantics for the conversion.
Similar abilities are needed in VB.NET as well as C#, though I'm not sure the best syntax for the latter.

Such abilities would make it be possible to write code like:
T GetItem(long index) {
  if (index < 0 || index >= someCollection.LongCount()) // Could be extension method
    throw new ArgumentOutOfRangeException(...);
  return someCollection[(var match)index];
for use with with collections that are limited to int-sized indexing (for such collections, LongCount would invoke an extension method that chains to Count), and have such methods work correctly if a longer collection type is substituted. The cast could fail if for some reason a collection implements LongCount to return a value larger than Int32.MaxValue but only implements an int indexer, but throwing an overflow exception would be reasonable in that case. Likewise, graphics code:
void movePolar(double x, double y, double radius, double angle)
  radians = angle * Math.Pi / 180.0;
  drawSurface.MoveTo((var)(x+radius*Math.Cos(radians)), (var)(y-radius*Math.Sin(radians)),       
could be usable with a drawing environment that expects float, but make maximum precision available if code is refactored to use a drawing environment that expects double. Absent such a feature, it would be necessary for the last line to be written as:
  drawSurface.MoveTo((float)(x+radius*Math.Cos(radians)), (float)(y-radius*Math.Sin(radians)),       
Writing the code that way would complicate refactoring to use a drawing surface that expects double. If the casts aren't removed, the compiler would assume they were intended for usage #3 above, thus generating code that would waste time on needless and erroneous rounding. Because no diagnostic would be produced in such cases, and because the casts would use the same syntax as other semantically-meaningful casts, correct and reliable refactoring would be difficult. By contrast, code with (var) casts could be refactored simply by using a drawing surface that can accept double.

PS--If having a "cast" without specifying the destination type would be too difficult, an alternative would be to allow a method or property to indicate that its type is int or float because of an expectation that it will be passed to something that can't accept long or double, and that coercion to long or double would imply that the code was behaving contrary to intention and expectation and should thus flag a waning or error.