检查通用的IEnumerable是否为空

6
假设我有一个对象,它可能是类型为 IEnumerable<T> 的。我想编写一个方法,如果该对象是类型为 IEnumerable<T>,不为空且不为 null,则返回 true。
以下是目前的代码:
public bool IsNullOrEmpty(object obj)
{
    if (obj != null)
    {
        if (obj is IEnumerable<object>)
        {
            return (obj as IEnumerable<object>).Any();
        }
    }
    return false;
}

如果我传递的对象是List<string>类型,那么这个方法可以正常工作,但如果我传递的对象是List<int>类型,则无法正常工作。它失败的原因是因为obj is IEnumerable<object>返回false

你有什么办法可以让这个方法适用于所有通用的IEnumerable吗?


8
坦白地说,"修复"这个问题是一个不好的想法;通常情况下,您不应该假设IEnumerable[<T>]可重复,因此:调用.Any()可能意味着您永远无法获取实际数据 - Marc Gravell
如果是包含一个元素 falseList<bool>,那么这个函数会出问题吗? - Dakotah Hicock
2
如果你想知道枚举是否为空,那么你应该返回Any()取反结果吗? - O. R. Mapper
@DakotahHicock 不会 - Any 没有 lambda 参数时不会尝试评估其内容 - 它只会检查任何成员的存在。 - D Stanley
@DStanley,谢谢您的澄清。我不知道这个用法,因为我从来没有想过要使用“Any”。 - Dakotah Hicock
1
@MarcGravell:可以说,这对于任何第一次调用Any()的情况都是正确的,无论是在此处显示的方法内部还是外部。 - O. R. Mapper
7个回答

16
由于类型可能是未知的,您可以尝试检查 IEnumerable 接口并在枚举器上使用 MoveNext()编辑:我更新了方法名称。现在逻辑更加合理,因为原始问题的代码是检查集合中是否有项目。
public bool IsNotNullOrEmpty(object enumerable)
{
    if (enumerable is IEnumerable e)
    {
        using (var enumerator = e.GetEnumerator())
            return enumerator.MoveNext();
    }
    return false;
}

2
你不能在一个对象上调用这个。 - Robert Harvey
@RobertHarvey 是的,我没有意识到类型事先是未知的。我会尝试更新。 - TyCobb
你的方法没有尝试使用 Dispose 处理使用 GetEnumerator 创建的 IEnumerator - Nathan A
"enumerable is IEnumerable" 实际上已经执行了 null 检查,因此您可以删除 if(enumerable != null) 这行代码。(https://msdn.microsoft.com/en-us/library/scekt9xw.aspx:is 表达式在提供的表达式不为空并且提供的对象可以被转换为提供的类型而不会引发异常时返回 true。) - Dominik Weber

10

System.Collections.Generic.IEnumerable<T>继承自System.Collections.IEnumerable,因此,如果您只想检查非泛型的IEnumerable而不是泛型IEnumerable<T>,那么您可以将其强制转换为IEnumerable

关于您的代码,有几点需要注意:您首先使用is进行检查,然后使用as进行类型转换。这通常是不必要的;as已经进行了检查并在转换失败时返回null。因此,更简短的方法是:

var enumerable = obj as IEnumerable;
if (enumerable != null) {
    return !enumerable.Cast<object>().Any();
}

请注意,您需要在此处增加对Cast的附加调用,因为Any需要一个通用的IEnumerable<T>


3

您可以尝试将其转换为 IEnumerable

public static bool IsNullOrEmpty<T>(this T obj) where T : class
{
    if (obj == null) return true;
    IEnumerable seq = obj as IEnumerable;
    if (seq != null) return !seq.Cast<object>().Any();
    return false;
}

...

List<int> list = new List<int>();
bool nullOrEmpty = list.IsNullOrEmpty();  // true

顺便说一下,有趣的是,它可以正确地处理空字符串:

bool nullOrEmpty = "".IsNullOrEmpty();   // true

@TimSchmelter: 什么是空的非null非集合引用类型? - O. R. Mapper
@O.R.Mapper:方法名是IsNullOrEmpty,这也是要求。如果我想要检查一个集合是否为空,我不需要一个新的方法。 - Tim Schmelter
@TimSchmelter:确实如此。因此,没有理由排除值类型。 - O. R. Mapper
@O.R.Mapper:值类型永远不为 null,也永远不为空。我也不明白为什么您需要对一个值类型如 4711 进行扩展方法。您可以将其与任何想要比较的内容进行比较。空到底是什么,int.MinValue0int.MaxValue等? - Tim Schmelter
@O.R.Mapper:因为按照定义,IsNullOrEmpty 有两种用法:1. 检查对象是否为 null(引用类型);2. 检查 IEnumerable 是否为空。顺便提一下,正如答案中所提到的,string 是一个有趣的例子,因为该方法也可以正确地替换 String.IsNullOrEmpty - Tim Schmelter
显示剩余12条评论

3

您可以检查非泛型的 IEnumerable 并检查其是否为空。您可以添加一个检查以确保对象使用反射实现了 IEnumerable<T>

public static bool IsNullOrEmpty(object obj)
{
    var e = obj as System.Collections.IEnumerable;
    if (e == null || !e.GetType().GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return false;

    foreach (object _ in e)
    {
        return true;
    }
    return false;
}

1
如果IEnumerator是一个IDisposable,在MoveNext调用之后,您是否想要Dispose它?foreach总是这样做的。 - Nathan A
@NathanA: s/IEnumerable/IEnumerator/ - O. R. Mapper
@NathanA - 我也这么认为,但非泛型的 IEnumerator 没有实现 IDisposable - Lee
1
@O.R.Mapper - 你不需要这样做,但这也是foreach的工作原理。我将其更新为使用foreach以保持一致性。 - Lee
@Lee:如果可能的话,我赞成处理枚举器。我的问题是针对您声称"IEnumerator不实现IDisposable"提出的。 - O. R. Mapper
显示剩余3条评论

2

为了完整起见,以下是最新的C#版本(从C#8.0开始)的最先进答案:

static bool IsNotNullOrEmpty<T>(object obj) => obj is IEnumerable<T> seq && seq.Any();
static bool IsNullOrEmpty<T>(object obj) => !(obj is IEnumerable<T> seq && seq.Any());

假设在调用的地方您将会知道T

如果您不知道T,那么您不应该使用IEnumerable<object>,而是应该使用IEnumerable。就像这样:

static bool IsNotNullOrEmpty(object obj) => obj is IEnumerable seq && seq.GetEnumerator().MoveNext();

1

您可以先检查对象是否实现了ICollection,例如列表和数组,因为检查这些的大小比它们具有Count属性更便宜。如果不是,则可以检查它是否实现了IEnumerable,如果是,则创建枚举器并查看是否可以移动到第一项:

public static bool IsNullOrEmpty(object obj) {
  ICollection c = obj as ICollection;
  if (c != null) {
    return c.Count == 0;
  }
  IEnumerable e = obj as IEnumerable;
  if (e != null) {
    return !e.GetEnumerator().MoveNext();
  }
  return false;
}

如果你只是想要 Count 字段,为什么要坚持使用 IList 而不是更一般、限制更少的 ICollection 呢? - O. R. Mapper
@O.R.Mapper:说得好。我在考虑 Enumerable.Count<T> 扩展方法的实现时,以为它会检查 IList,但当我重新检查代码时发现它实际上是检查 ICollection - Guffa

0

这个答案将问题从 IEnumerable<T> 的范围扩展到了“我有的东西能在 foreach 循环中使用吗?”。

正如你所知,foreach 主要不是检查 IEnumerable,而是尝试枚举任何给定的东西。

对于所有某种意义上可枚举的类型,foreach 都可以正常工作:

public bool IsNullOrEmpty(object obj)
{
  if (obj != null) // we can't be null
  {
    var method = obj.GetType().GetMethod("GetEnumerator");
    if (method != null) // we have an enumerator method
    {
      var enumerator = method.Invoke(obj, null);
      if (enumerator != null) // we got the enumerator
      {
        var moveMethod = enumerator.GetType().GetMethod("MoveNext")
        if (moveMethod != null) // we have a movenext method, lets try it
          return !(bool)moveMethod.Invoke(enumerator, null); // ie true = empty, false = has elements
      }
    }
  }
  return true; // it's empty, lets return true
}

这基本上与 foreach 循环做了相同的事情,即检查可枚举性并且不太关心所涉及的类型。
因此,它应该真正被避免,但如果您不知何故必须检查是否可以将您的类型放入 foreach 循环中,则应该使用此功能。
(Note: Please make sure to display Chinese characters properly on your device.)

有人给我点踩?愿意评论一下吗?你明白这比检查IEnumerable更通用吗?ForEach有点类似这样的功能。 - flindeberg
嗯...我认为使用反射而不是强制转换为IEnumerable是一个不好的想法。这会对未知代码施加很多限制,如果您使用IEnumerable,则接口实现者清楚GetEnumerator和MoveNext方法应该做什么。反射也会带来一些性能损失。 - Dominik Weber
@DominikvonWeber 我实际上正在执行与“foreach”实现相同的过程,并且我明确指出我正在检查可枚举性而不是IEnumerable/IEnumerable<T>实现(有很大的区别)。如果你对此不同意,请参考C#规范。在接受将其转换为IEnumerable之后,我添加了这个答案,因为我不认为那样就足够了,而且遵循foreach的行为将更符合框架的一致性。 - flindeberg

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