4.5 结合 Option 和 IEnumerable 与 Bind
我已经提到,看待 Option 的一种方式是将其作为一个列表的特例,它既可以是空的(None),也可以正好包含一个值(Some)。你甚至可以在代码中表达这一点,把一个Option转换为一个IEnumerable,如下所示:
public struct Option<T>{
public IEnumerable<T> AsEnumerable()
{
if (IsSome)
yield return Value;
}
}
如果Option是Some,产生的IEnumerable将产生一个项目;如果是None,它将不产生任何项目。现在你可以访问所有定义在IEnumerable上的扩展方法,如下面的例子:
Some("thing").AsEnumerable().Count() // => 1
这真的有用吗?在实践中,IEnumerable经常被用来存储数据,而Option则在一个值不存在的时候跳过计算,所以它们的意图通常是不同的。
然而,在某些情况下,将它们结合起来是很有用的。在有些情况下,你可能会得到一个IEnumerable< Option< T > >,或者反过来说,一个Option<IEnumerable< T >>,而你可能想把它平铺成一个IEnumerable< T >。
例如,让我们回到调查的例子中,每个参与者被模拟为一个Subject,由于参与者可以选择披露他们的年龄,Subject.Age被模拟为一个Option< Age >:
class Subject {
public Option < Age > Age { get; set; }
}
IEnumerable < Subject > Population => new [] {
new Subject { Age = Age.Of(33) },
new Subject {}, //此人没有透露其年龄。
new Subject { Age = Age.Of(37) },
};
你把参与者的详细资料存储在一个IEnumerable< Subject >中。现在,假设你需要计算参与者的平均年龄,对于那些选择披露他们年龄的人。你可以怎么做呢?你可以从选择Age的所有值开始:
var optionalAges = Population.Map(p => p.Age);
// => [Some(Age(33)), None, Some(Age(37))]
如果你用Map来选择接受调查者的年龄,你会得到一个Option列表。 因为一个Option可以被看作是一个列表,所以optionalAges也可以被看作是一个列表。
为了将这种直觉转化为代码,让我们给Bind添加一些重载,将Option转换为IEnumerable,这样Bind就可以被应用,就好像我们在平铺一个嵌套的IEnumerable:
public static IEnumerable<R> Bind<T, R>
(this IEnumerable<T> list, Func<T, Option<R>> func)
=> list.Bind(t => func(t).AsEnumerable());
public static IEnumerable<R> Bind<T, R>
(this Option<T> opt, Func<T, IEnumerable<R>> func)
=> opt.AsEnumerable().Bind(func);
尽管根据FP理论,Bind应该只对一种类型的容器起作用,但是Option总是可以被 "提升 "到更通用的IEnumerable这一事实使得这些重载有效,并且在实践中相当有用:
- 第一个重载可以用来获得一个IEnumerable< T >,而Map会给你一个IEnumerable<Option< T >>,就像在目前的调查例子中。
- 第二个重载可以用来获得一个IEnumerable< T >,而Map会给你一个Option<IEnumerable < T > >。
在调查方案中,我们现在可以用Bind来过滤掉所有的Nones,得到所有实际给出的年龄列表:
var optionalAges = Population.Map(p => p.Age);
// => [Some(Age(33)), None, Some(Age(37))]
var statedAges = Population.Bind(p => p.Age);
// => [Age(33), Age(37)]
var averageAge = statedAges.Map(age => age.Value).Average();
// => 35
这使得我们可以利用Bind的 "扁平化 "特性来过滤掉所有的None情况。 前面的输出显示了同时调用Map和Bind的结果,以便你可以比较结果。