Home Dashboard Directory Help
Search

Error "An attribute argument must be a constant expression [etc]" incorrectly received when the argument is of type Enum by TimwiTerby


Status: 

Closed
 as Won't Fix Help for as Won't Fix


6
0
Sign in
to vote
Type: Bug
ID: 507907
Opened: 11/3/2009 9:31:58 AM
Access Restriction: Public
1
Workaround(s)
view
6
User(s) can reproduce this bug

Description

    sealed class My1Attribute : Attribute { public My1Attribute(object obj) { } }
    sealed class My2Attribute : Attribute { public My2Attribute(Enum obj) { } }
    enum MyEnum { One, Two, Three };

    [My1(MyEnum.One)]     // <-- works
    class ClassOne { }

    [My2(MyEnum.One)]     // <-- compiler error. Expand Details below for expected behaviour
    class ClassTwo { }
Details
Sign in to post a comment.
Posted by Microsoft on 10/31/2010 at 12:20 PM
Hello,

After looking more into this feature request we decided that the value of this feature (compile-time error detection in certain scenarios involving custom attributes) is not worth the high cost associated with CLI format breaking change.

We do not plan to implement this feature in next CLR release.

Thank you for your feedback,
-Karel Zikmund
Developer on CLR team
Posted by TimwiTerby on 7/29/2010 at 1:58 PM
Thank you for your response, Karel. I have opened a thread on the CLR forum as requested:

http://social.msdn.microsoft.com/Forums/en-US/clr/thread/3d53ce2a-156e-4483-8053-a85ad4f9bde8

Please see my response there.
Posted by Microsoft on 7/29/2010 at 12:59 PM
Hello Theraot,
I don't see how your comment is relevant to this topic. Please let's stay focused on this suggestion only in this bug.

Thanks,
-Karel Zikmund
Posted by Microsoft on 7/29/2010 at 12:57 PM
Hello TimwiTerby,

> This is irrelevant: the same is true irrespective of whether the parameter type is System.Enum or System.Object.

It is actually relevant - Think how IL code for this code should look like:
    void MyMethod(System.Enum arg) { System.WriteLine(arg); }
    MyMethod(MyInt1Enum.One);
    Mymethod(MyInt4Enum.Two);
System.Object is passed by reference which is a fixed size (4 or 8 bytes on x86 or x64 arch respectively).
Enums and value types are passed by value (unless they are boxed) and their size therefore has to be known at JIT time.

> Not true: With System.Object, the compiler will allow passing something that is not an enum.

Right, compilers could do in theory a little better job in early detection of this if they choose to. At runtime the code would be the same.

> Not true: Reflection retrieves the enum type from the boxed enum value just fine, and distinguishes correctly between MyEnum.One and AnotherEnum.One, even when both are explicitly declared as having the same integer value.

I don't think so. When I try this:
    using System;
    class My1Attribute : Attribute { public My1Attribute(object obj) {} }
    enum MyEnum { One = 1, Two = 1 };
    [My1(MyEnum.One)]
    class Class { static void Main() {} }
The custom attribute in IL is stored as
    .custom instance void My1Attribute::.ctor(object) = {object(int32(1))}
In other words, the information that value '1' is MyEnum.One is lost. Reflection cannot possible make it up.
If you don't agree, please post a sample that shows what you meant.


I would suggest to move this discussion to CLR forum on MSDN. That will allow us easier communication and sample posting than here on MS Connect. We can update this bug when we get to a conclusion. Please create a thread on MSDN forum and link it here.

Thanks,
-Karel Zikmund
Posted by THERAOT on 7/29/2010 at 6:08 AM
Hey Karel Zikmund

It's so sad generics wheren't around in 1.1 and they could have been exploded better in 2.0.... they could have been used in BinaryReader to simplify things, and in Enums to tell the type of the underlying members. Why not a bittable constraint? that was easier in the early steps of 2.0. But you kept some compatibility at source level.

Atleast we can write our own BinaryReader if we want, not that it's pretty, it's hard to tell if a type is bittable or not at runtime given the type as a generic parameter. Why not a "IBittable" interface with "ToBinary" or something like that? Oh yes... it would need a "FromBinary" but that one got to be static, and there are not static interfaces (they could exist, it was easier in the early days).

Now it's too late.

Anyway I like to think that todays CLR TypeSystem is better than it was, but it needs a few others things until some enthusiast gets its hand to write an alternative "System.dll" (even some parts of mscorlib, as Cecil from Mono is alternative to Reflection) that takes full adventage of this things. Perhaps one he could use in his own language also wrote on .NET?

Just trying to rise a concern.
Posted by TimwiTerby on 7/29/2010 at 1:51 AM
> it will not know how big the argument is (1-4 bytes) without knowing the exact enumeration type ("MyEnum").

This is irrelevant: the same is true irrespective of whether the parameter type is System.Enum or System.Object.

> In other words, by using System.Enum you will get the same compile-time/run-time checking behavior as with System.Object.

Not true: With System.Object, the compiler will allow passing something that is not an enum.

> Reflection will see only value '0' and without additional information it will not be able to link the value '0' with "MyEnum.One" [...] Note that this affects also your System.Object workaround.

Not true: Reflection retrieves the enum type from the boxed enum value just fine, and distinguishes correctly between MyEnum.One and AnotherEnum.One, even when both are explicitly declared as having the same integer value.

> In your real-life example I would suggest to identify groups by integer number

If I did that, *then* it would be impossible to retrieve the enum type.

I think there is a fundamental misunderstanding here. Nothing in your response explains why the CLR cannot allow System.Enum and System.ValueType as a parameter type when it can already allow System.Object.
Posted by Microsoft on 7/23/2010 at 3:18 PM
Hello TimwiTerby,

Let me try to explain the reasons behind this behavior:

1. Re: "Of course I realise the workaround of using ‘object’, but it forces the error-checking into run-time, impacting the stability of the product. One would have thought that a strongly-typed compiler should be able to check this kind of error early."

Enumerations can have various underlying types - they can be stored as signed or unsigned integers of size 1-4 bytes.
If a method (like your constructor) takes System.Enum, it will not know how big the argument is (1-4 bytes) without knowing the exact enumeration type ("MyEnum").
If it should deal with all sizes/enumerations, then it will have to do the same thing as boxing. For that System.Object type can be used as you found out.
In other words, by using System.Enum you will get the same compile-time/run-time checking behavior as with System.Object. Strongly-typed compiler will not be able to check this early.


2. Enumeration values are constants. Constants are stored by value in .NET assemblies. It means that Reflection will see only value '0' in your example and without additional information it will not be able to link the value '0' with "MyEnum.One", because it cannot figure out in general if the value is "MyEnum.One" or "AnotherEnum.AnotherOne". The type information (MyEnum) is not stored alongside the enumeration value (0).

Note that this affects also your System.Object workaround. Reflection can give you back the value 0, 1, 2, etc. But it will not be able tell you "MyEnum.One" unless you explicitly ask the type "MyEnum" for all its constants with value '0'. You have to find the right type to use yourself.
Also note that these examples of custom attributes are also valid:
    enum My2Enum { One = 0, Two = 0 };
    
    [My1(My2Enum.One)]
    [My1(My2Enum.Two)] // You will not be able to distinguish this from previous custom attribute 'One' via Reflection
    [My1((My2Enum)42)] // The value '42' is not listed as My2Enum constant, but it is still valid.


Overall this points out that the purpose of enumerations is just syntactic sugar for naming integer values. The enumeration constants themselves don't play big role, only their values do. This is how .NET enumerations were designed 10 years ago.


In your real-life example I would suggest to identify groups by integer number - that will allow users of your custom attribute to use their own enumerations if they want:
     sealed class My3Attribute : Attribute { public My3Attribute(int obj) { } }
     [My3((int)MyEnum.One)]
     class ClassThree { }


Let me know if you have different opinion on this matter or if you think I misunderstood your explanation.
-Karel Zikmund
Developer on CLR TypeSystem and MetaData team
Posted by TimwiTerby on 7/19/2010 at 8:19 PM
Many thanks for your reply, and thank you for considering the issue further.

Hm, so the problem lies with the CLR metadata format — I didn’t expect this. I admit I don’t know much about this format, but I wouldn’t have expected that the restrictions depend on the type of the parameter, or that boxing the value requires extra provisions. Oh well. Of course, I can still hope that you might be able to allow System.ValueType and System.Enum in addition to System.Object without major breaking changes to the format, but I realise you will want to see a compelling use-case for it.

So I’ll try to describe the case I ran into. I’ll have to keep this somewhat abstract because otherwise it’ll be too long and there will be too much detail that is irrelevant, but I hope you’ll be able to see the picture.

Imagine you want a class library that provides functionality to generate a GUI dialog box. This dialog box allows the user to enter strings in a series of textboxes. However, I needed the functionality to be generic enough to allow the client code to specify how many strings there are, what the “prompt” for each string is, etc. However, there may be hundreds of strings, so presenting a long list of textboxes is inappropriate. The client code should therefore be able to define “groups”, each containing a subset of the strings, and each group should have a name and description. Then instead of hundreds of textboxes, the GUI can present the user with a listbox containing a reasonable number of groups, and selecting a group presents a reasonable number of textboxes.

The way I implemented this is as follows. The client code declares a class containing several fields of type “string”. The class library reads these fields via Reflection and generates the dialog box accordingly. The fields are decorated with custom attributes to specify the “prompt” and the “group” for each string. But for obvious reasons, I didn’t want to have to repeat the parameters for each group (e.g. its description) on every string field, so I decided to have the client code declare an enum type — each enum value represents a group. Custom attributes on the enum values specify the parameters of each group. The custom attribute on the string fields would simply refer to one of the enum values.

This is where I ran into the problem. The class library (which declares all the custom attributes) cannot refer to the specific enum type because the client code is supposed to be able to declare its own. However, the class library wants to be sure that what is passed in is actually an enum type, and not something else like a string literal or a typeof().

Initially I used nested classes to specify the “grouping” instead, and this worked fine — until the customer wanted to be able to place some of the strings into multiple groups. This is why I decided for the idea with the enum type so the client can just specify several groups on the same string field.

I realise that there are more “traditional” approaches for the whole scenario that don’t involve Reflection, for example lists and dictionaries. However, I went for this Reflection-based approach for several reasons: ① The client programmer can use IntelliSense and compile-time checking on the code that accesses the strings. ② The object returned can be easily serialised and deserialised with something like XmlSerializer or DataContractSerializer and the resulting XML file looks reasonable. ③ The declaration of all the strings and groups forms a lexical unit and can be placed in a self-contained source file of its own; the information is not separated and spread out throughout the client codebase.

I hope I have provided all the necessary detail to appreciate the use-case, and I would be interested to hear what your thoughts are on how this impacts your evaluation of how important or unimportant it is to allow System.Enum as a parameter type on custom attributes.
Posted by Microsoft on 7/9/2010 at 9:00 PM
Sorry if my answer was terse! We'd be interested to hear more about the scenario where you ran into this.

However, it's likely to be the case that there's little we can do to make this scenario actually work without changes to the CLR's metadata format, something which we don't take lightly. There are restrictions imposed by the CLR's current metadata format on what values may be stored as attribute values for particular types. Provisions are made to allow boxing for attribute parameters of type Object, but as you've found, this does not extend to cases where the type is System.Enum or System.ValueType. Other restrictions imposed by the metadata format are yet more impactful than this, such as the restriction that attribute types must be non-generic.

I've reactivated this bug and moved it over to the CLR team. They'll be able to consider your feedback regarding the type limitations on attribute values directly.

Alex Turner
Program Manager
Visual Basic and C# Compiler
Posted by TimwiTerby on 6/11/2010 at 1:09 PM
I am rather dissatisfied with the answer provided. The problem I posted here is one I actually ran into in real-life, commercial code. If you are interested in finding out why I think I need System.Enum as a parameter type on an enum, I am happy to elaborate, just ask.

Additionally, perhaps I should point out that the error occurs not only with System.Enum but also with System.ValueType, broadening the problem.

Of course I realise the workaround of using ‘object’, but it forces the error-checking into run-time, impacting the stability of the product. One would have thought that a strongly-typed compiler should be able to check this kind of error early.
Posted by Microsoft on 6/3/2010 at 6:12 PM
Thanks for reporting this issue you've encountered with Visual Studio 2008!

This error is indeed wrong and we could give a better error here or perhaps get this to work. This problem is fairly narrowly constrained, however, to using Enum itself as a parameter type in attributes. It seems relatively unlikely for an attribute to know that it can only take Enum values as an arugment without knowing which Enum type. For the cases where this happens, the workaround is as you found, to use object (boxing will be required either way).

Unfortunately, we won't be able to fix this bug in the next version of Visual Studio.

Alex Turner
Program Manager
Visual C# Compiler
Posted by Noldorin on 12/20/2009 at 3:06 PM
I can confirm that this bug is also occurring in .NET 4.0/VS 2010 Beta 2.
Posted by Microsoft on 11/6/2009 at 12:07 AM
Thanks for your feedback.

We are rerouting this issue to the appropriate group within the Visual Studio Product Team for triage and resolution. These specialized experts will follow-up with your issue.

Thank you
Posted by Microsoft on 11/4/2009 at 2:42 AM
Thank you for your feedback, We are currently reviewing the issue you have submitted. If this issue is urgent, please contact support directly(http://support.microsoft.com)
Sign in to post a workaround.
Posted by PatrickPijnappel on 5/1/2010 at 11:55 AM
This problem occurs when using parameters with default values. Removing the default values fixes the problem.