No, obviously, you cannot do it this way. And I consider it as one of the (very few) fundamental .NET failures. It is certainly impossible in .NET, but, as you probably know, possible in some languages.
This is the fundamental limitation related to the fact that numeric types don't have common interface. Integer and floating-point numbers have different operation set, but even in similar types, say, of different size, the operations are different because they return different types.
In principle, there is the work-around, but it would be quite questionable, because it might require too much effort compared to the algorithm you want to abstract out of the operand type. Well, it depends on how valuable are those algorithms. Here is the idea.
First, you need to create an interface representing pretty much all the arithmetic operations of certain set of types, which can be numeric, or, say, complex type such as vectors, matrices, etc. This is how it may look:
interface INumericOperationSet<OPERAND> {
OPERAND Zero { get; }
OPERAND One { get; }
OPERAND Sum(OPERAND left, OPERAND right);
OPERAND Multiplication(OPERAND left, OPERAND right);
OPERAND Division(OPERAND left, OPERAND right);
}
Now let's assume you a agree to manually implement it for all the types you want to use. I'll show it only for two types, such as
class DoubleOperationSet : INumericOperationSet<double> {
public double Zero { get { return 0; } }
public double One { get { return 1; } }
public double Sum(double left, double right) { return left + right; }
public double Multiplication(double left, double right) {
return left + right; }
public double Division(double left, double right) { return left / right; }
}
class SingleOperationSet : INumericOperationSet<Single> {
public Single Zero { get { return 0; } }
public Single One { get { return 1; } }
public Single Sum(Single left, Single right) { return left + right; }
public Single Multiplication(Single left, Single right) {
return left + right; }
public Single Division(Single left, Single right) { return left / right; }
}
Now, you can implement some generic class using all the set of types at once. As you cannot provide constraints for the primitive type, you have to supply this type unconstrained, but with corresponding version of the type representing the operation set; and this type will be constraint to the operand type:
static class AlgorithmSet<OPERAND, OPERATION_SET> where OPERATION_SET : INumericOperationSet<OPERAND>, new() {
static OPERATION_SET operations = new OPERATION_SET();
internal static OPERANDSumOfElements(
System.Collections.Generic.IList<OPERAND> collection) {
OPERAND value = operations.Zero;
foreach(var element in collection)
value = operations.Sum(value, element);
return value;
}
}
The problem here is big number of operations you need to implement and repeat the implementations in the frustratingly dumb way. One little problem is the need to call of the operation set constructor (see
new OPERATION_SET()
in the last code fragment above), because the implemented interface methods are non-static.
The bigger problem is the difference between floating-point and integer values and the general problem: many operations are inapplicable to integers (
System.Math
is really only for floating-point values), and even less are applicable to unsigned integers (simply: you cannot really apply subtraction to unsigned integers, because the result can go outside of the value set). In practice, however, we can ignore such problems, because there are many operations which cannot be performed on the set of values: simply (checked) addition of two long integer value can overflow the result.
For one thing, it reduces the amount of work: there are not too many types you can really use, but from the other hand, it reduced the value of the approach. Personally, I am very skeptical about the applicability of this approach; it's hard for me to imaging that it would really worth the effort. But anyway, I answered your question. :-)
[EDIT]
Note that it really creates a kind of constraint for any types of generic parameters, including primitive types, which can be called "constraints to the fixed set of types. In other words, you can constraint some generic parameter by second generic parameters, so first generic parameter will be only of one type from some list, and the second one should match. Let's see how it can look in its pure case:
interface IConstraint<OPERAND> {}
class CharConstraint : IConstraint<char> {}
class IntConstraint : IConstraint<int> {}
class StringConstraint : IConstraint<string> {}
Now we can create any type of generic class which can only be instantiated only with any of the three types listed above, more exactly, only with any of the three pairs of types. For example, having the class with operand constraint
static class ConstraintUser<OPERAND, CONTRAINT>
where CONTRAINT : IConstraint<OPERAND> {
internal static string OperandToString(OPERAND operand) {
return operand.ToString(); }
}
we can have only three different instantiations of it to a complete type, as in
string stringRepresentaton =
ConstraintUser<char, CharConstraint>.OperandToString('a');
stringRepresentaton =
ConstraintUser<int, IntConstraint>.OperandToString(22);
stringRepresentaton =
ConstraintUser<string, StringConstraint>.OperandToString("Anything");
So, the problem is formally solved. But does it make any practical sense? Hardly too much. Here is why: what the types in the type set of the constraint we designed have in common? The answer: they are all derived from the CLR root type:
System.Object
, so inside the constrained generic class they can only use functionality of
System.Object
. But then, why not simply using no constraint and boxing where it is required?
Well, one can exercise one's fantasy to invent some applications. For example, limit some generic type like
ConstraintUser
to the two-byte characters, which is required by some specialized streams; or you can limit the types to the set used by some old non-generic API which should be wrapped around, something like that. Perhaps this is the case when just the demonstration of the principle possibility is more important than the applications.
—SA