我正在编写一个函数,它根据条目的数据库表查找目录的完整路径。每条记录都包含一个密钥,目录的名称和父目录的密钥(如果您熟悉,它是MSI中的目录表)。我有一个迭代的解决方案,但它开始看起来有点讨厌。我以为我可以编写一个优雅的尾递归解决方案,但我不确定了。
我会告诉你我的代码,然后解释我面临的问题。
Dictionary<string, string> m_directoryKeyToFullPathDictionary = new Dictionary<string, string>();
...
private string ExpandDirectoryKey(Database database, string directoryKey)
{
// check for terminating condition
string fullPath;
if (m_directoryKeyToFullPathDictionary.TryGetValue(directoryKey, out fullPath))
{
return fullPath;
}
// inductive step
Record record = ExecuteQuery(database, "SELECT DefaultDir, Directory_Parent FROM Directory where Directory.Directory='{0}'", directoryKey);
// null check
string directoryName = record.GetString("DefaultDir");
string parentDirectoryKey = record.GetString("Directory_Parent");
return Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey), directoryName);
}
当我意识到我遇到了问题(删除了一些小的验证/按摩)时,这就是代码的样子。我希望尽可能使用memoization来进行短路,但是这需要我对字典进行函数调用以存储递归ExpandDirectoryKey
调用的输出。我意识到我在那里也有一个Path.Combine
电话,但我认为这可以用... + Path.DirectorySeparatorChar + ...
来规避。
我考虑使用一个帮助方法来记忆目录并返回值,这样我就可以在上面函数的末尾像这样调用它:
return MemoizeHelper(
m_directoryKeyToFullPathDictionary,
Path.Combine(ExpandDirectoryKey(database, parentDirectoryKey)),
directoryName);
但我觉得这是作弊而不会被优化为尾递归。
有什么想法吗?我应该使用完全不同的策略吗?这根本不需要是一个超级高效的算法,我真的很好奇。我正在使用.NET 4.0,顺便说一句。
谢谢!
P.S。如果您对我的终止条件感到疑惑,请不要担心。我使用根目录预先编写了字典。
答案 0 :(得分:2)
即使你确实得到了正确的代码布局,你将遇到的第一个主要问题是C#编译器并不能真正支持尾递归。 Tail recursion in C#
在那里使用信息,你可以使它工作。但它充其量只是不优雅。
答案 1 :(得分:2)
你说你很好奇,我也是,让我们看看你对这种方法的看法。我正在概括你的问题以解释我的观点。我们假设您有Record
类型:
class Record
{
public int Id { get; private set; }
public int ParentId { get; private set; }
public string Name { get; private set; }
public Record(int id, int parentId, string name)
{
Id = id;
ParentId = parentId;
Name = name;
}
}
和此函数代表的“数据库”:
static Func<int, Record> Database()
{
var database = new Dictionary<int, Record>()
{
{ 1, new Record(1, 0, "a") },
{ 2, new Record(2, 1, "b") },
{ 3, new Record(3, 2, "c") },
{ 4, new Record(4, 3, "d") },
{ 5, new Record(5, 4, "e") },
};
return x => database[x];
}
让我们介绍一种“代表”递归的方法:
public static class Recursor
{
public static IEnumerable<T> Do<T>(
T seed,
Func<T, T> next,
Func<T, bool> exit)
{
return Do(seed, next, exit, x => x);
}
public static IEnumerable<TR> Do<T, TR>(
T seed,
Func<T, T> next,
Func<T, bool> exit,
Func<T, TR> resultor)
{
var r = seed;
while (true)
{
if (exit(r))
yield break;
yield return resultor(r);
r = next(r);
}
}
}
正如您所看到的,这根本不是递归,而是允许我们根据“看起来像”递归的部分来表达算法。我们的路径生成函数如下所示:
static Func<int, string> PathGenerator(Func<int, Record> database)
{
var finder = database;
return id => string.Join("/",
Recursor.Do(id, //seed
x => finder(x).ParentId, //how do I find the next item?
x => x == 0, //when do I stop?
x => finder(x).Name) //what do I extract from next item?
.Reverse()); //reversing items
}
此函数使用Recursor
来表示算法,并收集所有已生成的部分以组成路径。这个想法是Recursor
不会累积,它将累积的责任委托给调用者。我们将这样使用它:
var f = PathGenerator(Database());
Console.WriteLine(f(3)); //output: a/b/c
你也想要记忆,让我们来介绍一下:
public static class Memoizer
{
public static Func<T, TR> Do<T, TR>(Func<T, TR> gen)
{
var mem = new Dictionary<T, TR>();
return (target) =>
{
if (mem.ContainsKey(target))
return mem[target];
mem.Add(target, gen(target));
return mem[target];
};
}
}
有了这个,您可以记住您对昂贵资源(数据库)的所有访问,只需更改PathGenerator
中的一行,即可:
var finder = database;
到此:
var finder = Memoizer.Do<int, Record>(x => database(x));