How do I detect that an object is a generic collection, and what types it contains?
I have a string serialization utility that takes a variable of (almost) any type and converts it into a string. Thus, for example, according to my convention, an integer value of 123 would be serialized as "i:3:123" (i=integer; 3=length of string; 123=value).
The utility handles all primitive type, as well as some non-generic collections, like ArrayLists and Hashtables. The interface is of the form
public static string StringSerialize(object o) {}
and internally I detect what type the object is and serialize it accordingly.
Now I want to upgrade my utility to handle generic collections. The funny thing is, I can't find an appropriate function to detect that the object is a generic collection, and what types it contains - both of which pieces of information I need in order to serialize it correctly. To date I've been using coding of the form
if (o is int) {// do something}
but that doesn't seem to work with generics.
What do you recommend?
EDIT: Thanks to Lucero, I've gotten closer to the answer, but I'm stuck at this little syntactical conundrum here:
if (t.IsGenericType) {
if (typeof(List<>) == t.GetGenericTypeDefinition()) {
Type lt = t.GetGenericArguments()[0];
List<lt> x = (List<lt开发者_Python百科>)o;
stringifyList(x);
}
}
This code doesn't compile, because "lt
" is not allowed as the <T>
argument of a List<>
object. Why not? And what is the correct syntax?
Use the Type to gather the required information.
For generic objects, call GetType() to get their type and then check IsGenericType
to find out if it is generic at all. If it is, you can get the generic type definition, which can be compared for instance like this: typeof(List<>)==yourType.GetGenericTypeDefinition()
.
To find out what the generic types are, use the method GetGenericArguments
, which will return an array of the types used.
To compare types, you can do the following: if (typeof(int).IsAssignableFrom(yourGenericTypeArgument))
.
EDIT to answer followup:
Just make your stringifyList
method accept an IEnumerable
(not generic) as parameter and maybe also the known generic type argument, and you'll be fine; you can then use foreach
to go over all items and handle them depending on the type argument if necessary.
Re your conundrum; I'm assuming stringifyList
is a generic method? You would need to invoke it with reflection:
MethodInfo method = typeof(SomeType).GetMethod("stringifyList")
.MakeGenericMethod(lt).Invoke({target}, new object[] {o});
where {target}
is null
for a static method, or this
for an instance method on the current instance.
Further - I wouldn't assume that all collections are a: based on List<T>
, b: generic types. The important thing is: do they implement IList<T>
for some T
?
Here's a complete example:
using System;
using System.Collections.Generic;
static class Program {
static Type GetListType(Type type) {
foreach (Type intType in type.GetInterfaces()) {
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IList<>)) {
return intType.GetGenericArguments()[0];
}
}
return null;
}
static void Main() {
object o = new List<int> { 1, 2, 3, 4, 5 };
Type t = o.GetType();
Type lt = GetListType(t);
if (lt != null) {
typeof(Program).GetMethod("StringifyList")
.MakeGenericMethod(lt).Invoke(null,
new object[] { o });
}
}
public static void StringifyList<T>(IList<T> list) {
Console.WriteLine("Working with " + typeof(T).Name);
}
}
At the most basic level, all generic lists implement IEnumerable<T>
, which is in itself a descendant of IEnumerable
. If you want to serialize a list, then you could just cast it down to IEnumerable and enumerate the generic objects inside them.
The reason why you can't do
Type lt = t.GetGenericArguments()[0];
List<lt> x = (List<lt>)o;
stringifyList(x);
is because generics still need to be statically strong typed, and what you're trying to do is to create a dynamic type. List<string>
and List<int>
, despite using the same generic interface, are two completely distinct types, and you can't cast between them.
List<int> intList = new List<int>();
List<string> strList = intList; // error!
What type would stringifyList(x)
receive? The most basic interface you could pass here is IEnumerable
, since IList<T>
doesn't inherit from IList
.
To serialize the generic list, you need to keep information on the original Type of the list so that you can re-create with Activator
. If you want to optimize slightly so that you don't have to check the type of each list member in your stringify method, you could pass the Type you've extracted from the list directly.
I did not want to accept that there is no way to cast to List<lt>
. So I came up with using a List<dynamic>
.
But since i need it for all types of IEnumerables, my solution looks like this:
object o = Enumerable.Range(1, 10)
.Select(x => new MyClass { Name = $"Test {x}" });
//.ToList();
//.ToArray();
if (o.IsIEnumerableOfType<IHasName>(out var items))
{
// here you can use StringifyList...
foreach (var item in items)
{
Console.WriteLine(item.Name);
}
}
static class Ext
{
public static bool IsIEnumerableOfType<T>(this object input,
[NotNullWhen(true)] out IEnumerable<T>? values)
{
var innerType = input.GetType()
.GetInterfaces()
.FirstOrDefault(x =>
x.IsGenericType
&& x.GetGenericTypeDefinition() == typeof(IEnumerable<>))?
.GetGenericArguments()[0];
if (typeof(T).IsAssignableFrom(innerType))
{
values = ((IEnumerable<object>)input).OfType<T>();
return true;
}
values = null;
return false;
}
}
class MyClass : IHasName
{
public string Name { get; set; } = "";
}
Features:
- You can use any Type of IEnumerable (inkl. Arrays and Lists)
- Your can not only specify the actual class but its interface
- It does not give any warnings due to the use of
[NotNullWhen(true)]
Update:
For Dictionaries it's nearly the same code. Just change IEnumerable
to IDictionary
and use
Type keyType = t.GetGenericArguments()[0];
Type valueType = t.GetGenericArguments()[1];
to get the key and value type.
Update based on @Enigmativity comments
The use of
dynamic
is not needed.object
is fine and is safer. – Enigmativity
Yes, you can use IEnumerable<object>
in this case.
You also only need this test:
if (typeof(IEnumerable<object>).IsAssignableFrom(input.GetType()))
. – Enigmativity
And, when you really get down to it you can just write
if (o is IEnumerable<IHasName> items)
in the main code. – Enigmativity
This only works for IEnumerable
, but if you want to work with a list and the list should be of type IList<IHasName>
It will not work anymore. So my solution is more versatile. (The accepted answer uses IList and inspired me)
I tried this with a List and it did not enter the if-statement:
object x = new List<MyClass>();
if (x is IList<IHasName> list)
{
for (int i = 0; i < list.Count; i++)
{
list[i] = newItems.ElementAt(i);
}
}
Use case:
if (value.IsListOfType<UpdatableBase>(out var list))
{
for (int i = 0; i < list.Count; i++)
{
list[i] = newItems.ElementAt(i);
}
}
The extension methode looks like:
public static bool IsListOfType<T>(this object input, [NotNullWhen(true)] out IList<T>? values)
{
Type? innerType = input.GetType()
.GetInterfaces()
.FirstOrDefault(x =>
x.IsGenericType
&& x.GetGenericTypeDefinition() == typeof(IList<>))?
.GetGenericArguments()[0];
if (typeof(T).IsAssignableFrom(innerType))
{
values = ((IList<object>)input).OfType<T>().ToList();
return true;
}
values = null;
return false;
}
But, finally, this question is about using a run-time type, not a compile time one. – Enigmativity
My solution is suitable for the use case described in the question. He can use object
or what ever (base) type his serializer is able to stringify as T-parameter.
精彩评论