我对如何最好地将代码重构为更易读的内容感到困惑。
考虑这段代码:
var foo = getfoo();
if(foo!=null)
{
var bar = getbar(foo);
if(bar!=null)
{
var moo = getmoo(bar);
if(moo!=null)
{
var cow = getcow(moo);
...
}
}
}
return;
如您所见,需要许多嵌套的if
块,因为每个嵌套if都依赖于先前的值。
现在我想知道如何让我的代码在这方面更清洁。
我自己想到的一些选择是:
ArgumentNullException
s,之后我会抓住它们并将return语句放在我的finally子句中(或在try / catch块之外)goto:
这些选项中的大多数看起来对我来说有点“脏”,所以我想知道是否有一种很好的方法来清理我创建的这个混乱。
答案 0 :(得分:45)
我会选择多个return
语句。这使代码易于阅读和理解。
出于显而易见的原因,不要使用goto
。
不要使用例外,因为您正在进行的检查并不例外,这是您可以预期的事情,因此您应该考虑到这一点。针对异常的编程也是一种反模式。
答案 1 :(得分:44)
考虑将空检查反转为:
var foo = getfoo();
if (foo == null)
{
return;
}
var bar = getbar(foo);
if (bar == null)
{
return;
}
...etc
答案 2 :(得分:27)
您可以链接表达式。赋值返回指定的值,因此您可以检查其结果。此外,您可以在下一个表达式中使用指定的变量。
一旦表达式返回false,其他表达式就不再执行,因为整个表达式都会返回false(因为and
操作)。
所以,这样的事情应该有效:
Foo foo; Bar bar; Moo moo; Cow cow;
if( (foo = getfoo()) != null &&
(bar = getbar(foo)) != null &&
(moo = getmoo(bar)) != null &&
(cow = getcow(moo)) != null )
{
..
}
答案 3 :(得分:25)
goto
完全可以接受(如果不可取)的几种情况之一。在这样的函数中,通常会有一些资源被分配,或者中间的状态变化需要在函数退出之前撤消。
基于回归的解决方案(例如rexcfnghk和Gerrie Schenck)的常见问题是您需要记住在每次返回之前撤消这些状态更改。这会导致代码重复,并打开微妙错误的大门,尤其是在较大的功能中。 不要这样做。
goto
的结构方法。请特别注意他们的示例代码取自Linux内核的copy_process
中的kernel/fork.c
。该概念的简化版如下:
if (!modify_state1(true))
goto cleanup_none;
if (!modify_state2(true))
goto cleanup_state1;
if (!modify_state3(true))
goto cleanup_state2;
// ...
cleanup_state3:
modify_state3(false);
cleanup_state2:
modify_state2(false);
cleanup_state1:
modify_state1(false);
cleanup_none:
return;
基本上,这只是“箭头”代码的一个更易读的版本,它不使用不必要的缩进或重复代码。这个概念很容易扩展到最适合您情况的任何内容。
作为最后一点,特别是关于CERT的第一个兼容示例,我只想补充一点,只要有可能,设计代码就更简单,以便可以一次性处理清理。这样,您可以编写如下代码:
FILE *f1 = null;
FILE *f2 = null;
void *mem = null;
if ((f1 = fopen(FILE1, "r")) == null)
goto cleanup;
if ((f2 = fopen(FILE2, "r")) == null)
goto cleanup;
if ((mem = malloc(OBJSIZE)) == null)
goto cleanup;
// ...
cleanup:
free(mem); // These functions gracefully exit given null input
close(f2);
close(f1);
return;
答案 4 :(得分:16)
首先你的建议(在每个if子句之后返回)是一个很好的出路:
// Contract (first check all the input)
var foo = getfoo();
if (Object.ReferenceEquals(null, foo))
return; // <- Or throw exception, put assert etc.
var bar = getbar(foo);
if (Object.ReferenceEquals(null, bar))
return; // <- Or throw exception, put assert etc.
var moo = getmoo(bar);
if (Object.ReferenceEquals(null, moo))
return; // <- Or throw exception, put assert etc.
// Routine: all instances (foo, bar, moo) are correct (not null) and we can work with them
...
第二种可能性(在你的情况下)是稍微修改你的getbar()以及getmoo()函数,使得它们在null输入时返回null,所以你将拥有
var foo = getfoo();
var bar = getbar(foo); // return null if foo is null
var moo = getmoo(bar); // return null if bar is null
if ((foo == null) || (bar == null) || (moo == null))
return; // <- Or throw exception, put assert(s) etc.
// Routine: all instances (foo, bar, moo) are correct (not null)
...
第三种可能性是在复杂的情况下你可以使用Null Object Desing Patteren
答案 5 :(得分:11)
老实说:
var foo;
var bar;
var moo;
var cow;
var failed = false;
failed = failed || (foo = getfoo()) == null;
failed = failed || (bar = getbar(foo)) == null;
failed = failed || (moo = getmoo(bar)) == null;
failed = failed || (cow = getcow(moo)) == null;
更清晰 - 没有箭头 - 并且可以永久延伸。
请不要转到Dark Side
并使用goto
或return
。
答案 6 :(得分:5)
var foo = getFoo();
var bar = (foo == null) ? null : getBar(foo);
var moo = (bar == null) ? null : getMoo(bar);
var cow = (moo == null) ? null : getCow(moo);
if (cow != null) {
...
}
答案 7 :(得分:3)
如果您可以更改正在调用的内容,则可以将其更改为永远不会返回null,而是改为NULL-Object。
这将允许您完全丢失所有ifs。
答案 8 :(得分:1)
另一种方法是使用“假”单循环来控制程序流。我不能说我会推荐它,但它看起来比箭头更好看,更具可读性。
添加“阶段”,“阶段”或类似该变量可以简化调试和/或错误处理。
int stage = 0;
do { // for break only, possibly with no indent
var foo = getfoo();
if(foo==null) break;
stage = 1;
var bar = getbar(foo);
if(bar==null) break;
stage = 2;
var moo = getmoo(bar);
if(moo==null) break;
stage = 3;
var cow = getcow(moo);
return 0; // end of non-erroreous program flow
} while (0); // make sure to leave an appropriate comment about the "fake" while
// free resources if necessary
// leave an error message
ERR("error during stage %d", stage);
//return a proper error (based on stage?)
return ERROR;
答案 9 :(得分:1)
try
{
if (getcow(getmoo(getbar(getfoo()))) == null)
{
throw new NullPointerException();
}
catch(NullPointerException ex)
{
return; //or whatever you want to do when something is null
}
//... rest of the method
这使方法的主要逻辑保持整洁,并且只有一个特殊的回报。它的缺点是如果get *方法很慢,它可能会很慢,并且很难在调试器中告诉哪个方法返回了null值。
答案 10 :(得分:1)
雷克斯克尔的回答确实非常好 如果你可以改变代码,Jens Schauder的答案可能更好(Null Object pattern)
如果您可以使示例更具体,您可能会获得更多答案 例如,根据方法的“位置”,您可以使用以下内容:
namespace ConsoleApplication8
{
using MyLibrary;
using static MyLibrary.MyHelpers;
class Foo { }
class Bar { }
class Moo { }
class Cow { }
internal class Program
{
private static void Main(string[] args)
{
var cow = getfoo()?.getbar()?.getmoo()?.getcow();
}
}
}
namespace MyLibrary
{
using ConsoleApplication8;
static class MyExtensions
{
public static Cow getcow(this Moo moo) => null;
public static Moo getmoo(this Bar bar) => null;
public static Bar getbar(this Foo foo) => null;
}
static class MyHelpers
{
public static Foo getfoo() => null;
}
}
答案 11 :(得分:0)
奇怪,没有人提到方法链。
如果您创建一个方法链接类
Public Class Chainer(Of R)
Public ReadOnly Result As R
Private Sub New(Result As R)
Me.Result = Result
End Sub
Public Shared Function Create() As Chainer(Of R)
Return New Chainer(Of R)(Nothing)
End Function
Public Function Chain(Of S)(Method As Func(Of S)) As Chainer(Of S)
Return New Chainer(Of S)(Method())
End Function
Public Function Chain(Of S)(Method As Func(Of R, S)) As Chainer(Of S)
Return New Chainer(Of S)(If(Result Is Nothing, Nothing, Method(Result)))
End Function
End Class
你可以在任何地方使用它来将任意数量的函数组合成一个执行序列来产生一个Result或Nothing(Null)
Dim Cow = Chainer(Of Object).Create.
Chain(Function() GetFoo()).
Chain(Function(Foo) GetBar(Foo)).
Chain(Function(Bar) GetMoo(Bar)).
Chain(Function(Moo) GetCow(Moo)).
Result
答案 12 :(得分:-3)
这是我使用goto 的一种情况。
您的示例可能不足以让我超越边缘,如果您的方法足够简单,则多次返回会更好。但是这种模式可能会相当广泛,最后你需要一些清理代码。虽然我可以使用大多数其他答案,但通常唯一清晰的解决方案是使用goto
。
(当你这样做时,一定要把标签的所有引用都放在一个块中,这样任何查看代码的人都知道goto
和变量都局限于代码的那一部分。)
在Javascript和Java中,你可以这样做:
bigIf: {
if (!something) break bigIf;
if (!somethingelse) break bigIf;
if (!otherthing) break bigIf;
// Conditionally do something...
}
// Always do something else...
return;
Javascript和Java没有goto
,这让我相信其他人已经注意到在这种情况下你做需要它们。
异常对我也有用,除了你强制调用代码的try / catch乱码。 同样,C#在throw上放入一个堆栈跟踪,这将减慢代码方式,特别是如果它通常在第一次检查时启动。