我不时看到一种“感觉”错误的做法,但是我不能很清楚地指出它的错误之处。也许这只是我的偏见。

开发人员定义了一个使用布尔值作为其参数之一的方法,该方法调用了另一个,依此类推,最终使用该布尔值,仅用于确定是否采用一定的动作。例如,仅当用户具有特定权限时,或者也许(如果我们)处于(或不处于)测试模式,批处理模式或实时模式时,或者仅当系统处于运行状态时,才可以使用此操作来允许操作。某种状态。

总是有另一种方式来做到这一点,无论是通过查询何时该采取行动(而不是传递参数),还是通过该方法的多个版本,或者该类的多个实现。等等。我的问题不是如何改进此问题,而是它是否真的错了(如我所怀疑的那样),以及如果错了,这是什么问题。

评论

这是决策归属的问题。将决策移到中心位置,而不要乱扔它们。这将使复杂度低于每次有一个if时都具有两倍的代码路径。

马丁·福勒(Martin Fowler)对此发表了一篇文章:martinfowler.com/bliki/FlagArgument.html

另请参阅stackoverflow.com/questions/1240739/…和stackoverflow.com/questions/6107221/…

我并不总是同意尼克所说的,但在这种情况下,我100%同意:不要使用布尔参数。

我认为这个问题包括两个部分:1.对方法使用布尔变量,以及2.使用变量(布尔值与否)确定行为。第一部分显示了错误的设计决策,但是可以通过例如使用一个枚举。第二部分是一个更有趣的问题:根据某些特定变量切换行为的好设计吗?

#1 楼

是的,这可能是代码异味,这将导致难以维护的代码难以理解,并且容易重用的可能性较低。

正如其他张贴者所指出的那样,上下文就是一切(如果是一次性的,或者如果这种做法被公认为是故意招致的技术债务,则不要再费力气,以后再进行重构),但是广义上讲,如果有一个参数传递到选择要执行的特定行为的函数中,则需要进一步的逐步优化;将此功能分解为较小的功能将产生更高的内聚力。

那么什么是高内聚力功能?

这是一个仅执行一件事和一件事的函数。

您所描述的传入参数的问题在于,该函数执行的功能不只两件事。它可能会或可能不会检查用户的访问权限,具体取决于布尔参数的状态,然后根据该决策树执行某项功能。

最好将访问控制的关注点与任务,操作或命令的关注点分开。

您已经注意到,这些关注点之间的纠缠似乎已不复存在。 。

因此,“内聚性”概念可以帮助我们确定所讨论的函数不是高度内聚的,并且可以重构代码以生成一组更具内聚力的函数。

因此,这个问题可以重申。鉴于我们都同意最好避免传递行为选择参数,我们如何改善问题?

我会完全摆脱该参数。即使在测试时也具有关闭访问控制的能力,这是潜在的安全风险。出于测试目的,存根或模拟访问检查以测试允许的访问和拒绝访问的场景。

参考:凝聚力(计算机科学)

评论


Rob,您能否解释一下凝聚力是什么以及凝聚力如何应用?

–雷
2012年5月10日上午11:12

Ray,我对此的思考越多,我就越应该反对基于访问控制将布尔值引入应用程序的布尔值的安全漏洞。对代码库的质量改进将是一个不错的副作用;)

–Rob Kielty
2012年5月10日13:08

内聚力及其应用的很好解释。这确实应该获得更多选票。我也同意安全性问题...尽管如果它们都是私有方法,则潜在的漏洞较小

–雷
2012年5月10日13:39

谢谢雷。在时间允许的情况下,听起来很容易进行重构。可能值得在TODO评论中强调该问题,在技术权威和对我们有时要做的事情承受的压力的敏感性之间取得平衡。

–Rob Kielty
2012年5月10日14:00

“无法维持”

–aaa90210
18年1月12日在2:24

#2 楼

我很久以前就停止使用此模式,原因很简单;维修费用。几次我发现我有一些函数frobnicate(something, forwards_flag)在我的代码中被多次调用,并且需要找到代码中将false值作为forwards_flag值传递的所有位置。您无法轻松地搜索到这些内容,因此这成为维护工作的麻烦。而且,如果您需要在每个站点上进行错误修复,则可能会遇到一个不幸的问题。

但是可以在不从根本上改变方法的情况下轻松解决此特定问题:

 enum FrobnicationDirection {
  FrobnicateForwards,
  FrobnicateBackwards;
};

void frobnicate(Object what, FrobnicationDirection direction);
 


使用此代码,只需搜索FrobnicateBackwards的实例。虽然可能有一些代码将其分配给变量,所以您必须遵循一些控制线程,但我发现在实践中,这种替代方法行之有效的情况很少见。

但是至少原则上以这种方式传递标志是另一个问题。这就是说,某些(只有某些)具有这种设计的系统可能会将过多的关于代码的深层嵌套部分(使用标志)的实现细节暴露给外层(它们需要知道要传递哪个值)在此标志)。要使用Larry Constantine的术语,此设计在设置器和布尔标志的用户之间可能具有过强的耦合。弗兰基(Franky),尽管在不进一步了解代码库的情况下很难在这个问题上有任何确定性。

为了解决您提供的具体示例,我会在每个示例中都有一定程度的关注,但主要是出于风险/正确性的原因。就是说,如果您的系统需要传递指示系统处于什么状态的标志,则可能会发现您已经获得了应该考虑这一点但不检查参数的代码(因为它没有传递给此功能)。因此,您会遇到一个错误,因为有人忽略了传递参数。

值得承认的是,几乎所有需要传递给每个函数的系统状态指示器实际上都是全局变量。全局变量的许多缺点都将适用。我认为在许多情况下,最好将系统状态的知识(或用户的凭据或系统的身份)封装在一个对象中,该对象负责根据该数据正确执行操作。然后,您传递对该对象的引用,而不是原始数据。这里的关键概念是封装。

评论


真正的好例子,以及对我们正在处理的事物的性质及其对我们的影响的深刻见解。

–雷
2012年5月9日23:25

+1。我为此尽可能使用枚举。我见过一些函数,这些函数后来又添加了额外的布尔参数,并且调用开始看起来像DoSomething(myObject,false,false,true,false)。不可能弄清楚额外的布尔参数的含义,而对于有意义地命名的枚举值,这很容易。

– Graeme Perrow
2012年5月10日18:42

哦,老兄,最后是如何FrobnicateBackwards的一个很好的例子。一直在寻找这个。

– Alex Pritchard
15年6月24日在20:08

#3 楼

这不一定是错误的,但是它可能代表代码的味道。

关于布尔参数应避免的基本情况是:



<
然后,当您打电话时,通常会根据所需的确切行为分别调用public void foo(boolean flag) { doThis(); if (flag) doThat(); } foo(false)

这确实是一个问题,因为这是内聚力差的情况。您正在方法之间创建不必要的依赖关系。

在这种情况下,您应该做的是将foo(true)doThis保留为单独的公共方法,然后执行:

 doThat 




 doThis();
doThat();
 


那样,您无需创建耦合即可将正确的决定留给调用方(就像您要传递布尔参数一样)。

当然,并非所有布尔值参数的使用方式很糟糕,但肯定是代码的味道,如果您在源代码中看到很多内容,就很可疑了。

这只是解决此问题的一个示例基于我编写的示例的问题。在其他情况下,可能需要采用其他方法。

Martin Fowler的一篇很好的文章进一步解释了相同的想法。

PS:如果使用方法doThis(); 代替调用两个简单方法的实现更为复杂,那么您所要做的就是应用一个小的重构提取方法,因此生成的代码看起来类似于我编写的foo的实现。

评论


感谢您喊出“代码气味”一词-我知道它闻起来很难闻,但无法完全理解气味是什么。您的示例几乎完全符合我的预期。

–雷
2012年5月10日11:00

在很多情况下,foo()中的if(flag)doThat()是合法的。向每个调用者推送有关调用doThat()的决定会导致重复,如果您稍后发现某些方法,则必须删除该重复,标志行为也需要调用doTheOther()。我宁愿将依赖关系放在同一个类中,而不必以后去搜索所有调用方。


2012年5月10日13:46



@Blrfl:是的,我认为更直接的重构将是创建doOne和doBoth方法(分别用于false和true情况)或使用单独的枚举类型,如James Youngman所建议的那样。

–hugomg
2012年5月10日19:40

@missingno:您仍然存在将冗余代码推出给调用者以做出doOne()或doBoth()决策的相同问题。子例程/函数/方法具有自变量,因此可以改变其行为。如果参数的名称已经说明了它的作用,那么将枚举用于真正的布尔条件听起来很像重复自己。


2012年5月10日20:19

如果一个接一个地调用两个方法或一个方法被认为是多余的,那么编写try-catch块或一个if否则也是多余的。这是否意味着您将编写一个函数来抽象所有这些函数?没有!请注意,创建一个仅调用另一个方法的方法并不一定表示一个好的抽象。

– Alex
2012年5月10日20:46

#4 楼

首先,编程不是一门科学,而是一门艺术。因此,很少有“错误”和“正确”的编程方式。大多数编码标准只是一些程序员认为有用的“首选项”。但最终它们相当随意。因此,我永远不会将参数选择本身标记为“错误”,当然也不会像布尔参数那样泛泛而有用。在许多情况下,使用boolean(或int)封装状态是完全合理的。

总的来说,编码决策应主要基于性能和可维护性。如果性能不受影响(并且我无法想象您的示例中的性能如何),那么您的下一个考虑因素应该是:这对我(或未来的编校)而言将有多容易维护?它直观易懂吗?它是孤立的吗?在这方面,您的链接函数调用示例实际上似乎很脆弱:如果您决定将bIsUp更改为bIsDown,那么代码中还需要更改多少其他位置?另外,您的参数清单是否激增?如果您的函数具有17个参数,则可读性是一个问题,您需要重新考虑是否欣赏了面向对象体系结构的好处。

评论


我感谢第一段中的警告。我故意说出“错误”来挑衅,并且肯定地承认我们正在处理“最佳实践”和设计原则领域,并且这些事情通常都是情境性的,必须权衡多个因素

–雷
2012年5月9日23:23

您的回答使我想起一句话,我不记得它的来源-“如果您的函数有17个参数,则可能缺少一个”。

–乔里斯·蒂默曼斯
2012年5月10日上午10:25

我非常同意这一观点,并回答这个问题,是的,传递布尔值标志通常不是一个好主意,但是它从来没有像坏/好那样简单...

– JohnB
13年8月14日在14:31

#5 楼

我认为Robert C Martins Clean代码文章指出,应尽可能消除布尔型参数,因为它们表明一种方法可以完成多项工作。一种方法应该做一件事,而只有我认为这是他的座右铭之一。

评论


@dreza,您指的是柯利斯定律。

–马特·戴维(MattDavey)
13年6月6日在10:13

当然,根据经验,您应该知道什么时候应该忽略此类争论。

– gnasher729
18年6月25日在22:27

#6 楼

我认为这里最重要的是实用。

当布尔值确定整个行为时,只需再做一个方法即可。

当布尔值仅确定中间部分行为时,您可能希望保留它减少代码重复。在可能的情况下,您甚至可以将方法分为三部分:两种用于boolean选项的调用方法,另一种负责大部分工作。

例如:

 private void FooInternal(bool flag)
{
  //do work
}

public void Foo1()
{
  FooInternal(true);
}

public void Foo2()
{
  FooInternal(false);
}
 


当然,在实践中,您总会在这两个极端之间找到一点。通常,我只是选择感觉正确的东西,但是我宁愿在减少代码重复的方面犯错。

评论


我仅使用布尔参数来控制私有方法中的行为(如此处所示)。但问题是:如果某些dufus决定将来增加FooInternal的可见性,那又如何?

– ADTC
2014年10月15日在7:35

实际上,我会走另一条路。 FooInternal内部的代码应分为4部分:2个用于处理布尔值true / false情况的函数,一个用于发生在之前的工作,另一个用于在发生之后的工作。然后,您的Foo1变为:{doWork(); HandleTrueCase(); doMoreWork()}。理想地,将doWork和doMoreWork函数分别分解为(一个或多个)有意义的离散动作块(即,作为单独的函数),而不仅仅是为了分割的两个函数。

– jpaugh
17年6月14日在19:44



#7 楼

我喜欢通过返回不可变实例的生成器方法来自定义行为的方法。以下是番石榴Splitter的用法:

 private static final Splitter MY_SPLITTER = Splitter.on(',')
       .trimResults()
       .omitEmptyStrings();

MY_SPLITTER.split("one,,,,  two,three");
 


这样的好处是:


出色的可读性
配置与操作方法的清晰分离
通过迫使您考虑对象是什么,对象应该做什么和不应该做什么来促进内聚。在这种情况下,它是Splitter。您永远不会将someVaguelyStringRelatedOperation(List<Entity> myEntities)放在名为Splitter的类中,但是您会考虑将其作为静态方法放在StringUtils类中。
实例是预先配置的,因此可以很容易地进行依赖注入。客户端无需担心是将true还是false传递给一种方法来获得正确的行为。


评论


作为部分番石榴爱好者和传教士,我不喜欢您的解决方案...但是我不能给您+1,因为您跳过了我真正想要的部分,这是错误的(或有臭味的)关于另一种方式。我认为这实际上是您的某些解释中所含的内容,因此,如果您可以明确地表明这一点,则可以更好地回答该问题。

–雷
2012年5月10日上午11:04

我喜欢将配置方法和acton方法分开的想法!

– Sher10ck
2015年8月7日,下午5:01

到Guava库的链接已断开

–乔什·诺(Josh Noe)
17年7月21日在16:16

#8 楼

绝对有代码气味。如果它没有违反“单一责任原则”,那么它可能会违反“告诉,不要问”的原则。请考虑:


您可以总结不使用连词(和,但等等)的方法的作用吗?
方法的子部分是否可以轻松地分组为另一个较小的方法? (这包括复杂的局部变量创建。)
如果您的标志确定了要调用参数的方法,那么您的参数是否执行得过多?
此方法是否属于您的参数之一? (您可能会注意到它是否使用getters / setters在参数中挑选了实例变量。)

如果事实证明没有违反这两个原则之一,则仍应使用枚举。布尔标志是魔术数字的布尔等效项。 foo(false)bar(42)一样有意义。枚举对策略模式很有用,并且具有让您添加其他策略的灵活性。 (只要记住为它们起个适当的名字即可。)

您的特定示例特别困扰我。为什么此标志通过许多方法传递?听起来您需要将参数拆分为子类。

#9 楼

我很惊讶没有人提到命名参数。

我看到布尔标志的问题是它们损害了可读性。例如,true



 myObject.UpdateTimestamps(true);
 


做什么? ?我不知道。但是呢:

 myObject.UpdateTimestamps(saveChanges: true);
 


现在很明显我们传递的参数是什么意思要做的事情:我们要告诉函数保存其更改。在这种情况下,如果该类是非公共类,那么我认为boolean参数就可以了。


当然,您不能强迫您的类的用户使用named-parameters 。因此,根据您的默认情况的普遍程度,通常首选enum或两种单独的方法。 .Net正是这样做的:

 //Enum used
double a = Math.Round(b, MidpointRounding.AwayFromZero);

//Two separate methods used
IEnumerable<Stuff> ascendingList = c.OrderBy(o => o.Key);
IEnumerable<Stuff> descendingList = c.OrderByDescending(o => o.Key); 
 


评论


问题不在于什么比行为确定标志更可取,而是这种标志是否闻到气味,如果是,为什么?

–雷
2012年5月10日17:03

@Ray:我认为这两个问题没有区别。在可以强制使用命名参数的语言中,或者当可以确定将始终使用命名参数(例如私有方法)时,可以使用布尔参数。如果该语言(C#)无法强制使用命名参数,并且该类是公共API的一部分,或者该语言不支持命名参数(C ++),则可能会编写诸如myFunction(true)之类的代码,这是代码气味。

– BlueRaja-Danny Pflughoeft
2012年5月10日17:32



命名参数方法更加错误。没有名称,将被迫阅读API文档。使用名称,您认为不需要:但是参数可能会被误命名。例如,它本可以用于保存(所有)更改,但是后来实现略有更改,以便仅保存较大的更改(对于big的某些值)。

– Ingo
13年6月6日在14:16

@Ingo我不同意。那是一个通用的编程问题。您可以轻松定义另一个SaveBig名称。任何代码都可以搞砸,这种搞砸并不是特定于命名参数的。

–马滕·博德威斯(Maarten Bodewes)
18年5月11日在19:01

@Ingo:如果您被白痴所包围,那么您就去其他地方找工作。这类事情就是您需要代码审查的地方。

– gnasher729
18年5月11日在19:49



#10 楼

TL; DR:请勿使用布尔参数。

请参见下文,为什么它们不好,以及如何替换它们(以粗体显示)。


布尔值参数很难理解,因此很难维护。主要问题是,当您读取命名该参数的方法签名时,目的通常很清楚。但是,大多数语言通常不需要命名参数。因此,您将拥有像RSACryptoServiceProvider#encrypt(Byte[], Boolean)这样的反模式,其中的boolean参数确定函数中将使用哪种加密。

因此您将得到如下调用:

rsaProvider.encrypt(data, true);


读者必须在其中查找方法的签名,才能确定true到底意味着什么。传递整数当然也很糟糕:

rsaProvider.encrypt(data, 1);


会告诉您很多-或更少:即使您定义了要用于整数的常量,函数的用户也可能会忽略它们并继续使用文字值。

解决此问题的最佳方法是使用枚举。如果必须传递带有两个值的枚举RSAPaddingOAEPPKCS1_V1_5,则可以立即读取代码:

rsaProvider.encrypt(data, RSAPadding.OAEP);



布尔值只能有两个值。这意味着,如果您有第三种选择,则必须重构签名。通常,如果向后兼容是一个问题,则无法轻松执行,因此您必须使用其他公共方法扩展任何公共类。这就是Microsoft最终在引入RSACryptoServiceProvider#encrypt(Byte[], RSAEncryptionPadding)时所采用的方法,其中他们使用枚举(或至少一个模拟枚举的类)而不是布尔值。

如果需要对参数本身进行参数化,则甚至可以更容易地使用完整的对象或接口作为参数。在上面的示例中,可以使用哈希值对OAEP填充自身进行参数化,以供内部使用。请注意,现在有6种SHA-2哈希算法和4种SHA-3哈希算法,因此,如果仅使用单个枚举而不是参数,则枚举值的数量可能会激增(这可能是Microsoft下一步要发现的事情) )。


布尔参数也可能表明该方法或类的设计不正确。与上面的示例一样:.NET以外的任何加密库都根本不在方法签名中使用填充标志。


我喜欢警告的几乎所有软件专家布尔参数。例如,约书亚·布洛赫(Joshua Bloch)在备受赞誉的《有效Java》一书中警告他们。通常,不应使用它们。您可能会争辩说,如果存在一个易于理解的参数,则可以使用它们。但是即使这样:Bit.set(boolean)可能可以使用以下两种方法更好地实现:Bit.set()Bit.unset()。如果不能直接重构代码,则可以定义常量,至少可以使它们更具可读性:

const boolean ENCRYPT = true;
const boolean DECRYPT = false;

...

cipher.init(key, ENCRYPT);


比:

cipher.init(key, true);


更易读,即使您愿意:

cipher.initForEncryption(key);
cipher.initForDecryption(key);


代替。

#11 楼


我不太清楚这是怎么回事。


如果它看起来像是代码气味,感觉像是代码气味,而且-很好-闻起来像代码气味,这可能是代码的味道。

您想要做的是:

1)避免产生副作用的方法。

2)处理具有中央的正式状态机的必要状态(例如这样)。

#12 楼

我同意使用布尔参数不能确定性能的所有问题;改进,可读性,可靠性,降低复杂性,降低因封装和内聚力差以及可维护性导致的总体拥有成本降低的风险。

我从70年代中期开始设计硬件,我们现在将其称为SCADA(监控和控制)。数据采集​​),它们是经过微调的硬件,具有在EPROM中运行宏遥控器并收集高速数据的机器代码。

逻辑称为Mealey&Moore机器,我们现在称为有限状态机。还必须按照与上述相同的规则来完成这些操作,除非它是具有有限执行时间的实时计算机,然后必须完成快捷方式以达到目的。

数据是同步的,但是命令是异步和命令逻辑遵循无记忆布尔逻辑,但具有基于先前,当前和期望的下一个状态的存储的顺序命令。为了使它能够以最高效的机器语言(仅64kB)运行,我们非常谨慎地以启发式IBM HIPO方式定义每个流程。有时这意味着传递布尔变量并执行索引分支。

但是现在有了足够的内存和OOK的简便性,封装是当今必不可少的组成部分,但是当代码以字节为单位进行实时和SCADA计数时,这是一个代价。机器代码..

#13 楼

它不一定是错误的,但是在具体的操作示例中,取决于“用户”的某些属性,我将通过对用户的引用而不是标记。

这可以阐明并帮助您

任何人阅读调用语句都会意识到结果将根据用户而改变。

在最终被调用的函数中,您可以轻松实现更复杂的功能。业务规则,因为您可以访问任何用户属性。

如果“链”中的一个功能/方法根据用户属性做了不同的事情,则很有可能对用户属性也有类似的依赖性将介绍“链”中的其他一些方法。

#14 楼

大多数时候,我会认为这种编码不好。但是,我可以想到两种情况,这可能是一个好习惯。因为已经有很多答案说明了为什么不好,所以我两次给出可能很好的答案:

第一个是如果序列中的每个调用本身都有意义。如果将调用代码从true更改为false或从false更改为true,或者将被调用的方法更改为直接使用boolean参数而不是将其传递,则将是有意义的。连续十次这样的调用的可能性很小,但是可能会发生,并且如果这样做的话,这将是一种很好的编程习惯。

由于我们处理布尔值。但是,如果程序具有多个线程或事件,则传递参数是跟踪线程/事件特定数据的最简单方法。例如,一个程序可能从两个或多个套接字获取输入。为一个套接字运行的代码可能需要生成警告消息,而为另一个套接字运行的代码可能不需要。然后(某种程度上),对于一个非常高级别的布尔集有意义的是,它可以通过许多方法调用传递到可能生成警告消息的位置。数据无法以任何全局方式保存(非常困难的情况除外),因为多个线程或交错的事件都需要各自的值。

要确定,在后一种情况下可能创建一个仅包含布尔值的类/结构,并将其传递给它。几乎可以肯定,我很快就会需要其他字段,例如在哪里发送警告消息。

评论


即使使用您编写的类/结构(即上下文),枚举WARN,SILENT也更有意义。或者实际上只是在外部配置日志记录-无需传递任何内容。

–马滕·博德威斯(Maarten Bodewes)
18年5月11日在19:28



#15 楼

上下文很重要。这种方法在iOS中非常普遍。仅作为一个经常使用的示例,UINavigationController提供方法-pushViewController:animated:,而animated参数是BOOL。该方法在两种方式上执行的功能基本相同,但是如果您输入YES,则动画化从一个视图控制器到另一个视图控制器的过渡,如果您传递NO,则它不动画。这似乎是完全合理的。只能提供两种方法来替代这一方法是很愚蠢的,以便您可以确定是否使用动画。

在Objective-C中证明这种方法可能更容易,与使用C和Java这样的语言相比,方法命名语法为每个参数提供的上下文更多。不过,我认为采用单个参数的方法可以很容易地采用布尔值并且仍然有意义:

 file.saveWithEncryption(false);    // is there any doubt about the meaning of 'false' here?
 


评论


实际上,我不知道在file.saveWithEncryption示例中false的含义。这是否意味着无需加密即可保存?如果是这样,为什么在世界范围内该方法的名称中会带有“具有加密”的权限?我可以理解有一个类似save(boolean withEncryption)的方法,但是当我看到file.save(false)时,乍一看该参数表明使用或不使用加密都是不明显的。我认为,这实际上是詹姆斯·扬曼(James Youngman)关于使用枚举的第一点。

–雷
2012年5月9日23:20

另一种假设是,假的意思是不要覆盖任何同名的现有文件。我知道人为的示例,但是请确保您需要检查该功能的文档(或代码)。

–詹姆斯·杨曼(James Youngman)
2012年5月9日23:34

有时无法通过加密保存的名为saveWithEncryption的方法是一个错误。它应该是file.encrypt()。save(),或者像Java的新EncryptingOutputStream(new FileOutputStream(...))。write(file)一样。

– ChristopherHammarström
2012年5月10日14:09



实际上,执行除声明之外的其他功能的代码不起作用,因此是一个错误。它不会产生一个名为saveWithEncryption(boolean)的方法,该方法有时会不加密而保存,就像它不会产生一个以加密方式保存的saveWithoutEncryption(boolean)一样。

– ChristopherHammarström
2012年5月10日15:10

该方法是不好的,因为显然“这里对'假'的含义存在怀疑”。无论如何,我永远不会首先编写类似的方法。保存和加密是独立的操作,一种方法应该做一件事并且做得很好。请参阅我之前的评论,以获取有关如何执行此操作的更好示例。

– ChristopherHammarström
2012年5月10日17:27