Why Does C# Forbid Generic Attribute Types

Why does C# forbid generic attribute types?

Well, I can't answer why it's not available, but I can confirm that it's not a CLI issue. The CLI spec doesn't mention it (as far as I can see) and if you use IL directly you can create a generic attribute. The part of the C# 3 spec that bans it - section 10.1.4 "Class base specification" doesn't give any justification.

The annotated ECMA C# 2 spec doesn't give any helpful information either, although it does provide an example of what's not allowed.

My copy of the annotated C# 3 spec should arrive tomorrow... I'll see if that gives any more information. Anyway, it's definitely a language decision rather than a runtime one.

EDIT: Answer from Eric Lippert (paraphrased): no particular reason, except to avoid complexity in both the language and compiler for a use case which doesn't add much value.

Workaround for C# generic attribute limitation

You could use reflection to load by id:

public class AuditAttribute : Attribute
{
public AuditAttribute(Type t)
{
this.Type = t;
}

public Type Type { get; set; }

public void DoSomething()
{
//type is not Entity
if (!typeof(Entity).IsAssignableFrom(Type))
throw new Exception();

int _id;

IRepository myRepository = new Repository();
MethodInfo loadByIdMethod = myRepository.GetType().GetMethod("LoadById");
MethodInfo methodWithTypeArgument = loadByIdMethod.MakeGenericMethod(this.Type);
Entity myEntity = (Entity)methodWithTypeArgument.Invoke(myRepository, new object[] { _id });
}
}

Generic type parameters bound to value types - making them nullable

This is language limitation.
See official docs

If the type argument for T is a value type, T? references the same value type, T. For example, if T is an int, the T? is also an int.


Attribute restrictions on C# generics


Is there a way to restrict T to types marked with the [Serializable] attribute?

No, there is not a way to do this using generic constraints. These contraints are clearly spelled out in the specification and this is not one of them.

However, you could write an extension method

public static bool IsTypeSerializable(this Type type) {
Contract.Requires(type != null);
return type.GetCustomAttributes(typeof(SerializableAttribute), true)
.Any();
}

and say

Contract.Requires(typeof(T).IsTypeSerializable());

No, it's not the same thing, but it's the best that you can do. The constraints on generics are fairly limited.

Lastly, you could consider saying

where T : ISerializable

Again, not the same thing, but it's something to consider.

one generic class for multiple variable types


Using Lambdas and Expressions

Here is the example code you asked for

class Program
{
static void Main(string[] args)
{
var avg_float = new Progressing_Average<float>();
avg_float.AddValue(1f);
avg_float.AddValue(2f);
avg_float.AddValue(3f);

Console.WriteLine($"ave={avg_float.Value:f4}");

var avg_dec = new Progressing_Average<decimal>();
avg_dec.AddValue(1m);
avg_dec.AddValue(2m);
avg_dec.AddValue(3m);

Console.WriteLine($"ave={avg_dec.Value:c}");
}
}

with sample output

ave=2.0000
ave=$2.00

Generic Math

Ever since NET 3.5, you can do generic math using Expressions and Lambda functions. There is a bit of setup that is needed, but the example below is fairly straightforward.

Some notes:

  • Define generic parameter T and restrict it to numeric types. Newer NET implementations have better ways of doing this but for compatibility, with .NET Framework I use the following where constraint in T

  • Define static methods (actually static anonymous functions) for the required math operations. Some basic ones are +, -, *, / and the conversion from int to T for mixing _Count.

    These methods are built using a static constructor for the type and are called once for each different T used.

  • Replace the math operators with the equivalent functions. Like a+b becomes Add(a,b).

Class Definition

public class Progressing_Average<T> where T: struct, 
IComparable,
IComparable<T>,
IConvertible,
IEquatable<T>,
IFormattable
{
static Progressing_Average()
{
var a = Expression.Parameter(typeof(T));
var b = Expression.Parameter(typeof(T));
var n = Expression.Parameter(typeof(int));

Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(a, b), a, b).Compile();
Sub = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(a, b), a, b).Compile();
Mul = Expression.Lambda<Func<T, T, T>>(Expression.Multiply(a, b), a, b).Compile();
Div = Expression.Lambda<Func<T, T, T>>(Expression.Divide(a, b), a, b).Compile();
FromInt = Expression.Lambda<Func<int, T>>(Expression.Convert(n, typeof(T)), n).Compile();
}
static Func<T, T, T> Add { get; }
static Func<T, T, T> Sub { get; }
static Func<T, T, T> Mul { get; }
static Func<T, T, T> Div { get; }
static Func<int, T> FromInt { get; }

public Progressing_Average()
{
_Count = 0;
Value = FromInt(0);
}
public T Value { get; private set; }
private int _Count { get; set; }
public void AddValue(T input)
{
if (_Count == int.MaxValue)
{
throw new IndexOutOfRangeException("max amount has been reached! use preciseaverage or moving avg instead!");
}
_Count++;
Value = Add(Value, Div(Sub(input, Value), FromInt(_Count)));
}
}


Support for User Defined Types

If you relax the constraints a bit, you can even use your own types above.

Define the processing class as

public class Progressing_Average<T> where T: struct, 
IEquatable<T>,
IFormattable

and a user-defined type, such as

public readonly struct Point :
IEquatable<Point>,
IFormattable
{
public Point(float x, float y) : this()
{
X = x;
Y = y;
}

public float X { get; }
public float Y { get; }

public static Point operator +(Point a, Point b)
=> new Point(a.X + b.X, a.Y + b.Y);
public static Point operator -(Point a, Point b)
=> new Point(a.X - b.X, a.Y - b.Y);
public static Point operator *(Point a, Point b)
=> new Point(a.X * b.X, a.Y * b.Y);
public static Point operator /(Point a, Point b)
=> new Point(a.X / b.X, a.Y / b.Y);

public static implicit operator Point(int x)
=> new Point(x,x);
public static implicit operator Point(float x)
=> new Point(x,x);
public static implicit operator Point(decimal x)
=> new Point((float)x,(float)x);
public static implicit operator Point(double x)
=> new Point((float)x,(float)x);

#region IFormattable Members
public string ToString(string formatting, IFormatProvider provider)
{
return $"({X},{Y})";
}
public string ToString(string formatting)
=> ToString(formatting, null);
public override string ToString()
=> ToString("g");
#endregion

#region IEquatable Members

/// <summary>
/// Equality overrides from <see cref="System.Object"/>
/// </summary>
/// <param name="obj">The object to compare this with</param>
/// <returns>False if object is a different type, otherwise it calls <code>Equals(Point)</code></returns>
public override bool Equals(object obj)
{
return obj is Point item && Equals(item);
}

/// <summary>
/// Checks for equality among <see cref="Point"/> classes
/// </summary>
/// <returns>True if equal</returns>
public bool Equals(Point other)
{
return X.Equals(other.X)
&& Y.Equals(other.Y);
}
/// <summary>
/// Calculates the hash code for the <see cref="Point"/>
/// </summary>
/// <returns>The int hash value</returns>
public override int GetHashCode()
{
unchecked
{
int hc = -1817952719;
hc = (-1521134295) * hc + X.GetHashCode();
hc = (-1521134295) * hc + Y.GetHashCode();
return hc;
}
}
public static bool operator ==(Point target, Point other) { return target.Equals(other); }
public static bool operator !=(Point target, Point other) { return !target.Equals(other); }

#endregion

}

Now watch as you can use the above struct to get averages

class Program
{
static void Main(string[] args)
{
var avg_vec = new Progressing_Average<Point>();
avg_vec.AddValue(new Point(1, 2));
avg_vec.AddValue(new Point(2, 3));
avg_vec.AddValue(new Point(3, 4));
Console.WriteLine($"ave={avg_vec.Value}");
}
}

with output

ave=(2,3)

There is some skullduggery going on here, because behind the scenes the expression Expression.Convert() automatically calls any implicit operator for conversions, as well as any user defined arithmetic operators such as operator + (a,b).



Related Topics



Leave a reply



Submit