Это перевод пятой части серии статей Джона Скита "Реализация LINQ to Objects". С оригинальной версией поста можно ознакомиться здесь.
Продолжая с методами, не являющимися методами расширения, пришло время для, возможно, самого простого оператора LINQ – Empty.
Что это?Empty – это обобщенный статический метод с единственной сигнатурой, не принимающий параметров:
public static IEnumerable<TResult> Empty<TResult>()
Он возвращает пустую последовательность соответствующего типа. Это все, что он делает.
Есть только один интересный момент: документировано, что Empty кеширует пустую последовательность. Иначе говоря, он возвращает ссылку на ту же самую пустую коллекцию, каждый раз когда вы его вызываете (для того же самого аргумента типа, естественно).
Что мы собираемся тестировать?Существуют только две вещи, которые мы можем здесь протестировать:
- Конечная последовательность пуста
- Конечная последовательность кешируется для каждого типа аргумента
Я использую тот же подход, что и с Range для вызова статического метода, но в этот раз с псевдонимом для EmptyClass. Вот тесты:
[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>());
}
Конечно, они не проверяют, что последовательность кэшируется для каждого потока отдельно, но сойдёт и так.
Давайте имплентируем это!На самом деле, реализация немного интереснее, чем можно было предположить из описания. Если бы не аспекты кеширования, реализация могла бы выглядеть следующим образом:
// Не кэшировать пустую последовательность
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. В некотором смысле, это противоположность ситуации, возникшей в предыдущем посте – хитрая реализация вместо медленной, но возможно более простой, основанной на dictionary. В данном случае меня устраивает компромисс, потому что как только вы поймете, как работают обобщенные типы и статические переменные, код покажется простым. Это тот случай, когда простота для каждого своя.
ЗаключениеИтак, это 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>();
// Предотвращаем создание в другом месте
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()
{
// Пустой метод
}
public bool MoveNext()
{
return false; // Следующего элемента никогда не будет
}
public void Reset()
{
// Пустой метод
}
}
#else
private static class EmptyEnumerable<T>
{
internal static readonly T[] Instance = new T[0];
}
#endif
Надеюсь, теперь каждый сможет собрать версию, которой он будет доволен :)
Комментариев нет:
Отправить комментарий