在C#中使用Maybe monad和扩展方法?

编辑2015这个问题和它的答案不再相关。 在C#6出现之前,有一个空传播运算符(?)被提出来,它避免了在这个问题和随后的答案中讨论的hacky-workarounds。 截至2015年,在C#中,您现在应该使用Form.ActiveForm?.ActiveControl?.Name。


我一直在思考在.NET中的空传播问题,这往往导致丑陋,重复的代码,如下所示:

尝试#1通常的代码:

string activeControlName = null; var activeForm = Form.ActiveForm; if (activeForm != null) { var activeControl = activeForm.ActiveControl; if(activeControl != null) { activeControlname = activeControl.Name; } } 

关于一个Maybe <T> monad,或者使用某种“if not null”扩展方法,已经在StackOverflow上进行了一些讨论:

尝试#2,扩展方法:

 // Usage: var activeControlName = Form.ActiveForm .IfNotNull(form => form.ActiveControl) .IfNotNull(control => control.Name); // Definition: public static TReturn IfNotNull<TReturn, T>(T instance, Func<T, TReturn> getter) where T : class { if (instance != null ) return getter(instance); return null; } 

我觉得这样比较好,但是有一些句法混乱,重复的“IfNotNull”和lambdas。 我现在正在考虑这个devise:

尝试#3,也许有扩展方法的<T>

 // Usage: var activeControlName = (from window in Form.ActiveForm.Maybe() from control in window.ActiveControl.Maybe() select control.Name).FirstOrDefault(); // Definition: public struct Maybe<T> : IEnumerable<T> where T : class { private readonly T instance; public Maybe(T instance) { this.instance = instance; } public T Value { get { return instance; } } public IEnumerator<T> GetEnumerator() { return Enumerable.Repeat(instance, instance == null ? 0 : 1).GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } public static class MaybeExtensions { public static Maybe<T> Maybe<T>(this T instance) where T : class { return new Maybe<T>(instance); } } 

我的问题是 :这是一个邪恶的扩展方法的滥用? 它比旧的空检查好吗?

有趣的是,如此多的人独立select名称IfNotNull ,在C#中这是一个最明智的名字! 🙂

最早在SO上发现的一个: 使用这种(基于扩展方法)速记的可能的缺陷

我的一个(无知的上述): pipe道向前在C#

另一个更近的例子: 如何检查深度lambdaexpression式中的空值?

IfNotNull扩展方法可能不受欢迎有几个原因。

  1. 有些人坚持认为,如果一个扩展方法的参数为null它应该会抛出一个exception。 如果方法名称清楚,我不同意。

  2. 应用过于广泛的扩展将会使自动完成菜单变得混乱。 这可以通过正确使用命名空间来避免,所以他们不会惹恼那些不想要它们的人。

我也玩过IEnumerable方法,就像试验一样,看看有多less东西可以适应Linq关键字,但是我认为最终的结果是比IfNotNull链或原始的命令式代码更不可读。

我已经结束了一个简单的自包含的Maybe类与一个静态方法(而不是扩展方法),这对我来说工作非常好。 但是,我和一个小团队一起工作,而我的下一个最高级的同事则对函数式编程和lambda等感兴趣,所以他不会被推迟。

就像我是扩展方法的粉丝一样,我不认为这是真正有用的。 你仍然得到了expression式的重复(在monadic版本),这只是意味着你必须向每个人解释Maybe 。 在这种情况下,添加的学习曲线似乎没有足够的好处。

IfNotNull版本至less可以避免重复,但是我认为它还是有点太过于冗长而没有更清晰。

也许有一天我们会得到一个无效的解引用操作符…


就像旁边一样,我最喜欢的半邪恶扩展方法是:

 public static void ThrowIfNull<T>(this T value, string name) where T : class { if (value == null) { throw new ArgumentNullException(name); } } 

这可以让你把这个:

 void Foo(string x, string y) { if (x == null) { throw new ArgumentNullException(nameof(x)); } if (y == null) { throw new ArgumentNullException(nameof(y)); } ... } 

成:

 void Foo(string x, string y) { x.ThrowIfNull(nameof(x)); y.ThrowIfNull(nameof(y)); ... } 

这个参数名称还有一个令人讨厌的重复,但至less它更加整洁。 当然,在.NET 4.0中,我会使用Code Contracts,这就是我现在要写的关于Stack Overflow的好工作回避;)

如果你想要一个扩展方法来减less嵌套的if,就像你所做的那样,你可以尝试这样的事情:

 public static object GetProperty(this object o, Type t, string p) { if (o != null) { PropertyInfo pi = t.GetProperty(p); if (pi != null) { return pi.GetValue(o, null); } return null; } return null; } 

所以在你的代码中你只需要做:

 string activeControlName = (Form.ActiveForm as object) .GetProperty(typeof(Form),"ActiveControl") .GetProperty(typeof(Control),"Name"); 

我不知道是否由于反思迟缓,我是否经常使用它,而且我并不认为这比替代方法好得多,但它应该起作用,不pipe你是否沿着办法…

(注意:我可能已经把这些types混合起来):)

如果你正在处理C#6.0 / VS 2015及以上版本,他们现在有一个内置的空传播解决scheme:

 string ans = nullableString?.Length.ToString(); // null if nullableString == null, otherwise the number of characters as a string. 

最初的样本工作,一目了然是最容易阅读的。 真的需要改进吗?

IfNotNull解决scheme是最好的(直到C#团队给我们一个空安全的解引用操作符,即)。

我对任何解决scheme都不是太疯狂。 原文的简单版本出了什么问题:

 string activeControlName = null; if (Form.ActiveForm != null) if (Form.ActiveForm.ActivControl != null) activeControlname = activeControl.Name; 

如果不是这样的话,那么我会考虑编写一个NotNullChain或FluentNotNull对象,它可以将一些非空testing链接起来。 我同意IfNotNull扩展方法作用于null看起来有点奇怪 – 即使扩展方法只是语法糖。

我认为Mark Synowiec的答案可能是通用的。

恕我直言,我认为C#核心团队应该看看这个“问题”,虽然我认为有更大的事情要解决。

当然,原始的2嵌套IF比其他select更可读。 但是build议你想更普遍地解决问题,这里是另一个解决scheme:

 try { var activeForm = Form.ActiveForm; assumeIsNotNull(activeForm); var activeControl = activeForm.ActiveControl; assumeIsNotNull(activeControl); var activeControlname = activeControl.Name; } catch (AssumptionChainFailed) { } 

哪里

 class AssumptionChainFailed : Exception { } void assumeIsNotNull(object obj) { if (obj == null) throw new AssumptionChainFailed(); }