老实说,我什至不明白这里发生了什么。
我也没有。该代码似乎实现了合同“如果c 是最小元素,则返回它,否则,返回a、b 和c 的中间元素,以及一些未知目的的可空性检查。
这是一个奇怪的实现合同,它让我认为它可能是一个未经测试的“返回中间元素”的实现,它有一个错误;如果c 在实践中从来都不是最小的元素,那么这个错误可能永远不会被捕获。
如何将其翻译成 C#?
理解上下文中代码的含义,然后在惯用的 C# 中实现这些语义。理解是重要的部分;一旦你理解了代码,编写一个正确的、可测试的实现就很简单了。
这是我的翻译。这是正确的吗?
private double bounds(double a, double? b, double? c) {
if (b != null) a = Math.Max(a, b.Value);
if (c != null) a = Math.Min(a, c.Value);
return a;
}
这是正确的,因为它准确地实现了原始代码的语义,可能有错误的代码。但也好不到哪里去。让我们改进它。突出三点:
- 该方法不消耗
this,所以应该是static。
- 该方法是 C# 方法,应遵循 C# 的命名约定。
- 覆盖其形式的方法在设计时难以阅读,在运行时也难以调试。就此而言,任何改变变量的方法都比不改变变量的方法更难理解。
- 变量名为
a、b、c,对它们有意义的变量难以阅读,因为它们掩盖了这些含义。
让我们解决所有问题。
private static double Bounds(double a, double? b, double? c)
{
double larger = (b == null) ? a : Math.Max(a, b.Value);
double smaller = (c == null) ? larger : Math.Min(larger, c.Value);
return smaller;
}
a、b 和 c 我们仍然没有好名字,但至少我们知道 larger 和 smaller 反映了它们的含义。
我们完成了吗?总是问自己我怎样才能让这更简单?
您可以通过将检查移到帮助程序中来使其更简单。
private static double Max(double x, double? y) =>
(y == null) ? x : Math.Max(x, y.Value)
private static double Min(double x, double? y) =>
(y == null) ? x : Math.Min(x, y.Value)
现在我们的方法变成了:
private static double Bounds(double a, double? b, double? c)
{
double larger = Max(a, b);
double smaller = Min(larger, c);
return smaller;
}
现在我们意识到我们可以将其重写为
private static double Bounds(double a, double? b, double? c) =>
Min(Max(a, b), c);
目前还不清楚这个东西的目的是什么,但我希望你同意我的实现在它的作用上再清楚不过了。在我的实现中,很明显语义是“如果c 最少,则返回它,否则返回中间元素”。将我的版本的可理解性与原始的可怕混乱进行比较,看看哪一个更容易理解。
这里的教训是什么?
- 将代码组织成单一赋值形式
- 将复杂的逻辑移入辅助方法,将其抽象出来
- 重写代码以消除不必要的解释变量。
- 代码变得简洁易懂。
进一步的思考;假设我的猜想是对的,意图是这样写:
private static double Middle(double test, double? bottom, double? top)
方法的语义是:
-
bottom 和 top 可能都为空。
- 如果两者都不为空,则
bottom 必须是较小的。
- 如果
bottom和top都不为null,则返回中间的值。
- 如果
bottom 和top 都为空,那么test 就是中间的值。
- 如果
bottom 为空但top 非空,则test 和top 中较小的值是中间的值。在这里,我们将“null”视为底部的“负无穷大”。
- 类似地,如果
top 为空但bottom 不为空,则bottom 和test 中较大的一个是中间的。这里的空顶部被视为“正无穷大”。
这实际上是一个非常明智的合约,但它要求我们知道bottom 总是小于top 当它们不都为空时。在 C# 中一个好的实现要么是 Debug.Assert 这个事实(如果方法是 private),要么是 throw 如果违反了先决条件(如果方法是 public)。
如果是这种情况,那么更好的选择是这样做:
private static double Middle(double test, double? bottom, double? top) =>
Middle(
test,
bottom ?? Double.NegativeInfinity,
top ?? Double.PositiveInfinity);
private static double Middle(double test, double bottom, double top) =>
Math.Min(Math.Max(test, bottom), top);
(如果你不熟悉??,则读作“coalesce”,x??y表示“如果x不为空,则使用x.Value,否则使用y”。这是一个非常有用的运算符。)
再一次,看看当您在编写代码时考虑到可读性时代码看起来多么漂亮和简单。很明显,“null 表示底部为负无穷大,顶部为正无穷大”,因为这就是代码所说的!
或者,编写一个正确的 Middle 实现,不需要对三个输入进行任何排序:
// True if test is between b1 and b2, false otherwise
private static bool Between(double test, double b1, double b2) =>
((b1 <= test) & (test <= b2)) | ((b2 <= test) & (test <= b1));
// Returns the middle value of a, b, and c.
private static double Middle(double a, double b, double c)
{
if (Between(a, b, c) return a;
if (Between(b, a, c) return b;
return c;
}
或者,如果你喜欢
private static double Middle(double a, double b, double c) =>
Between(a, b, c) ? a : Between(b, a, c) ? b : c;
现在我们可以再次轻松地编写 Bounds 方法:
private static double Bounds(double test, double? bottom, double? top) =>
Middle(
test,
bottom ?? Double.NegativeInfinity,
top ?? Double.PositiveInfinity);
再一次,想想我们在这里努力的目标。 做一件事并且做得非常好的简单方法。一旦有了这些,您就可以使用这些方法来构建其他简单的方法,并逐渐增加程序的复杂性,而不会使其难以理解。