Нулевые массивы и IEnumerable<T>
В предыдущем вопросе обсуждается IEnumerable и соглашение об использовании пустых коллекций вместо нулевых. Это хорошая практика, так как она устраняет множество ошибок, допускающих ошибки.
Но ответы не совсем касаются одного из случаев. Много раз я вынужден иметь дело с нулевыми значениями, особенно когда массивы возвращаются из сторонних методов. Пример:
(foreignObj.GetPeople() ?? Enumerable.Empty<Person>())
.Where(p => p.Name != "John")
.OrderBy(p => p.Name)
.Take(4);
Я написал вспомогательный метод, который несколько улучшает читабельность.
public class SafeEnumerable
{
public static IEnumerable<T> From<T>(T[] arr)
{
return arr ?? Enumerable.Empty<T>();
}
}
В результате чего:
SafeEnumerable.From(foreignObj.GetPeople())
.Where(p => p.Name != "John")
.OrderBy(p => p.Name)
.Take(4);
Я не против этого, но я ищу лучшие идеи. Кажется, я добавляю что-то, что должно быть уже там.
4 ответа
Проблема в том, где вы получили коллекцию (IEnumerable<T>
). Если вы всегда заняты проверкой null
Значения коллекции, вы должны рассмотреть, чтобы изменить источник. Например:
public User GetUser(long id) { }
public List<User> GetUsers(long companyId) { }
Первый метод имеет смысл, если он возвращает null
когда пользователь не найден, null
возвращаемое значение означает, что не найдено. Но второй метод, на мой взгляд, никогда не должен возвращать null
в любых нормальных обстоятельствах. Если пользователи не найдены, должен быть возвращен пустой список вместо null
значение, которое означает, что что-то в программе неверно. И приведенный пример в вашем вопросе я не верю directoryInfo.GetFiles("*.txt")
возвращает ноль, если нет txt
файл найден, вместо этого он должен вернуть пустую коллекцию.
Я создал серию методов расширения для IEnumerable, первым из которых является EmptyIfNull
например.
public static class EnumerableExtensions {
public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> collection) {
return collection ?? Enumerable.Empty<T>();
}
}
это позволяет мне делать
var q = foreignObj.GetPeople().EmptyIfNull().Where(p=>p.Name != "John").OrderBy(p => p.Name).Take(4);
Затем я добавил "безопасные" расширения, чтобы сделать код немного короче, чтобы его можно было печатать / было легче читать
например
public static IEnumberable<T> SafeWhere<T>(this collection<T> source,Func<T,bool> predicate) {
return source==null ? Enumerable.Empty<T>() : source.Where(predicate);
}
дающий
var q = foreignObj.GetPeople().SafeWhere(p=>p.Name != "John").OrderBy(p => p.Name).Take(4);
Если вы не можете изменить источник, чтобы исправить метод, который возвращает ноль, тогда ваш подход имеет смысл.
Возможно, вы могли бы сделать его методом расширения, чтобы его можно было использовать более идиоматическим способом LINQy:
var query = foreignObj.GetPeople()
.AsNonNullEnumerable()
.Where(p => p.Name != "John")
.OrderBy(p => p.Name)
.Take(4);
// ...
public static class EnumerableExtensions
{
public static IEnumerable<T> AsNonNullEnumerable<T>(this IEnumerable<T> source)
{
return source ?? Enumerable.Empty<T>();
}
}
К сожалению, я не думаю, что для этого есть что-то встроенное. Если вы не повторяете себя:
foreach(var item in (GetUsers() ?? new User[0])) // ...
Немного "лучшая" реализация (на примере того, что компилятор C# генерирует для yield return
sytnax), который немного меньше тратится на память:
class SafeEnumerable<T> : IEnumerable<T>, IEnumerator<T>
{
private IEnumerable<T> _enumerable;
public SafeEnumerable(IEnumerable<T> enumerable) { _enumerable = enumerable; }
public IEnumerator<T> GetEnumerator()
{
if (_enumerable == null)
return this;
else
return _enumerable.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
public T Current { get { throw new InvalidOperationException(); } }
object System.Collections.IEnumerator.Current { get { throw new InvalidOperationException(); } }
public bool MoveNext() { return false; }
public void Reset() { }
public void Dispose() { }
}