Запрос LINQ для выполнения проецирования, пропусков или обертывания исключений, где источник генерирует IEnumerable.GetNext()
Я бы хотел общее решение, но в качестве примера предположим, что у меня есть IEnumerable<string>
, где некоторые могут быть проанализированы как целые числа, а некоторые не могут.
var strings = new string[] { "1", "2", "notint", "3" };
Очевидно, если бы я сделал Select(s => int.Parse(s, temp))
, он перебрасывал бы исключение при перечислении.
В этом случае я мог бы сделать .All(s => int.TryParse(s, out temp))
во-первых, однако мне нужно общее решение, в котором я не должен дважды перечислять IEnumerable
.
В идеале я хотел бы иметь возможность сделать следующее, которое вызывает мой метод исключения исключений магии:
// e.g. parsing strings
var strings = new string[] { "1", "2", "notint", "3" };
var numbers = strings.Select(s => int.Parse(s)).SkipExceptions();
// e.g. encountering null object
var objects = new object[] { new object(), new object(), null, new object() }
var objecttostrings = objects.Select(o => o.ToString()).SkipExceptions();
// e.g. calling a method that could throw
var myClassInstances = new MyClass[] { new MyClass(), new MyClass(CauseMethodToThrow:true) };
var myClassResultOfMethod = myClassInstances.Select(mci => mci.MethodThatCouldThrow()).SkipExceptions();
Как я могу написать метод расширения SkipExceptions()
?
Некоторые отличные ответы для метода SelectSkipExceptions()
, однако мне интересно, может ли быть создан метод SkipExceptions()
в тех же строках, что и AsParallel()
.
Ответы
Ответ 1
Как насчет этого (возможно, вы захотите присвоить этому специальному Select Extension лучшее имя)
public static IEnumerable<TOutput> SelectIgnoringExceptions<TInput, TOutput>(
this IEnumerable<TInput> values, Func<TInput, TOutput> selector)
{
foreach (var item in values)
{
TOutput output = default(TOutput);
try
{
output = selector(item);
}
catch
{
continue;
}
yield return output;
}
}
Edit5
Добавлен оператор using, спасибо за предложение в комментариях
public static IEnumerable<T> SkipExceptions<T>(
this IEnumerable<T> values)
{
using(var enumerator = values.GetEnumerator())
{
bool next = true;
while (next)
{
try
{
next = enumerator.MoveNext();
}
catch
{
continue;
}
if(next) yield return enumerator.Current;
}
}
}
Однако это зависит от входящего IEnumerable, который уже не создается (и, следовательно, уже выбрал Exceptions), как список предыдущей функции. Например. это, вероятно, будет не, если вы вызываете это следующим образом: Выберите (..). ToList(). SkipExceptions()
Ответ 2
Создайте метод TryParseInt
, который возвращает Nullable<int>
:
int? TryParseInt(string s)
{
int i;
if (int.TryParse(s, out i))
return i;
return null;
}
И используйте его в своем запросе следующим образом:
var numbers = strings.Select(s => TryParseInt(s))
.Where(i => i.HasValue)
.Select(i => i.Value);
См. также эту статью Билла Вагнера, который представляет очень похожий случай.
Теперь я не думаю, что вы можете написать что-то вроде универсального метода SkipExceptions
, потому что вы слишком поздно поймаете исключение и закончите цикл Select
... Но вы, вероятно, могли бы написать SelectSkipException
:
public static IEnumerable<TResult> SelectSkipExceptions<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
if (source == null)
throw new ArgumentNullException("source");
if (selector == null)
throw new ArgumentNullException("selector");
return source.SelectSkipExceptionsIterator(selector);
}
private static IEnumerable<TResult> SelectSkipExceptionsIterator<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach(var item in source)
{
TResult value = default(TResult);
try
{
value = selector(item);
}
catch
{
continue;
}
yield return value;
}
}
Ответ 3
Даже принятый ответ не может быть "общим" достаточно. Что, если в какой-то день вы обнаружите, что вам нужно знать, какие исключения произошли?
Следующее расширение
static class EnumeratorHelper {
//Don't forget that GetEnumerator() call can throw exceptions as well.
//Since it is not easy to wrap this within a using + try catch block with yield,
//I have to create a helper function for the using block.
private static IEnumerable<T> RunEnumerator<T>(Func<IEnumerator<T>> generator,
Func<Exception, bool> onException)
{
using (var enumerator = generator())
{
if (enumerator == null)
yield break;
for (; ; )
{
//You don't know how to create a value of T,
//and you don't know weather it can be null,
//but you can always have a T[] with null value.
T[] value = null;
try
{
if (enumerator.MoveNext())
value = new T[] { enumerator.Current };
}
catch (Exception e)
{
if (onException(e))
continue;
}
if (value != null)
yield return value[0];
else
yield break;
}
}
}
public static IEnumerable<T> WithExceptionHandler<T>(this IEnumerable<T> orig,
Func<Exception, bool> onException)
{
return RunEnumerator(() =>
{
try
{
return orig.GetEnumerator();
}
catch (Exception e)
{
onException(e);
return null;
}
}, onException);
}
}
поможет. Теперь вы можете добавить SkipExceptions
:
public static IEnumerable<T> SkipExceptions<T>(this IEnumerable<T> orig){
return orig.WithExceptionHandler(orig, e => true);
}
Используя другой обратный вызов onException
, вы можете делать разные вещи
- Разбить итерацию, но игнорировать исключение:
e => false
- Попробуйте продолжить итерацию:
e => true
- Записать исключение и т.д.
Ответ 4
Здесь небольшая полная программа, чтобы продемонстрировать ответ, вдохновленный, возможно, монадой. Возможно, вы захотите изменить имя класса "Maybe", поскольку оно вдохновлено, а не фактически "возможно", как определено на других языках.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestMaybe
{
class Program
{
static void Main(string[] args)
{
var strings = new string[] { "1", "2", "notint", "3" };
var ints = strings.Select(s => new Maybe<string, int>(s, str => int.Parse(str))).Where(m => !m.nothing).Select(m => m.value);
foreach (var i in ints)
{
Console.WriteLine(i);
}
Console.ReadLine();
}
}
public class Maybe<T1, T2>
{
public readonly bool nothing;
public readonly T2 value;
public Maybe(T1 input, Func<T1, T2> map)
{
try
{
value = map(input);
}
catch (Exception)
{
nothing = true;
}
}
}
}
Изменить: в зависимости от потребностей вашего кода вы также можете захотеть nothing
установить true
, если результат map(input)
равен нулю.
Ответ 5
Это тот же самый ответ, что и у Томаса, но с выражением лямбда и LINQ. +1 для Томаса.
Func<string, int?> tryParse = s =>
{
int? r = null;
int i;
if (int.TryParse(s, out i))
{
r = i;
}
return r;
};
var ints =
from s in strings
let i = tryParse(s)
where i != null
select i.Value;
Ответ 6
Вы можете просто объединить метод Where и Select.
var numbers = strings.Where(s =>
{
int i;
return int.TryParse(s, out i);
}).Select(int.Parse);
Использование метода Where эффективно устраняет необходимость написания собственного метода SkipExceptions, потому что это в основном то, что вы делаете.