我的一位同事表示,布尔值作为方法参数是不可接受的。它们应由枚举代替。起初我没有看到任何好处,但他给了我一个例子。
什么更容易理解?
file.writeData( data, true );
或者
enum WriteMode {
Append,
Overwrite
};
file.writeData( data, Append );
现在我明白了!;-)
这绝对是一个示例,其中枚举作为第二个参数使代码更具可读性。
那么,您对这个话题有何看法?
我的一位同事表示,布尔值作为方法参数是不可接受的。它们应由枚举代替。起初我没有看到任何好处,但他给了我一个例子。
什么更容易理解?
file.writeData( data, true );
或者
enum WriteMode {
Append,
Overwrite
};
file.writeData( data, Append );
现在我明白了!;-)
这绝对是一个示例,其中枚举作为第二个参数使代码更具可读性。
那么,您对这个话题有何看法?
布尔值代表“是/否”的选择。如果要表示“是/否”,则使用布尔值,它应该是不言自明的。
但是,如果它是在两个选项之间进行选择,而这两个选项都不是明确的是或否,那么枚举有时会更具可读性。
枚举还允许将来进行修改,您现在需要第三个选择(或更多)。
使用最适合您的问题的模型。在您给出的示例中,枚举是一个更好的选择。但是,在其他时候布尔值会更好。这对你来说更有意义:
lock.setIsLocked(True);
或者
enum LockState { Locked, Unlocked };
lock.setLockState(Locked);
在这种情况下,我可能会选择布尔选项,因为我认为它非常清晰明确,而且我很确定我的锁不会有两个以上的状态。尽管如此,第二个选择是有效的,但不必要的复杂,恕我直言。
对我来说,使用布尔值和枚举都不是一个好方法。Robert C. Martin 在他的Clean Code Tip #12: Eliminate Boolean Arguments中非常清楚地捕捉到了这一点:
布尔参数大声声明该函数不止一件事。它们令人困惑,应该被消除。
如果一个方法做不止一件事,您应该编写两种不同的方法,例如在您的情况下:file.append(data)
和file.overwrite(data)
.
使用枚举并不能使事情更清楚。它没有改变任何东西,它仍然是一个标志参数。
我认为您几乎自己回答了这个问题,我认为最终目标是使代码更具可读性,在这种情况下,枚举做到了,IMO 始终最好查看最终目标而不是一揽子规则,也许多想一下作为准则,即枚举在代码中通常比通用布尔值、整数等更具可读性,但规则总会有例外。
还记得古巴导弹危机期间阿德莱史蒂文森向联合国驻联合国大使佐林提出的问题吗?
“你现在在世界舆论的法庭上,你可以回答 是或否。你否认[导弹]存在,我想知道我是否正确理解你......我准备等待直到地狱结冰为止,如果这是你的决定的话。”
如果您在方法中使用的标志具有这样一种性质,您可以将其固定为二元决策,并且该决策永远不会变成三向或多向决策,请选择布尔值。适应症:你的旗帜叫做isXXX。
如果是模式开关,请不要将其设为布尔值。在编写方法时,总有一种模式比你想象的要多。
一种模式的困境例如困扰着 Unix,其中文件或目录今天可能具有的可能的权限模式会导致模式的奇怪双重含义,具体取决于文件类型、所有权等。
我遇到这件事的原因有两个:
因为有些人会写这样的方法:
ProcessBatch(true, false, false, true, false, false, true);
这显然很糟糕,因为混淆参数太容易了,而且你不知道你在指定什么。不过,只有一个布尔值并不算太糟糕。
因为通过简单的是/否分支控制程序流可能意味着您有两个完全不同的功能,它们以一种尴尬的方式包装成一个。例如:
public void Write(bool toOptical);
真的,这应该是两种方法
public void WriteOptical();
public void WriteMagnetic();
因为这些代码可能完全不同;他们可能必须进行各种不同的错误处理和验证,甚至可能必须以不同的方式格式化输出数据。您不能仅通过使用Write()
或什至来判断这一点Write(Enum.Optical)
(当然,如果您愿意,您可以让这些方法中的任何一个只调用内部方法 WriteOptical/Mag)。
我想这取决于。除了#1,我不会对此大惊小怪。
枚举更好,但我不会将布尔参数称为“不可接受的”。有时输入一个小布尔值并继续前进会更容易(想想私有方法等)
在具有命名参数的语言(如 Python 和 Objective-C)中,布尔值可能没问题,因为名称可以解释参数的作用:
file.writeData(data, overwrite=true)
或者:
[file writeData:data overwrite:YES]
我不同意这是一个好规则。显然,Enum 在某些情况下提供了更好的显式或详细代码,但通常它似乎超出了范围。
首先让我举个例子:程序员编写好的代码的责任(和能力)并没有真正受到布尔参数的危害。在您的示例中,程序员可以通过编写以下代码来编写冗长的代码:
dim append as boolean = true
file.writeData( data, append );
或者我更喜欢更一般的
dim shouldAppend as boolean = true
file.writeData( data, shouldAppend );
第二:您提供的 Enum 示例只是“更好”,因为您正在传递一个 CONST。在大多数应用程序中,传递给函数的参数很可能至少有一些(如果不是大部分)是变量。在这种情况下,我的第二个示例(给出具有好名称的变量)要好得多,而 Enum 不会给您带来什么好处。
枚举有一个明确的好处,但你不应该只是用枚举替换所有的布尔值。在很多地方,真/假实际上是表示正在发生的事情的最佳方式。
但是,将它们用作方法参数有点可疑,仅仅是因为如果不深入研究它们应该做什么,你就看不到它们,因为它们让你看到真/假的实际含义
属性(尤其是使用 C#3 对象初始化程序)或关键字参数(la ruby 或 python)是一种更好的方法,可以在其他情况下使用布尔参数。
C# 示例:
var worker = new BackgroundWorker { WorkerReportsProgress = true };
红宝石示例
validates_presence_of :name, :allow_nil => true
Python 示例
connect_to_database( persistent=true )
我唯一能想到的布尔方法参数是正确的做法是在 java 中,你没有属性或关键字参数。这是我讨厌 java 的原因之一 :-(
虽然在许多情况下枚举确实比布尔值更具可读性和可扩展性,但“布尔值不可接受”的绝对规则是愚蠢的。它不灵活且适得其反——它不会为人类判断留下空间。它们是大多数语言中的基本内置类型,因为它们很有用 - 考虑将其应用到其他内置类型:例如说“永远不要使用 int 作为参数”会很疯狂。
这条规则只是风格问题,而不是潜在的错误或运行时性能问题。更好的规则是“出于可读性的原因,更喜欢枚举而不是布尔值”。
看看 .Net 框架。布尔值用作很多方法的参数。.Net API 并不完美,但我不认为使用布尔值作为参数是一个大问题。工具提示总是给你参数的名称,你也可以建立这种指导——在方法参数上填写你的 XML 注释,它们会出现在工具提示中。
我还应该补充一点,在某些情况下,您应该清楚地将布尔值重构为枚举 - 当您的类或方法参数中有两个或多个布尔值时,并非所有状态都是有效的(例如,拥有它们是无效的两者都设置为真)。
例如,如果您的课程具有以下属性
public bool IsFoo
public bool IsBar
同时让它们都为真是错误的,你实际上得到的是三个有效状态,更好地表达为:
enum FooBarType { IsFoo, IsBar, IsNeither };
您的同事可能会更好地遵守的一些规则是:
仅当您不打算扩展框架的功能时,才可以接受布尔值。Enum 是首选,因为您可以扩展 enum 而不会破坏函数调用的先前实现。
Enum 的另一个优点是更易于阅读。
如果该方法提出以下问题:
KeepWritingData (DataAvailable());
在哪里
bool DataAvailable()
{
return true; //data is ALWAYS available!
}
void KeepWritingData (bool keepGoing)
{
if (keepGoing)
{
...
}
}
布尔方法参数似乎具有绝对完美的意义。
这取决于方法。如果该方法所做的某些事情很明显是真/假,那很好,例如下面[虽然我不是说这是该方法的最佳设计,它只是一个使用明显的示例]。
CommentService.SetApprovalStatus(commentId, false);
但是在大多数情况下,例如您提到的示例,最好使用枚举。在 .NET Framework 本身中有许多不遵循此约定的示例,但那是因为他们在周期的后期引入了此设计指南。
它确实使事情变得更加明确,但确实开始大量扩展接口的复杂性 - 在纯粹的布尔选择中,例如附加/覆盖它似乎有点过头了。如果您需要添加更多选项(在这种情况下我无法想到),您始终可以执行重构(取决于语言)
枚举当然可以使代码更具可读性。还有一些事情需要注意(至少在 .net 中)
因为枚举的底层存储是 int,所以默认值将为零,因此您应该确保 0 是合理的默认值。(例如,结构在创建时将所有字段设置为零,因此无法指定除 0 以外的默认值。如果您没有 0 值,您甚至无法在不强制转换为 int 的情况下测试枚举,这将是风格不好。)
如果您的枚举对您的代码是私有的(从不公开),那么您可以在此处停止阅读。
如果您的枚举以任何方式发布到外部代码和/或保存在程序之外,请考虑明确编号。编译器会自动从 0 开始对它们进行编号,但是如果您重新排列枚举而不给它们值,则最终可能会出现缺陷。
我可以合法地写
WriteMode illegalButWorks = (WriteMode)1000000;
file.Write( data, illegalButWorks );
为了解决这个问题,任何使用您无法确定的枚举的代码(例如公共 API)都需要检查该枚举是否有效。您通过以下方式执行此操作
if (!Enum.IsDefined(typeof(WriteMode), userValue))
throw new ArgumentException("userValue");
唯一需要注意的Enum.IsDefined
是它使用反射并且速度较慢。它还存在版本控制问题。如果您需要经常检查枚举值,您最好采用以下方法:
public static bool CheckWriteModeEnumValue(WriteMode writeMode)
{
switch( writeMode )
{
case WriteMode.Append:
case WriteMode.OverWrite:
break;
default:
Debug.Assert(false, "The WriteMode '" + writeMode + "' is not valid.");
return false;
}
return true;
}
版本控制问题是旧代码可能只知道如何处理您拥有的 2 个枚举。如果添加第三个值,Enum.IsDefined 将为 true,但旧代码不一定能处理它。哎呀。
使用枚举可以做更多有趣的事情[Flags]
,并且验证代码略有不同。
我还要注意,为了可移植性,您应该ToString()
在枚举上使用 call,并Enum.Parse()
在读回它们时使用。两者ToString()
和Enum.Parse()
也可以处理[Flags]
枚举,所以没有理由不使用它们。请注意,这是另一个陷阱,因为现在您甚至无法在不破坏代码的情况下更改枚举的名称。
所以,有时当你问自己我能不能只用一个布尔值时,你需要权衡上述所有因素?
恕我直言,对于任何可能有两个以上选项的情况,枚举似乎都是显而易见的选择。但是在某些情况下,您只需要一个布尔值。在那种情况下,我会说使用一个 bool 可以工作的枚举将是一个使用 7 个单词的例子,而 4 个单词可以。
当您有一个明显的切换只能是两件事之一(即灯泡的状态,开或关)时,布尔值是有意义的。除此之外,最好以这样一种方式编写它,以便您传递的内容很明显 - 例如磁盘写入 - 无缓冲、行缓冲或同步 - 应该这样传递。即使您现在不想允许同步写入(因此您仅限于两个选项),也值得考虑让它们更详细,以便乍一看知道它们在做什么。
也就是说,您也可以使用 False 和 True(布尔值 0 和 1),然后如果您以后需要更多值,请将函数扩展为支持用户定义的值(例如,2 和 3)以及旧的 0/1 值将很好地移植,因此您的代码不应该中断。
有时,使用重载对不同的行为进行建模会更简单。继续您的示例将是:
file.appendData( data );
file.overwriteData( data );
如果您有多个参数,则此方法会降级,每个参数都允许一组固定的选项。例如,打开文件的方法可能有文件模式(打开/创建)、文件访问(读/写)、共享模式(无/读/写)的几种排列。配置的总数等于各个选项的笛卡尔积。当然,在这种情况下,多重重载是不合适的。
在某些情况下,枚举可以使代码更具可读性,尽管在某些语言(例如 C#)中验证确切的枚举值可能很困难。
通常,布尔参数作为新的重载附加到参数列表中。.NET 中的一个示例是:
Enum.Parse(str);
Enum.Parse(str, true); // ignore case
后一个重载在比第一个更高版本的 .NET 框架中可用。
如果您知道只有两种选择,那么布尔值可能就可以了。枚举可以以不会破坏旧代码的方式进行扩展,尽管旧库可能不支持新的枚举值,因此不能完全忽略版本控制。
编辑
在较新版本的 C# 中,可以使用命名参数,IMO 可以像枚举一样使调用代码更清晰。使用与上面相同的示例:
Enum.Parse(str, ignoreCase: true);
我确实同意枚举是一个很好的方法,在你有 2 个选项的方法中(只有两个选项你可以在没有枚举的情况下获得可读性。)
例如
public void writeData(Stream data, boolean is_overwrite)
喜欢枚举,但布尔值也很有用。
这是一篇旧帖子的后期条目,它位于页面下方以至于没有人会阅读它,但是因为没有人已经说过......
内联注释对解决意外bool
问题大有帮助。最初的例子特别令人发指:想象一下试图在函数 declearation 中命名变量!会是这样的
void writeData( DataObject data, bool use_append_mode );
但是,为了举例,假设那是声明。然后,对于一个无法解释的布尔参数,我将变量名放在一个内联注释中。比较
file.writeData( data, true );
和
file.writeData( data, true /* use_append_mode */);
这实际上取决于论点的确切性质。如果它不是是/否或真/假,则枚举使其更具可读性。但是对于枚举,您需要检查参数或具有可接受的默认行为,因为可以传递基础类型的未定义值。
在您的示例中使用枚举而不是布尔值确实有助于使方法调用更具可读性。但是,这可以替代我最喜欢的 C# 中的愿望项,即方法调用中的命名参数。这个语法:
var v = CallMethod(pData = data, pFileMode = WriteMode, pIsDirty = true);
将是完全可读的,然后您可以做程序员应该做的事情,即为方法中的每个参数选择最合适的类型,而不考虑它在 IDE 中的外观。
C# 3.0 允许在构造函数中使用命名参数。我不知道为什么他们也不能用方法来做到这一点。
布尔值true
/false
仅。所以不清楚它代表什么。Enum
可以有有意义的名称,例如OVERWRITE
,APPEND
等。所以枚举更好。