.NET reflection: determining whether an array of T

2019-06-07 01:11发布

问题:

Note, this question is a bit subtle so read it carefully: I'm not just trying to find out whether some artibitrary type implements IEnumerable:

Here's a function I've written with an initial implementation:

    // is "toType" some sort of sequence that would be satisfied
    // by an array of type T? If so, what is the type of T?
    // e.g.
    // getArrayType(typeof(string[]) == typeof(string)
    // getArrayType(typeof(IEnumerable<int>)) == typeof(int)
    // getArrayType(typeof(List<string>)) == null // NOTE - Array<string> does not convert to List<string>
    // etc.
    private static Type getArrayType(Type toType)
    {
        if (toType.IsArray)
        {
            if (toType.GetArrayRank() != 1)
                return null;
            return toType.GetElementType();
        }
        // Look for IEnumerable<T>, and if so, return the type of T
        if (toType.IsGenericType && toType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            return toType.GetGenericArguments()[0];

        return null;
    }

Can it be better and handle more cases? e.g. currently

getType(typeof(ICollection<string>)) == null 

but string[] is convertible to ICollection

Note also, I don't know in advance what the "element" type is.

The context is: I'm writing a reflection binding to a scripting language and I want it to "just work" if you pass an object[] to some method that expects IEnumerable (it would convert each of the elements of the input array to a string, in this case).

So to clarify, say I have some method signature:

void WriteCSV(ICollection<string> fields);

and my script interpreter has an array of objects that all happen to be Convert-ible to string:

object[] fields = new object[] { "one", 2, "three" };

Then my script interpreter needs to figure out that what's really needed in this case is an array of strings.

Whereas I want my script interpreter to give up on, say:

void WriteCSV(IRecord record);

even though IRecord might even implement some IEnumerable:

interface IRecord : IEnumerable<string>
{
    void OtherMethods();
}

There's no way I can construct an IRecord from an array of anything.

So just finding out what IEnumerables a type implements isn't what I need. I said it was subtle didn't I?

回答1:

It can be done by creating an array of the generic argument and see if it implements the target type. The example code checks for interfaces only since array ensures implementing interfaces only (IList, ICollection, IEnumerable). In addition i returned from the example the type of array (just to make it clear), in you code you will want to return the generic argument as in the sample code you posted.

private static Type GetArrayType(Type toType)
{
    if (toType.IsArray)
    {
        if (toType.GetArrayRank() != 1)
            return null;
        return toType;
    }

    // Verifies the type is generic and has only one generic argument
    if (toType.IsGenericType && toType.GetGenericArguments().Length == 1)
    {
        // Creates an array of the generic argument, e.g. IList<int> -> int[]
        var arrayOfType = toType.GetGenericArguments()[0].MakeArrayType();

        // Checks if the toType is an interface that the array implements
        if (arrayOfType.GetInterfaces().Contains(toType))
        {
            return arrayOfType    // arrayOfType.GetGenericArguments()[0];
        }
    }    
    return null;
}

public static void Test()
{
    Assert.AreEqual(typeof(int[]), GetArrayType(typeof(IList<int>)));
    Assert.AreEqual(typeof(int[]), GetArrayType(typeof(ICollection<int>)));
    Assert.AreEqual(typeof(int[]), GetArrayType(typeof(IEnumerable<int>)));

    Assert.IsNull(GetArrayType(typeof(List<int>)));
    Assert.IsNull(GetArrayType(typeof(Dictionary<int, string>)));
}

I hope it helps.



回答2:

Based on my answer posted in your last question:

You can use this piece of code to get all implementations of the IEnumberable<T> interface on a particular type, and then extract their generic parameter.

Type type = typeof(ICollection<string>);

IEnumerable<Type> elementTypes = type.GetInterfaces()
    .Where(i => i.IsGenericType 
        && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
    .Select(i => i.GetGenericArguments()[0]);

In this example, elementTypes will be a single element containing the type for System.String.

What you do with the element types is up to you. Since an interface can be implemented multiple times, so you may wish to modify the query to obtain a single implementation.



回答3:

Array case is covered by IEnumerable<T> as one dimensional arrays (the ones you are looking for using GetArrayrank)implement IList<T> which is in turn derived from IEnumerable<T>. It makes sense to look for IEnumerable next and treat element type as object - that is the way foreach construct treats collections.

One more thing to consider is a case when supplied type implements IEnumerable<T> several times (for example, IEnumerable<object> and IEnumerable<string>). For example, DataContractSerializer in this case doesn't consider the type as a collection type.