Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Attribute with params object[] constructor gives inconsistent compiler errors

I am getting the error

An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

Notice the screenshot below:

enter image description here

Notice that if I use the DataRow attribute with one or three parameters, I don't get a compile error. But if I use two parameters and the second parameter is an array of strings, then I do get a compile error.

The signatures for DataRowAttribute are public DataRowAttribute (object data1); and public DataRowAttribute (object data1, params object[] moreData);

The first one gives me no problem, but the second one seems to be getting confused.

I considered that maybe the params object[] could be causing some confusion. Maybe it couldn't determine whether I meant [DataRow(new[] { 1 }, new[] { "1" })] or [DataRow(new[] { 1 }, "1")]

To resolve that, I tried to cast the second attribute to object ([DataRow(new[] { 1 }, (object)new[] { "1" })]), but the error didn't go away and it warned me that the cast was redundant. I also tried specifying the types of the array explicitly, but that also did not help.

I could just add a third dummy parameter, even null seems to fix this, but that's just a workaround. What's the correct way to do this?

like image 639
Dan Friedman Avatar asked Feb 13 '18 23:02

Dan Friedman


2 Answers

tldr:

The correct workaround is to tell the compiler to not use the expanded form:

[DataRow(new[] { 1 }, new object[] { new[] { "1" } })]

Excessive analysis:

The answer of Michael Randall is basically correct. Let's dig in by simplifying your example:

using System;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MyAttribute : Attribute {
    public MyAttribute(params object[] x){}
}
public class Program
{
    [MyAttribute()]
    [MyAttribute(new int[0])]
    [MyAttribute(new string[0])] // ERROR
    [MyAttribute(new object[0])]
    [MyAttribute(new string[0], new string[0])]  
    public static void Main() { }
}

Let's first consider the non error cases.

    [MyAttribute()]

There are not enough arguments for the normal form. The constructor is applicable in its expanded form. The compiler compiles this as though you had written:

    [MyAttribute(new object[0])]

Next, what about

    [MyAttribute(new int[0])]

? Now we must decide if the constructor is applicable in its normal or expanded form. It is not applicable in normal form because int[] is not convertible to object[]. It is applicable in expanded form, so this is compiled as though you'd written

    [MyAttribute(new object[1] { new int[0] } )]

Now what about

    [MyAttribute(new object[0])]

The constructor is applicable in both its normal and expanded form. In that circumstance the normal form wins. The compiler generates the call as written. It does NOT wrap the object array in a second object array.

What about

    [MyAttribute(new string[0], new string[0])]  

? There are too many arguments for the normal form. The expanded form is used:

    [MyAttribute(new object[2] { new string[0], new string[0] })] 

That should all be straightforward. What then is wrong with:

    [MyAttribute(new string[0])] // ERROR

? Well, first, is it applicable in normal or expanded form? Plainly it is applicable in expanded form. What is not so obvious is that it is also applicable in normal form. int[] does not implicitly convert to object[] but string[] does! This is an unsafe covariant array reference conversion, and it tops my list for "worst C# feature".

Since overload resolution says that this is applicable in both normal and expanded form, normal form wins, and this is compiled as though you'd written

[MyAttribute((object[]) new string[0] )] // ERROR

Let's explore that. If we modify some of our working cases above:

    [MyAttribute((object[])new object[0])] // SOMETIMES ERROR!
    [MyAttribute((object[])new object[1] { new int[0] } )]
    [MyAttribute((object[])new object[2] { new string[0], new string[0] })]

All of these now fail in earlier versions of C# and succeed in the current version.

Apparently the compiler previously allowed no conversion, not even an identity conversion, on the object array. Now it allows identity conversions, but not covariant array conversions.

Casts that can be handled by the compile time constant value analysis are allowed; you can do

[MyAttribute(new int[1] { (int) 100} )]

if you like, because that conversion is removed by the constant analyzer. But the attribute analyzer has no clue what to do with an unexpected cast to object[], so it gives an error.

What about the other case you mention? This is the interesting one!

[MyAttribute((object)new string[0])]

Again, let's reason it through. That's applicable only in its expanded form, so this should be compiled as though you'd written

[MyAttribute(new object[1] { (object)new string[0] } )]

But that is legal. To be consistent, either both these forms should be legal, or both should be illegal -- frankly, I don't really care either way -- but it is bizarre that one is legal and the other isn't. Consider reporting a bug. (If this is in fact a bug it is probably my fault. Sorry about that.)

The long and the short of it is: mixing params object[] with array arguments is a recipe for confusion. Try to avoid it. If you are in a situation where you are passing arrays to a params object[] method, call it in its normal form. Make a new object[] { ... } and put the arguments into the array yourself.

like image 194
Eric Lippert Avatar answered Nov 05 '22 22:11

Eric Lippert


Assuming your constructor is

public Foo(params object[] vals) { }

Then i think you are running up against some overlooked and non obvious compiler Dark Magic.

For example, obviously the below will work

[Foo(new object[] { "abc", "def" },new object[] { "abc", "def" })]
[Foo(new string[] { "abc", "def" },new string[] { "abc", "def" })]

This also works for me

[Foo(new [] { 2 }, new [] { "abc"})]
[Foo(new [] { 1 }, new [] { "a"})]

However this does not

[Foo(new [] { "a" })]
[Foo(new [] { "aaa"})]
[Foo(new string[] { "aaa" })]

An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

I think the key take-home peice of information here is

A method with a params array may be called in either "normal" or "expanded" form. Normal form is as if there was no "params". Expanded form takes the params and bundles them up into an array that is automatically generated. If both forms are applicable then normal form wins over expanded form.

As an example

PrintLength(new string[] {"hello"}); // normal form
PrintLength("hello"); // expanded form, translated into normal form by compiler.

When given a call that is applicable in both forms, the compiler always chooses the normal form over the expanded form.

However i think this gets even messier again with object[] and even attributes.

I'm not going to pretend i know exactly what the CLR is doing (and there are many more qualified people that may answer). However for reference, take a look at the CLR SO wizard Eric Lippert's similar answers for a more detailed illumination of what might be going on

C# params object[] strange behavior

Why does params behave like this?

Is there a way to distingish myFunc(1, 2, 3) from myFunc(new int[] { 1, 2, 3 })?

like image 25
TheGeneral Avatar answered Nov 05 '22 23:11

TheGeneral