使用Enumerable.Empty()的意外行为

本文关键字:意外 Enumerable Empty string 使用 | 更新日期: 2023-09-27 18:15:23

我希望Enumerable.Empty<string>()返回字符串的空数组。相反,它似乎返回一个具有单个null值的数组。这破坏了其他LINQ操作符,如DefaultIfEmpty,因为可枚举对象实际上不是空的。这似乎没有记录在任何地方,所以我想知道我是否错过了什么(99%的可能性)。

GameObject类

 public GameObject(string id,IEnumerable<string> keywords) {
        if (String.IsNullOrWhiteSpace(id)) {
            throw new ArgumentException("invalid", "id");
        }
        if (keywords==null) {
            throw new ArgumentException("invalid", "keywords");
        }
        if (keywords.DefaultIfEmpty() == null) { //This line doesn't work correctly.
            throw new ArgumentException("invalid", "keywords");
        }
        if (keywords.Any(kw => String.IsNullOrWhiteSpace(kw))) {
            throw new ArgumentException("invalid", "keywords");
        }
        _id = id;
        _keywords = new HashSet<string>(keywords);
    }

测试
    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void EmptyKeywords() {
        GameObject test = new GameObject("test",System.Linq.Enumerable.Empty<string>());
    }

使用Enumerable.Empty<string>()的意外行为

看起来您期望这个条件:

keywords.DefaultIfEmpty() == null

表示true。然而,如果源序列为空,DefaultIfEmpty返回一个包含元素类型默认值的单例序列(在本例中为string)。因此它将返回一个包含null的序列。但它本身不是null,所以条件返回false

你误解了DefaultIfEmpty的实现,这里是参考源代码的实现。

public static IEnumerable<TSource> DefaultIfEmpty<TSource>(this IEnumerable<TSource> source) {
    return DefaultIfEmpty(source, default(TSource));
}
public static IEnumerable<TSource> DefaultIfEmpty<TSource>(this IEnumerable<TSource> source, TSource defaultValue) {
    if (source == null) throw Error.ArgumentNull("source");
    return DefaultIfEmptyIterator<TSource>(source, defaultValue);
}
static IEnumerable<TSource> DefaultIfEmptyIterator<TSource>(IEnumerable<TSource> source, TSource defaultValue) {
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) {
            do {
                yield return e.Current;
            } while (e.MoveNext());
        }
        else {
            yield return defaultValue;
        }
    }
}

所以它所做的是,如果IEnumerable<T>不是空的,它只是返回IEnumerable<T>,如果IEnumerable<T>是空的,它返回新的IEnumerable<T>,其中有一个对象,值为default(T)。它将永远不会返回null,这是您的测试正在测试的。如果您想测试这个,您需要执行

if(keywords.DefaultIfEmpty().First() == null)

然而,这将导致IEnumerable<string>被计算多次。我会放弃LINQ,就像LINQ方法那样做,并且做得很长(这也消除了你在new HashSet<string>(keywords)内部的额外评估)。

 public GameObject(string id,IEnumerable<string> keywords) 
 {
    if (String.IsNullOrWhiteSpace(id)) {
        throw new ArgumentException("invalid", "id");
    }
    if (keywords==null) {
        throw new ArgumentException("invalid", "keywords");
    }
    _keywords = new HashSet<string>();
    using (var enumerator = keywords.GetEnumerator())
    {
        if (e.MoveNext())
        {
            do 
            {
                if(e.Current == null)
                    throw new ArgumentException("invalid", "keywords");
                _keywords.Add(e.Current);
            } while (e.MoveNext());
        }
        else 
        {
            throw new ArgumentException("invalid", "keywords");
        }
    }
    _id = id;
}

这使得您只循环一次IEnumerable<string> .

这解决你的问题了吗?

 public GameObject(string id, IEnumerable<string> keywords) {
    if (String.IsNullOrWhiteSpace(id)) {
        throw new ArgumentException("invalid", "id");
    }
    if (keywords == null || !keywords.Any()
            || keywords.Any(k => String.IsNullOrWhiteSpace(k))) {
        throw new ArgumentException("invalid", "keywords");
    }
    _id = id;
    _keywords = new HashSet<string>(keywords);
}

*根据@ScottChamberlain &@ginkner