虽然我已经添加了关于“功能 if”又名条件表达式的响应。似乎需要更多的东西,那就是促进决策树。我在回复中提到使用 monads 是最好的方法。这是它是如何完成的。如果您之前没有使用过 monad,那么这可能看起来像是巫术,但它本质上是一种更好的方式来执行 @Gert Arnold 发布的内容。
我将采用 Gert 的基于客户适用性的决策树的想法。我将使用 LINQ,而不是使用带有 lambda 的流利风格来做出决定。这是 C# 对 monad 的原生支持。使用代码的结果将如下所示:
var client = new Client {
CriminalRecord = false,
UsesCreditCard = true,
YearsInJob = 10,
Income = 70000
};
var decision = from reliable in ClientDecisions.Reliable
from wealthy in ClientDecisions.Wealthy
select "They're reliable AND wealthy";
var result = decision(client);
if (result.HasValue) Console.WriteLine(result.Value);
decision = from reliableOrWealthy in Decision.Either(
ClientDecisions.Reliable,
ClientDecisions.Wealthy
)
from stable in ClientDecisions.Stable
select "They're reliable OR wealthy, AND stable";
result = decision(client);
if (result.HasValue) Console.WriteLine(result.Value);
decision = from reliable in ClientDecisions.Reliable
from wealthy in ClientDecisions.Wealthy
from stable in ClientDecisions.Stable
select "They're reliable AND wealthy, AND stable";
result = decision(client);
if (result.HasValue) Console.WriteLine(result.Value);
这种方法的美妙之处在于它是完全可组合的。您可以使用小的“决策部分”并将它们组合起来以做出更大的决策。完全不需要if。
在上面的代码中,您将看到ClientDecisions 静态类的用法。这包含一些可重复使用的决策部分:
public static class ClientDecisions
{
public static Decision<Client, Client> Reliable =>
from client in Decision.Ask<Client>()
where !client.CriminalRecord && client.UsesCreditCard
select client;
public static Decision<Client, Client> Wealthy =>
from client in Decision.Ask<Client>()
where client.Income > 100000
select client;
public static Decision<Client, Client> Stable =>
from client in Decision.Ask<Client>()
where client.YearsInJob > 2
select client;
}
注意这不是使用IEnumerable 或IQueryable。在 C# 中,可以为任何类型提供 Select、SelectMany 和 Where 方法,如果实现正确,您可以将任何类型变成一元类型。
有趣的是,在这种情况下,我将把一个委托变成一个 monad。这样做的原因是因为我们需要一个值传递到计算中。这是委托的定义:
public delegate DecisionResult<O> Decision<I,O>(I input);
以上示例中的输入为Client,输出为string。但请注意,上面的委托返回DecisionResult<O>,而不是O(即string)。这是因为我们通过计算传播了一个名为HasValue 的属性。如果HasValue 在任何时候是false,则计算结束。这使我们能够在计算期间做出决定,从而阻止其余的计算执行。这是DecisionResult<T> 类:
public struct DecisionResult<T>
{
private readonly T value;
public readonly bool HasValue;
internal DecisionResult(bool hasValue, T value = default(T))
{
this.value = value;
HasValue = hasValue;
}
public T Value =>
HasValue
? value
: throw new DecisionFailedException();
}
接下来,我们将添加几个帮助方法来生成 DecisionResult<T> 值。稍后将在Select、SelectMany 和Where 方法中使用它们。
public static class DecisionResult
{
public static DecisionResult<O> Nothing<O>() =>
new DecisionResult<O>(false);
public static DecisionResult<O> Return<O>(O value) =>
new DecisionResult<O>(true,value);
}
现在我们有了核心类型,我们可以将 Select、SelectMany 和 Where 扩展方法写入 Decision<I,O> 委托(是的,您可以为委托编写扩展方法!)
public static class Decision
{
public static Decision<I, V> Select<I, U, V>(this Decision<I, U> self, Func<U, V> map) =>
input =>
{
var res = self(input);
return res.HasValue
? DecisionResult.Return(map(res.Value))
: DecisionResult.Nothing<V>();
};
public static Decision<I, V> SelectMany<I, T, U, V>(
this Decision<I, T> self,
Func<T, Decision<I, U>> select,
Func<T, U, V> project) =>
input =>
{
var resT = self(input);
if (resT.HasValue)
{
var resU = select(resT.Value)(input);
return resU.HasValue
? DecisionResult.Return(project(resT.Value, resU.Value))
: DecisionResult.Nothing<V>();
}
else
{
return DecisionResult.Nothing<V>();
}
};
public static Decision<I, O> Where<I, O>(this Decision<I, O> self, Predicate<O> pred) =>
input =>
{
var res = self(input);
return res.HasValue
? pred(res.Value)
? DecisionResult.Return(res.Value)
: DecisionResult.Nothing<O>()
: DecisionResult.Nothing<O>();
};
}
这些方法允许在 LINQ 表达式中使用 Decision<I,O>。我们需要在Decision 类中添加几个额外的方法。第一个是Ask<I>,它只是获取传递给计算的值并允许在表达式中使用它。之所以将其命名为 Ask,是因为该 monad 与 Haskell 的标准 Reader monad 非常相似,并且按照约定称为 ask。
public static class Decision
{
public static Decision<I,I> Ask<I>() =>
input => DecisionResult.Return(input);
}
我们还希望允许条件操作,因此我们还将Either<I,O>(...) 添加到Decision。这需要任意数量的Decision<I,O> monad,一个接一个地遍历它们,如果成功则立即返回结果,否则继续。如果没有一个计算成功,那么整个计算将结束。这允许“或”类型行为和“切换”样式行为:
public static class Decision
{
public static Decision<I, O> Either<I, O>( params Decision<I, O>[] decisions ) =>
input =>
{
foreach(var decision in decisions)
{
var res = decision(input);
if( res.HasValue )
{
return res;
}
}
return DecisionResult.Nothing<O>();
};
}
我们不需要“逻辑与”,因为它可以实现为一系列from 表达式或where 表达式。
最后我们需要一个异常类型,以防程序员在.HasValue == false 时尝试在DecisionResult<T> 中使用.Value。
public class DecisionFailedException : Exception
{
public DecisionFailedException()
:
base("The decision wasn't made, and therefore doesn't have a value.")
{
}
}
使用这种技术(并基于扩展方法),您可以完全避免使用if。这是一种真正实用的技术。当然,这不是惯用的,但它是 C# 中最具声明性的方式。