本文翻譯自Jon Skeet的系列博文“Edulinq”。
本篇原文地址:
這一篇繼續講非擴展方法。這次我們要講的是Empty,它有可能是最簡單的LINQ操作符了。
Empty是什麼?
Empty是一個泛型的,靜態的方法,它只有一個簽名形式,不接受任何參數:
public static IEnumerable<TResult> Empty<TResult>()
它返回一個特定類型的空序列。這就是它的唯一作用。
它的行爲只有一點比較有趣:文檔上說Empty會對空序列做緩存。換句話說,對於同一個類型參數來講,它每次都會返回同一個空序列。
我們要測試什麼?
能夠測試的東西也就只有兩點:
l 返回序列爲空。
l 對每個類型參數來說,返回值會被緩存起來。
和測試Range的時候的方法一樣,我們用一個叫做EmptyClass的別名來引用包含Empty的類型。下面是測試代碼:
[Test]
public void EmptyContainsNoElements()
{
using (var empty = EmptyClass.Empty<int>().GetEnumerator())
{
Assert.IsFalse(empty.MoveNext());
}
}
[Test]
public void EmptyIsASingletonPerElementType()
{
Assert.AreSame(EmptyClass.Empty<int>(), EmptyClass.Empty<int>());
Assert.AreSame(EmptyClass.Empty<long>(), EmptyClass.Empty<long>());
Assert.AreSame(EmptyClass.Empty<string>(), EmptyClass.Empty<string>());
Assert.AreSame(EmptyClass.Empty<object>(), EmptyClass.Empty<object>());
Assert.AreNotSame(EmptyClass.Empty<long>(), EmptyClass.Empty<int>());
Assert.AreNotSame(EmptyClass.Empty<string>(), EmptyClass.Empty<object>());
}
當然,以上代碼並不能證明緩存不是每個線程一份。不過,這些測試也夠了。
來動手實現吧!
現在看來,Empty的實現要比它的描述更有趣。如果不是要做緩存,我們可以這樣實現Empty:
// Doesn't cache the empty sequence
public static IEnumerable<TResult> Empty<TResult>()
{
yield break;
}
不過我們需要遵守關於緩存的文檔。要實現緩存其實也不難。有一個很方便的事實可以爲我們所用,空數組是不可變的。數組的長度是固定的,通常無法使一個數組是隻讀的。數組中的任何一個元素都是可以改變的。不過一個空數組是不包含任何元素的,所以也就沒有什麼可被改變的。這樣,我們就可以反覆的重用同一個數組了。
現在你可能會猜我會用Dictionary<Type, Array>來實現,不過我們可以利用一個小手段。在一個泛型類型中,可以用一個靜態變量來實現針對類型參數的緩存,因爲每一個傳入了類型參數的泛型類型的靜態變量都是不同的。
很不幸,Empty是一個非泛型類型中的方法。所以我們需要創建另一個泛型類型來包含緩存。這很容易做到,而且CLR還幫我們做到了線程安全的類型初始化。所以,我們最後的實現會是這樣的:
public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyHolder<TResult>.Array;
}
private static class EmptyHolder<T>
{
internal static readonly T[] Array = new T[0];
}
以上的實現遵守了所有的關於緩存的文檔,而且代碼行數也很少。不過這個實現方式需要你很好的瞭解.NET中泛型的工作方式。這種做法和我們上一篇採取的策略相反,我們選擇了一種比較難懂的方式,而沒有選擇使用字典的易懂的方式。不過我很滿意這種方案,因爲一旦你瞭解了泛型類型和靜態變量的工作方式,這段代碼就很簡單了。
結論
Empty的實現就是這樣的。下一個操作符Repeat有可能會更簡單,雖然它也要分成兩個方法來實現。
附錄
因爲以上講解的方法有點難懂,所以下面再提供另一種實現:
public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyEnumerable<TResult>.Instance;
}
#if AVOID_RETURNING_ARRAYS
private class EmptyEnumerable<T> : IEnumerable<T>, IEnumerator<T>
{
internal static IEnumerable<T> Instance = new EmptyEnumerable<T>();
// Prevent construction elsewhere
private EmptyEnumerable()
{
}
public IEnumerator<T> GetEnumerator()
{
return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this;
}
public T Current
{
get { throw new InvalidOperationException(); }
}
object IEnumerator.Current
{
get { throw new InvalidOperationException(); }
}
public void Dispose()
{
// No-op
}
public bool MoveNext()
{
return false; // There's never a next entry
}
public void Reset()
{
// No-op
}
}
#else
private static class EmptyEnumerable<T>
{
internal static readonly T[] Instance = new T[0];
}
#endif
這下大家都滿足了吧:)