我如何检测一个对象是泛型集合,并且它包含哪些类型?

10
我有一个字符串序列化工具,可以将(几乎)任何类型的变量转换为字符串。例如,按照我的约定,整数值123会被序列化为"i:3:123"(i=整数;3=字符串长度;123=值)。
该工具处理所有基本类型,以及一些非通用集合,如ArrayLists和Hashtables。接口形式为 public static string StringSerialize(object o) {}
在内部,我检测对象的类型并相应地进行序列化。
现在我想升级我的实用程序以处理通用集合。有趣的是,我找不到适当的函数来检测对象是否为通用集合以及它包含的类型 - 我需要这两个信息才能正确序列化它。迄今为止,我一直在使用以下代码
if (o is int) {// do something}
但这似乎在通用集合中无效。
你有什么建议?

编辑:感谢Lucero,我已经接近答案了,但是在这个小的语法难题上卡住了:

if (t.IsGenericType) {
  if (typeof(List<>) == t.GetGenericTypeDefinition()) {
    Type lt = t.GetGenericArguments()[0];
    List<lt> x = (List<lt>)o;
    stringifyList(x);
  }
}

这段代码无法编译,因为"lt"不能作为List<>对象的<T>参数。为什么?正确的语法是什么?


如果你真的想这样做,那么你需要按照以下方式进行操作:https://dev59.com/yXVC5IYBdhLWcg3whRgw。但是我认为你可能没有理解泛型的意义。 - meandmycode
@meandmycode - 对于序列化实用程序代码来说,这是非常正常的。 - Marc Gravell
我不明白为什么在序列化时需要动态实例化,也许是为了反序列化,但这并不合理。 - meandmycode
我的观点是,没有必要为了序列化需要强制转换泛型列表,因为你只关心枚举对象序列。 - meandmycode
@meandmycode - 不,恰恰相反;你只是选择忽略“列表类型有时很重要”这个事实(即IList<T>中的T)。作为一个花费了很多时间编写实用的序列化代码的人,我确信这一点。 - Marc Gravell
显示剩余8条评论
4个回答

7
使用Type收集所需信息。
对于通用对象,调用GetType()来获取它们的类型,然后检查IsGenericType以确定它是否为通用类型。如果是,可以获取通用类型定义,就像这样进行比较:typeof(List<>)==yourType.GetGenericTypeDefinition()。 要找出通用类型是什么,使用方法GetGenericArguments,它将返回使用的类型数组。
要比较类型,可以执行以下操作:if (typeof(int).IsAssignableFrom(yourGenericTypeArgument))
编辑以回答后续问题:
只需使您的stringifyList方法接受IEnumerable(非泛型)作为参数,可能还需要已知的通用类型参数,您将会很好; 然后可以使用foreach遍历所有项,并根据需要处理它们的类型参数。

好的回答,谢谢 - 但我只是缺少一个小语法技巧:如何将对象o强制转换为List<TheType>? - Shaul Behr
1
假设所有集合都是a:泛型,或b:与List<T>相关联的是有风险的。我可以编写自己的FooCollection:IList<Foo>,它既不是a也不是b。 - Marc Gravell
你实际上不需要进行强制类型转换,只需使用非泛型的IEnumerable(例如,在类上执行foreach)。或者,如果您确实检查了泛型类型参数是否匹配,当IsAssignableFrom示例返回true时,您当然也可以进行硬转换,例如IList<int>。 - Lucero

6

关于你的难题,我假设 stringifyList 是一个通用方法?你需要通过反射调用它:

MethodInfo method = typeof(SomeType).GetMethod("stringifyList")
            .MakeGenericMethod(lt).Invoke({target}, new object[] {o});

当静态方法时,{target}null,当实例方法时,{target}为当前实例的this

此外,不要假设所有集合都是基于List<T>和泛型类型。重要的是:它们是否实现了某个TIList<T>接口?

以下是一个完整的示例:

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);
    }
}

谢谢Marc!但是请帮我理解一下为什么我们不得不使用反射?难道没有一种编译器能够理解调用StringifyList的方法吗?为什么微软不允许声明一个类型为List<lt>的List<>呢? - Shaul Behr
没有其他办法。如果想在运行时使用泛型类型,则 MakeGenericMethod 和 MakeGenericType 是唯一的选择。 - Marc Gravell
嗯。"无法对包含泛型参数的类型或方法执行后期绑定操作。" 你的代码跑起来了吗? - Shaul Behr
例子?是的;它输出“使用Int32工作” - 我刚刚双重检查了。 - Marc Gravell
@Fabio - 在这个例子中,stringifyList 是一个通用方法。ElementAt。关于 ElementAt - 你能澄清一下你拥有什么吗?也就是说,在这里上下文是什么? - Marc Gravell
显示剩余2条评论

1
在最基本的层面上,所有通用列表都实现了 IEnumerable<T> 接口,它本身是 IEnumerable 接口的后代。如果你想序列化一个列表,那么你可以将其转换为 IEnumerable 并枚举其中的泛型对象。
你不能这样做的原因是:
Type lt = t.GetGenericArguments()[0];
List<lt> x = (List<lt>)o;
stringifyList(x);

这是因为泛型仍然需要静态强类型,而你试图创建的是动态类型。List<string>List<int>,尽管使用相同的泛型接口,但它们是两种完全不同的类型,你不能在它们之间进行转换。

List<int> intList = new List<int>();
List<string> strList = intList; // error!

stringifyList(x)会接收什么类型?你可以传递最基本的接口IEnumerable,因为IList<T>不继承自IList

要序列化泛型列表,您需要保留有关列表原始类型的信息,以便您可以使用Activator重新创建。如果您想稍微优化一下,以便在字符串化方法中不必检查每个列表成员的类型,您可以直接传递从列表中提取的类型。


-1

我不想接受无法将List<lt>转换的事实。所以我想到了使用List<dynamic>

但由于我需要它适用于所有类型的IEnumerables,我的解决方案如下:

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; } = "";
}

特点:

  • 您可以使用任何类型的IEnumerable(包括数组和列表)
  • 您不仅可以指定实际类,还可以指定其接口
  • 由于使用了[NotNullWhen(true)],它不会给出任何警告

更新:

对于字典,几乎是相同的代码。只需将IEnumerable更改为IDictionary并使用即可。

Type keyType = t.GetGenericArguments()[0];
Type valueType = t.GetGenericArguments()[1];

获取键和值类型。

根据 @Enigmativity 的评论进行更新

不需要使用 dynamic。使用 object 就可以了,而且更安全。 - Enigmativity

是的,在这种情况下,您可以使用 IEnumerable<object>

您只需要进行这个测试:if (typeof(IEnumerable<object>).IsAssignableFrom(input.GetType()))。- Enigmativity

而且,当您真正深入研究时,您可以在主要代码中编写if (o is IEnumerable<IHasName> items)。 - Enigmativity

这仅适用于 IEnumerable,但是如果您想使用列表并且该列表应为类型 IList<IHasName>,它将不再起作用。因此,我的解决方案更具通用性。(被接受的答案使用 IList 并启发了我)

我尝试了一下 List,它没有进入 if 语句:

object x = new List<MyClass>();
if (x is IList<IHasName> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        list[i] = newItems.ElementAt(i);
    }
}

使用案例:

if (value.IsListOfType<UpdatableBase>(out var list))
{
    for (int i = 0; i < list.Count; i++)
    {
        list[i] = newItems.ElementAt(i);
    }
}

扩展方法看起来像这样:

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;
}

但是,最后,这个问题是关于使用运行时类型而不是编译时类型的。- Enigmativity
我的解决方案适用于问题描述中的用例。他可以使用object或者任何(基本)类型,他的序列化器能够将其字符串化为T参数。

不需要使用“dynamic”。使用“object”更好且更安全。 - Enigmativity
你只需要这个测试:if (typeof(IEnumerable<object>).IsAssignableFrom(input.GetType())) - Enigmativity
当你真正深入了解它时,你只需在主代码中编写 if (o is IEnumerable<IHasName> items) - Enigmativity
但是,最终,这个问题是关于使用运行时类型而不是编译时类型的。 - Enigmativity
感谢您的及时反馈。我已根据您的反馈更新了我的答案。 - RoJaIt
你明白这个问题是关于运行时类型“Type lt”以及如何使用它调用泛型方法。而你的回答具有编译时类型“IHasName”,因此并没有解答这个问题。 - Enigmativity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接