A
Andreas Huber
What follows is a discussion of my experience with .NET generics & the
..NET framework (as implemented in the Visual Studio 2005 Beta 1),
which leads to questions as to why certain things are the way they
are.
***** Summary & Questions *****
In a nutshell, the current .NET generics & .NET framework make it
sometimes difficult or even impossible to write truly generic code.
For example, it seems to be impossible to write a truly generic
Complex class or a truly generic Matrix class (see below for details).
Since this seems to be due to a combination of shortcomings in both
the .NET framework and the generics mechanism, I have two questions:
1. Why does the overload resolution mechanism only consider overloads
with generic parameters when we call a function with a generic
argument? If it considered other overloads as well, this would allow
us to work around classes that do not implement the proper generic
interfaces (see below for an example).
2. Why do .NET framework classes for primitive types (Byte, Int32,
Decimal, Double, etc.) not implement an interface like IArithmetic< T
In isolation, neither of these limitations would be a big problem, as
we could easily work around them. However, their combination seems to
make it impossible to use generics for some tasks.
***** Discussion *****
Let us try to develop a generic Complex class in C# with generics. Our
(admitedly naive) first shot would be as follows:
public struct Complex< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator +(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
left.real + right.real, left.imag + right.imag );
}
// other operators
}
Trying to compile this yields the following errors:
complex.cs(24,7): error CS0019: Operator '+' cannot be applied to
operands of type 'T' and 'T'
complex.cs(24,31): error CS0019: Operator '+' cannot be applied to
operands of type 'T' and 'T'
which is fair enough, as we didn't specify any constraints for T.
However, exactly what constraint should we specify to make this work?
After all we want to use our Complex class with primitive types (e.g.
float, double) *and* user-defined types. To make it usable for the
latter is easy enough:
public interface IArithmetic< T >
{
T Add( T other );
// other operations
}
public struct Complex< T > where T : IArithmetic< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator +(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
left.real.Add( right.real ),
left.imag.Add( right.imag ) );
}
// other operators
}
So far so good, but what do we do to make this work for types that do
not implement our interface, namely framework-supplied ones like
float, double and perhaps decimal? All these types only implement the
interfaces IComparable, IFormattable, IConvertible and IComparable< T
public interface IArithmetic< T >
{
T Add( T other );
}
public struct Complex< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator+(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
Add( left.real, right.real ),
Add( left.imag, right.imag ) );
}
// other operators
static float Add( float left, float right )
{
return left + right;
}
// other overloads for double, etc.
// overload for all other types
static U Add< U >( U left, U right )
{
// implementation postponed
}
}
I was hoping that the two Add overloads would allow me to discriminate
between primitive, framework-supplied types and user-supplied types.
This does not work, the generic overload is also selected when I add
two Complex< float > objects.
I then tried various other ways to discriminate between types, but
none of them led to anything more interesting than the above. It seems
that when you call a function with an argument of generic type that
the parameter of the function accepting said argument inevitably also
needs to be a generic type. I.e. the second Add overload can take two
forms:
static T Add( T left, T right )
or
static U Add< U >( U left, U right )
It seems that no matter which form we choose, no other Add overloads
(e.g. for float, etc.) will ever be considered.
..NET framework (as implemented in the Visual Studio 2005 Beta 1),
which leads to questions as to why certain things are the way they
are.
***** Summary & Questions *****
In a nutshell, the current .NET generics & .NET framework make it
sometimes difficult or even impossible to write truly generic code.
For example, it seems to be impossible to write a truly generic
Complex class or a truly generic Matrix class (see below for details).
Since this seems to be due to a combination of shortcomings in both
the .NET framework and the generics mechanism, I have two questions:
1. Why does the overload resolution mechanism only consider overloads
with generic parameters when we call a function with a generic
argument? If it considered other overloads as well, this would allow
us to work around classes that do not implement the proper generic
interfaces (see below for an example).
2. Why do .NET framework classes for primitive types (Byte, Int32,
Decimal, Double, etc.) not implement an interface like IArithmetic< T
(see below for an example)?
In isolation, neither of these limitations would be a big problem, as
we could easily work around them. However, their combination seems to
make it impossible to use generics for some tasks.
***** Discussion *****
Let us try to develop a generic Complex class in C# with generics. Our
(admitedly naive) first shot would be as follows:
public struct Complex< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator +(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
left.real + right.real, left.imag + right.imag );
}
// other operators
}
Trying to compile this yields the following errors:
complex.cs(24,7): error CS0019: Operator '+' cannot be applied to
operands of type 'T' and 'T'
complex.cs(24,31): error CS0019: Operator '+' cannot be applied to
operands of type 'T' and 'T'
which is fair enough, as we didn't specify any constraints for T.
However, exactly what constraint should we specify to make this work?
After all we want to use our Complex class with primitive types (e.g.
float, double) *and* user-defined types. To make it usable for the
latter is easy enough:
public interface IArithmetic< T >
{
T Add( T other );
// other operations
}
public struct Complex< T > where T : IArithmetic< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator +(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
left.real.Add( right.real ),
left.imag.Add( right.imag ) );
}
// other operators
}
So far so good, but what do we do to make this work for types that do
not implement our interface, namely framework-supplied ones like
float, double and perhaps decimal? All these types only implement the
interfaces IComparable, IFormattable, IConvertible and IComparable< T
around, I came up with the following:. None of these helps us implementing Complex. After some fiddling
public interface IArithmetic< T >
{
T Add( T other );
}
public struct Complex< T >
{
T real;
T imag;
public Complex( T real, T imag )
{
this.real = real;
this.imag = imag;
}
public static Complex< T > operator+(
Complex< T > left, Complex< T > right )
{
return new Complex< T >(
Add( left.real, right.real ),
Add( left.imag, right.imag ) );
}
// other operators
static float Add( float left, float right )
{
return left + right;
}
// other overloads for double, etc.
// overload for all other types
static U Add< U >( U left, U right )
{
// implementation postponed
}
}
I was hoping that the two Add overloads would allow me to discriminate
between primitive, framework-supplied types and user-supplied types.
This does not work, the generic overload is also selected when I add
two Complex< float > objects.
I then tried various other ways to discriminate between types, but
none of them led to anything more interesting than the above. It seems
that when you call a function with an argument of generic type that
the parameter of the function accepting said argument inevitably also
needs to be a generic type. I.e. the second Add overload can take two
forms:
static T Add( T left, T right )
or
static U Add< U >( U left, U right )
It seems that no matter which form we choose, no other Add overloads
(e.g. for float, etc.) will ever be considered.