LINQ a logování na příkladu logování kroků Dijsktrova algoritmu
Na LINQu je pěkné, jak jednoduše můžeme LINQ výraz upravit nebo jej bezbolestně rozšířit o další části. Nedávno jsem publikoval článek Dijsktrův alogritmus pomocí LINQu, extenzních metod a lambda výrazů a nyní si ukážeme drobnou úpravu v kódu, která způsobí, že se před každým rekurzivním voláním vždy vypíšou i prozatímní výsledky hledání nejkratší cesty.
Abychom mohli zalogovat výsledek, vytvoříme si vlastní extenzní metody pro výpis informací z předaného libovolného generického IEnumerable<T> do konzole.
static class MiscExtensions
{
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector, string beginString, string endString)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if(logDataSelector == null)
{
throw new ArgumentNullException("logDataSelector");
}
return innerLogToConsole(source, logDataSelector, beginString, endString);
}
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, Func<T, String> logDataSelector)
{
return LogToConsole(source, logDataSelector, null, null);
}
public static IEnumerable<T>LogToConsole<T>(this IEnumerable<T> source)
{
return LogToConsole(source, (obj => obj.ToString()), null, null);
}
public static IEnumerable<T> LogToConsole<T>(this IEnumerable<T> source, string beginString, string endString)
{
return LogToConsole(source, (obj => obj.ToString()), beginString, endString);
}
private static IEnumerable<T> innerLogToConsole<T>(IEnumerable<T> source, Func<T, String> selector, string beginString, string endString)
{
if (beginString != null)
{
Console.WriteLine(beginString);
}
foreach (var obj in source)
{
String val = selector(obj);
Console.WriteLine(val);
yield return obj;
}
if (endString != null)
{
Console.WriteLine(endString);
}
}
}
Metod pro logování máme více, abychom nemuseli pokaždé předat všechny argumenty. Prvním argumentem je vždy zdrojová sekvence, o jejíchž prvcích budou logovány informace. Argument logDataSelector nese odkaz na funkci, která umí z objektu ve zdrojové sekvenci získat jeho textovou reprezentaci. Jestliže delegát logDataSelector není předán, je k získání textové reprezentace objektu použita metoda ToString() zdrojového objektu. Další nepovinné argumenty beginString a endString jsou řetězce, které má extenzní funkce zapsat do konzole předtím, než jsou vypsána data o prvním objektu v zdrojové sekvenci (beginString), a po zalogování všech objektů v sekvenci (endString). V našem případě argumenty beginString a endString použijeme k vypsání řetězců, které ohraničí jednolivé kroky algoritmu. Naše extenzní funkce je “neinvazivní”, což znamená, že nefiltruje ani nekonvertuje objekty ve zdrojové sekvenci, ale po vypsání informace o zdrojovém objektu je nezměněný objekt příkazem yield return předán k dalšímu zpracování. Předchozí věta obsahuje varování, že nechcete-li se dočkat nepříjemných překvapení, delegát předaný v argumentu logDataSelector by neměl žádným způsobem měnit data zdrojového objektu, ale pouze je pasivně číst.
Celý algoritmus i s podrobným popisem už zde nebudu opakovat, vložím sem jen pro nás zajímavou rekurzivní metodu getShortestPathInner. Podpora logování je jednoduchou úpravou, protože pouze na námi vybraném neuralgickém místě v LINQ výrazu, které chceme špehovat, zavoláme naši extenzní funkci LogToConsole. Pro lepší orientaci je přidaný kód v následujícím výpisu zvýrazněn tučným červeným písmem.
private static IEnumerable<GraphPath<A0>> getShortestPathInner<A0, A1>(IEnumerable<GraphPath<A0>> initialGraphPath, IEnumerable<A0> processed, IEnumerable<A1> edges)
where A1 : IGraphEdge<A0>
{
var candidates = (from node in edges
where !processed.Contains(node.From)
select node.From).Distinct();
if (candidates.Count() == 0)
{
return initialGraphPath;
}
var minimum = initialGraphPath.Where(gPath => candidates.Contains(gPath.Current)).Min(gPath => gPath.TotalDistance);
var minimumGPath = (from gPath in initialGraphPath
where candidates.Contains(gPath.Current) &&
gPath.TotalDistance == minimum
select gPath).First();
var newGraphPath = from cNode in edges
where cNode.From.Equals(minimumGPath.Current)
select new GraphPath<A0>
{
Current = cNode.To,
Previous = minimumGPath.Current,
TotalDistance = cNode.Distance + minimumGPath.TotalDistance
};
var newGraphResult =
(initialGraphPath.Concat(newGraphPath).Where(obj =>
!initialGraphPath.Any(
obj2 => obj2.Current.Equals(obj.Current) &&
(obj2.TotalDistance < obj.TotalDistance))))
.LogToConsole(obj => String.Format("{0} - {1} - {2}",
obj.Previous, obj.Current, obj.TotalDistance),"--Další kolo algoritmu--", "--Konec kola--")
.ToArray();
var newProcessed = processed.Union(new[] { minimumGPath.Current });
return getShortestPathInner(newGraphResult, newProcessed, edges);
}
}
A zde je ukázka, jak vypadá výstup.
Logovat nemusíte jen do konzole, ale můžete si přidat další extenzní metody, které zohlední vaše speciální nároky, kam a jak se mají informace o objektech v sekvenci logovat. Cílem článku bylo jen ukázat, jak bezbolestné a hlavně elegantní je přidání logování do stávajících LINQ výrazů.
Tuesday, 03 March 2009 16:34:35 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ