在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
扩展方法可能不受欢迎有几个原因。
-
有些人坚持认为,如果一个扩展方法的参数为
null
它应该会抛出一个exception。 如果方法名称清楚,我不同意。 -
应用过于广泛的扩展将会使自动完成菜单变得混乱。 这可以通过正确使用命名空间来避免,所以他们不会惹恼那些不想要它们的人。
我也玩过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(); }