Click here to Skip to main content
15,890,043 members
Please Sign up or sign in to vote.
5.00/5 (4 votes)
See more:
Is there a way to put constraints on generic types on method signatures?
e.g. something like:
C#
public T dosomething(T val1,T val2) where T : int, decimal

It seems it only takes class types not primitive types, is there a way around it?
Posted
Comments
Sergey Alexandrovich Kryukov 21-Jul-15 1:07am    
This is one of the known interesting and important questions, my 5. I answered in detail.
—SA

1 solution

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:
C#
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);
    // ... and a lot more
}

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
C#
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; }
    // ... and a lot more
}

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; }
    // ... and a lot more
}

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:
C#
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;
    }

    // ...
    // say, for ProductOfElements, you will need to use
    // INumericOperationSet.Multiplication and INumericOperationSet.One
    // and so on...

} //class AlgorithmSet


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:
C#
interface IConstraint<OPERAND> {}
// starting to create a list of types
class CharConstraint : IConstraint<char> {}
class IntConstraint : IConstraint<int> {}
class StringConstraint : IConstraint<string> {}
// list of types is complete

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
C#
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
C#
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
 
Share this answer
 
v11
Comments
Afzaal Ahmad Zeeshan 21-Jul-15 3:04am    
+5, excellent answer.

Anyways, I had a little ambiguity. C# is not only a language for .NET framework (only it was initially), instead it is being used on Windows Runtime also, so wouldn't this problem be with C# language itself or the IL it compiles into instead of .NET framework.
Sergey Alexandrovich Kryukov 21-Jul-15 3:28am    
Thank you, Afzaal.

I understand. Perhaps you refer to my use of the term .NET. In this context, perhaps I should have said "CLI". This is certainly not the problem of C# along, but rather Common Language Infrastructure. However, I wonder if you noticed that Microsoft terminology itself is full of inconsistencies.

At the same time, my statement about ".NET failures", if understood something differently, can still be considered as a valid judgement (questionable or not, is a matter of opinions, but terminologically valid). How? I'll explain. Even though you can view this problem as the problem going outside .NET, you can understand my use of the term ".NET" as cultural phenomenon. And, historically, .NET in the present form, in the aspects we are discussed, was the first product where this kind of CLI was implemented. If some would agree to see some failures in CLI, it can be considered as a failure of .NET, not something which appeared later. Isn't that logical?

—SA
Afzaal Ahmad Zeeshan 21-Jul-15 5:27am    
Yes, that is logical and does make sense. Thank you for clarification, Sergey. C# was created as a part of .NET framework as a managed language. Thus, .NET framework can be a reason for such behavior.
Sergey Alexandrovich Kryukov 21-Jul-15 8:24am    
You are welcome. I added the second part to the answer, discussing the principle of pure constraint.
—SA
Mehdi Gholam 21-Jul-15 8:42am    
5'ed

It seems you can have
int dosomething(int,int)
decimal dosomething(decimal,decimal)
Which goes part of the way and you are left to mangle the types inside the donsomething().

After thinking about it since generics are handled at the CLR level there is little chance the VM can "filter" the types before trying to get the method (unless it specifically checks) and you might get an exception method not found...

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900