使用IEnumerable GetGenericArguments获取类型

5

我开发了一个MVC助手,用于生成显示和可编辑的表格(需要jquery插件才能允许在可编辑表格中动态添加和删除行,并进行完整的回传),例如:

@Htm.TableDisplayFor(m => m.MyCollection as ICollection)

使用该方法与属性结合,将在页脚包含总计信息,添加查看和编辑链接的列,为复杂类型呈现超链接等。例如:

[TableColumn(IncludeTotals = true)]

我即将在CodeProject上发布它,但在此之前,希望解决一个问题。助手首先从表达式中获取ModelMetadata,检查它是否实现了ICollection接口,然后获取集合中的类型(请注意下面的片段来自SO上的被接受答案,但正如下面所解释的那样,它并不完全正确)。
if (collection.GetType().IsGenericType)
{
  Type type = collection.GetType().GetGenericArguments()[0]

该类型用于生成表头的ModelMetadata(该表可能没有任何行)以及表体中每一行的ModelMetadata(如果某些项是继承类型,具有其他属性,否则会破坏列布局)。请注意保留HTML标记。
foreach (var item in collection)
{
  ModelMetadata itemMetadata = ModelMetadataProviders.Current
    .GetMetadataForType(() => item, type);

我希望能够使用 IEnumerable 而不是 ICollection,这样就不需要在LINQ表达式中调用.ToList()方法。

在大多数情况下,IEnumerable 工作得很好,例如:

IEnumerable items = MyCollection.Where(i => i....);

可以正常运行,因为.GetGenericArguments()返回只包含一个类型的数组。 问题是,某些查询中的'.GetGenericArguments()'返回2个或更多个类型,并且似乎没有逻辑顺序。例如:

IEnumerable items = MyCollection.OrderBy(i => i...);

返回 [0] 集合中的类型,[1] 用于排序的类型。

在这种情况下,.GetGenericArguments()[0] 仍然有效,但是

MyCollection.Select(i => new AnotherItem()
{
  ID = i.ID,
  Name = 1.Name
}

返回[0]原集合中的类型和[1]AnotherItem的类型。

所以.GetGenericArguments()[1]是我需要用来渲染AnotherItem表格的。

我的问题是,是否有可靠的方法使用条件语句获取我需要渲染表格的类型?

到目前为止,根据我的测试,除了使用OrderBy()时排序键是最后一个类型外,使用.GetGenericArguments().Last()在所有情况下都有效。

我尝试过的一些方法包括忽略值类型的类型(因为在使用OrderBy()时通常会出现),但是OrderBy()查询可能会使用string(可以检查),甚至更糟糕的是,类重载了==,<和>运算符(在这种情况下我将无法确定哪种是正确的类型),而且我找不到一种方法来测试集合是否实现了IOrderedEnumerable


所以,如果我理解正确,你提供了一个IEnumerable<T>,它是某个随意的LINQ查询结果(通常如此)。你想要可靠地获取IEnumerable<T>T部分? - Chris Sinclair
@Chris 是的,尽管它可能只是IEnumerable(例如ArrayList)。 - user3559349
1
如果是这样,请尝试在已实现的接口上进行迭代,找到 IEnumerable<T> 接口并提取 T 部分:var type = items.GetType().GetInterfaces().Where(t => t.IsGenericType).Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).Single().GetGenericArguments()[0]; 注意我的使用 Single:如果接口未实现,则会失败,或者如果它实现了两个(或更多)不同的 IEnumerable<T> 接口,则也会失败。您可以将其更改为使用 FirstOrDefaultFirst 并根据需要处理这些额外情况。 - Chris Sinclair
1
关于您的评论,ArrayList 是没有类型的。您可以提取第一个项并调用其 GetType() 方法,但是如果它没有任何项,则无法判断。此外,这也不能保证您拥有一组混合对象,或者想要使用基类而不是 GetType() 返回的派生类。如果您希望这样做,如果它没有实现 IEnumerable<T>(仅为 IEnumerable),则您可以将其视为类型 object - Chris Sinclair
@Chris 从目前为止的单元测试来看,检查已实现的接口看起来很有前途。 - user3559349
1个回答

6

已解决(使用Chris Sinclair发布的评论)

private static Type GetCollectionType(IEnumerable collection)
{
  Type type = collection.GetType();
  if (type.IsGenericType)
  {
    Type[] types = type.GetGenericArguments();
    if (types.Length == 1)
    {
      return types[0];
    }
    else
    {
      // Could be null if implements two IEnumerable
      return type.GetInterfaces().Where(t => t.IsGenericType)
        .Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
        .SingleOrDefault().GetGenericArguments()[0];
    }
  }
  else if (collection.GetType().IsArray)
  {
    return type.GetElementType();
  }
  // TODO: Who knows, but its probably not suitable to render in a table
  return null;
}

关于您对 SingleOrDefault 的使用以及您的评论“如果实现了两个IEnumerable,则可能为空”; 这是不正确的。 如果有多个,SingleOrDefault 将会 _抛出异常_。 即使它返回 null(如果它没有实现任何 IEnumerable<T> 接口),当您尝试调用 GetGenericArguments 时,您将直接获得 NullReferenceException。 我建议您拆分查询并执行更准确的检查。 - Chris Sinclair
另外,我个人更喜欢先进行接口检查,而不是默认使用第一个通用类型参数。虽然不太可能,但仍有可能有一个类定义像 class MyStuff<Foo> : IEnumerable<int>;在这种情况下,特别是如果您稍后对它们进行迭代(通过使用 foreach 或调用 GetEnumerator),您会希望优先考虑 int 类型(因为这将是被迭代的内容)而不是 Foo 类型。 - Chris Sinclair

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