C# FieldInfo.SetValue with an array parameter and arbitrary element type
Asked Answered
A

2

2

I am trying to set an array field using reflection like this:

FieldInfo field = ...
A[] someArray = GetElementsInSomeWay();
field.SetValue(this, someArray);

The field has type B[]. B inherits from A and the exact type of B is not known at compile time. GetElementsInSomeWay() returns A[] but the real elements inside are all B's. GetElementsInSomeWay() is a library method and can't be changed.

What I can do at most is to get the B with System.Type type = field.FieldType.GetElementType(). However I can't cast the array to the required type, e.g. someArray as type[] because [] requires an exact type before it to declare an array type. Or am I missing something here? Can I declare an array of some type, if the type becomes known in runtime using System.Type variable?

Doing it the direct way produces the following error (here A is UnityEngine.Component and B is AbilityResult which can also be one of a few dozens other classes, all inheriting (possibly thru a long inheritance chain) from UnityEngine.Component):

ArgumentException: Object type UnityEngine.Component[] cannot be converted to target type: AbilityResult[]
Parameter name: val
System.Reflection.MonoField.SetValue (System.Object obj, System.Object val, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Globalization.CultureInfo culture) (at /Applications/buildAgent/work/3df08680c6f85295/mcs/class/corlib/System.Reflection/MonoField.cs:133)
System.Reflection.FieldInfo.SetValue (System.Object obj, System.Object value) (at /Applications/buildAgent/work/3df08680c6f85295/mcs/class/corlib/System.Reflection/FieldInfo.cs:150)
Andalusite answered 9/12, 2012 at 18:46 Comment(1)
Either I don't understand or this is impossible. The field cannot be of type B[] where B is not known at the compile time.Deuteragonist
C
3

I think you need to understand better the variance of arrays. Arrays like object[] and string[] came already in .NET 1, a long time before generics (.NET 2). Otherwise they might have been called Array<object> and Array<string>.

Now, we all know that any string is an object. If this fact implies, for some "construction" Xxx, that any Xxx<string> is an Xxx<object>, then we call this covaraince. Since .NET 4, some generic interfaces and generic delegate types can be covarinat, and this is marked with an out keyword in their definition, as in

public interface IEnumerable<out T>
{
  // ...
}

If the relation is reversed upon "applying" Xxx<>, then that's called contravariance. So "string is object" with contravariance becomes "Xxx<object> is Xxx<string>".

Now, back to arrays. The natural question here is: Are arrays covariant or contravariant or neither ("invariant")? Since you can both read from and write to each "cell" in an array, they can't be fully covariant or contravariant. But try this code:

string[] arr1 = { "these", "are", "strings", };
object[] arr2 = arr1;                           // works! covariance!
var runtimeType = arr2.GetType();               // System.String[]

So a T[] is covariant in .NET. But didn't I just say I could write (like in not out) to an array? So what if I continue like this:

arr2[0] = new object();

I'm trying to put an object which is not a string into the zeroth slot. The above line has to compile (compile-time type of arr2 is object[]). But it also has to raise an excpetion run-time. That's the problem with arrays and covariance.

So what about contravariance? Try this:

object[] arrA = { new object(), DateTime.Now, "hello", };
string[] arrB = arrA;  // won't compile, no cotravariance

Making an explicit cast from object[] to string[] still won't work runtime.

And now, the answer to your question: You are trying to apply contravariance to arrays. AbilityResult is Component, but that does not imply that Component[] is AbilityResult[]. Whoever wrote the GetElementsInSomeWay() method, chose to create a new Component[]. Even if all components he put into it, are AbilityResult, there's still no contravariance. The author of GetElementsInSomeWay() could have chosen to make a new AbilityResult[] instead. He could still have return type Component[] beacuase of the covariance of .NET arrays.

Lesson to learn: The real type (run-time type as revealed by .GetType()) of an array will not change just because you cast. .NET does allow covariance (a variable of type Component[] might hold an object whose real type is AbilityResult[]). And finally, .NET does not allow contravariance (a variable of type AbilityResult[] never holds a reference to an object of real type Component[]).

Hope this helps. Otherwise, my answer should give you some terms you can google to find explanations superior to mine.

Cupidity answered 9/12, 2012 at 20:42 Comment(1)
Thanks, i guess I understood the problem. The cast from Component[] to AbilityResult[] is not possible because contravariance is forbidden, but field.SetValue requires the value of exactly the type of the field. This means there is probably no other way but to create a new array of the required type AbilityResult[] and copy all elements to it (which would work since the first Component[] array initially held AbilityResult objects)Andalusite
A
1

After some search i stumbled on this question: How do I create a C# array using Reflection and only type info?

A possible solution to my problem is:

A[] someArray = GetElementsInSomeWay();
System.Type type = field.FieldType.GetElementType();
Array filledArray = Array.CreateInstance(type, someArray.Length);
Array.Copy(someArray, filledArray, someArray.Length);
field.SetValue(this, filledArray);

I just tested, it works.

However, I'd still like to avoid copying the elements. In my case the arrays are pretty small (3-5 elements at most) but nevertheless would be nice to see a cleaner solution if there is one.

Andalusite answered 9/12, 2012 at 19:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.