布尔值作为方法参数是不可接受的吗?

时间:2020-03-06 14:44:52  来源:igfitidea点击:

我的一位同事指出,布尔值作为方法参数是不可接受的。它们应由枚举代替。起初我没有看到任何好处,但是他给了我一个例子。

什么更容易理解?

file.writeData( data, true );

或者

enum WriteMode {
  Append,
  Overwrite
};

file.writeData( data, Append );

现在我明白了! ;-)
这绝对是一个示例,其中作为第二参数的枚举使代码更具可读性。

那么,我们对此主题有何看法?

解决方案

枚举还允许将来进行修改,现在我们需要第三选择(或者更多选择)。

仅当我们不打算扩展框架的功能时,布尔值才是可接受的。最好使用Enum,因为我们可以扩展枚举而不破坏函数调用的先前实现。

枚举的另一个优点是更易于阅读。

布尔值表示"是/否"选择。如果要表示"是/否",则使用布尔值,这应该是不言自明的。

但是,如果在两个选项之间进行选择,而这两个选项都不是肯定或者否定的,那么枚举有时会更具可读性。

我认为我们几乎自己回答了这个问题,我认为最终目的是使代码更具可读性,在这种情况下,枚举是这样做的,IMO始终最好着眼于最终目标,而不是一揽子规则,也许应该多考虑一下作为一个准则,即枚举在代码中通常比通用布尔,整数等更具可读性,但该规则始终会有例外。

枚举更好,但我不会将布尔参数称为"不可接受的"。有时,放一些布尔值然后继续前进(想想私有方法等)会更容易。

如果该方法问一个问题,例如:

KeepWritingData (DataAvailable());

在哪里

bool DataAvailable()
{
    return true; //data is ALWAYS available!
}

void KeepWritingData (bool keepGoing)
{
   if (keepGoing)
   {
       ...
   }
}

布尔方法参数似乎完全是合理的。

这取决于方法。如果该方法所做的事情很显然是对/错的话,那很好,例如在下面[虽然不是我不是说这是此方法的最佳设计,但这只是用法显而易见的一个示例]。

CommentService.SetApprovalStatus(commentId, false);

但是,在大多数情况下,例如我们提到的示例,最好使用枚举。 .NET Framework本身中有许多示例未遵循此约定,这是因为它们是在周期的较晚时间引入了此设计指南的。

恕我直言,对于任何可能有两个以上选择的情况,枚举似乎都是显而易见的选择。但是在某些情况下,我们只需要布尔值即可。在那种情况下,我想说的是使用一个布尔值可以工作的枚举将是一个使用7个单词而4个可以使用的单词的示例。

当我们具有明显的切换时,布尔值才有意义,该切换只能是以下两种情况之一(即,灯泡的状态,打开或者关闭)。除此之外,最好以显而易见的方式编写它,例如无缓冲,行缓冲或者同步的磁盘写操作应按原样传递。即使我们现在不想允许同步写入(因此我们只能使用两个选项),还是值得考虑使它们更加冗长,以便一眼就知道它们的功能。

也就是说,我们还可以使用False和True(布尔值0和1),然后如果以后需要更多值,请将函数扩展为支持用户定义的值(例如2和3)以及旧的0/1值将很好地移植,因此代码不应中断。

它的确使事情变得更加明确,但确实开始以纯粹的布尔选择(例如,添加/覆盖似乎过大)极大地扩展了接口的复杂性。如果我们需要添加其他选项(在这种情况下我无法想到),则可以始终执行重构(取决于语言)

我不同意这是一个好规则。显然,在某些情况下,Enum可以提供更好的显式或者冗长的代码,但通常来说,它似乎无法实现。

首先让我举一个例子:
拥有布尔参数并不会真正危害程序员编写好的代码的责任(和能力)。在示例中,程序员可以通过编写以下代码来编写冗长的代码:

dim append as boolean = true
file.writeData( data, append );

还是我更一般

dim shouldAppend as boolean = true
file.writeData( data, shouldAppend );

第二:
我们给出的Enum示例只是"更好",因为我们正在传递CONST。在大多数应用程序中,最有可能传递给函数的时间参数中的至少一些(如果不是大多数的话)是变量。在这种情况下,我的第二个示例(给变量起好名字)要好得多,而Enum不会给我们带来什么好处。

使用最能为问题建模的模型。在我们给出的示例中,枚举是一个更好的选择。但是,在其他情况下,布尔值会更好。这对我们来说更有意义:

lock.setIsLocked(True);

或者

enum LockState { Locked, Unlocked };
lock.setLockState(Locked);

在这种情况下,我可能会选择布尔值选项,因为我认为它非常清楚且明确,而且我很确定我的锁不会有两个以上的状态。第二种选择仍然是有效的,但是不必要的麻烦,恕我直言。

还记得阿杜·史蒂文森(Adlai Stevenson)在古巴导弹危机期间向联合国佐林大使提出的问题吗?

"You are in the courtroom of world
  opinion right now, and you can answer
  yes or no. You have denied that [the missiles]
  exist, and I want to know whether I
  have understood you correctly.... I am
  prepared to wait for my answer until
  hell freezes over, if that's your
  decision."

如果方法中具有的标志具有可以将其固定为二进制决策的性质,并且该决策永远不会变成三向决策或者n向决策,请使用布尔值。主治:旗帜叫isXXX。

如果是模式切换,请勿将其设为布尔值。首先编写方法时,总会有比我们想到的模式更多的模式。

一个多模的困境有困扰着Unix,如今文件或者目录可能具有的许可模式可能会导致各种奇怪的双重含义,具体取决于文件类型,所有权等。

在具有命名参数的语言(例如Python和Objective-C)中,布尔值可能是可以的,因为名称可以解释参数的作用:

file.writeData(data, overwrite=true)

或者:

[file writeData:data overwrite:YES]

我碰到这是一件坏事,有两个原因:

  • 因为有些人会编写如下方法:
ProcessBatch(true, false, false, true, false, false, true);

这显然是不好的,因为它很容易混淆参数,并且我们不了解所指定的内容。虽然只有一个布尔值还算不错。

  • 因为通过一个简单的yes / no分支控制程序流可能意味着我们有两个完全不同的功能,它们以一种笨拙的方式包装在一起。例如:
public void Write(bool toOptical);

真的,这应该是两种方法

public void WriteOptical();
public void WriteMagnetic();

因为其中的代码可能完全不同;他们可能必须执行各种不同的错误处理和验证,甚至可能必须以不同的方式格式化输出数据。我们不能仅仅通过使用Write()甚至是Write(Enum.Optical)就能分辨出来(尽管我们当然可以让这些方法中的任何一个仅调用内部方法WriteOptical / Mag)。

我想这取决于。除了第一名,我不会做太大的事情。

枚举有一定的好处,但是我们不应该只是用枚举代替所有的布尔值。在很多地方,对/错实际上是表示所发生情况的最佳方法。

但是,将它们用作方法参数有点令人怀疑,这仅仅是因为我们必须深入研究它们应该做的事情才能看到它们,因为它们可以让我们看到对/错的实际含义。

属性(尤其是使用C#3对象初始化程序)或者关键字参数(la ruby​​或者python)是我们使用布尔型参数的更好方法。

C示例:

var worker = new BackgroundWorker { WorkerReportsProgress = true };

Ruby示例

validates_presence_of :name, :allow_nil => true

Python示例

connect_to_database( persistent=true )

我唯一想到的是布尔方法参数在哪里正确,这是在Java中,那里既没有属性也没有关键字参数。这是我讨厌java的原因之一:-(

枚举当然可以使代码更具可读性。仍然有一些注意事项(至少在.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;
}

版本问题是旧代码可能只知道如何处理我们拥有的两个枚举。如果添加第三个值,则Enum.IsDefined将为true,但是旧代码不一定能够处理它。哎呀

我们可以使用[Flags]枚举获得更多的乐趣,并且其验证代码略有不同。

我还将注意到,出于可移植性,我们应在枚举上使用调用ToString(),并在读回它们时使用Enum.Parse()。ToString()和Enum.Parse()都应使用也可以处理[Flags]枚举,因此没有理由不使用它们。提醒我们,这是另一个陷阱,因为现在我们必须在不破坏代码的情况下甚至无法更改枚举的名称。

因此,有时当我们问自己时,需要权衡上述所有内容。

这实际上取决于论点的确切性质。如果不是yes / no或者true / false,则枚举使其更具可读性。但是,使用枚举时,我们需要检查参数或者具有可接受的默认行为,因为可以传递基础类型的未定义值。

虽然在很多情况下枚举确实比布尔更易读和可扩展,但"布尔不可接受"的绝对规则是愚蠢的。它是僵化的,适得其反,没有留下人类判断的余地。它们是大多数语言中基本的内置类型,因为考虑将其应用于其他内置类型很有用:例如说"永远不要将int用作参数"会很疯狂。

该规则仅是样式问题,而不是潜在的错误或者运行时性能问题。更好的规则是"出于可读性考虑,最好枚举布尔值"。

看一下.Net框架。在许多方法中,布尔值都用作参数。 .Net API并不完美,但是我认为使用布尔值作为参数并不是一个大问题。工具提示始终为我们提供参数的名称,我们也可以构建这种指导,也可以在方法参数中填写XML注释,这些注释将出现在工具提示中。

我还应该补充一点,当我们在类或者方法参数中有两个或者多个布尔值,并且并非所有状态都有效时(例如,同时拥有两个状态是无效的),则应该明确地将布尔值重构为枚举设为true)。

例如,如果班级具有以下属性

public bool IsFoo
public bool IsBar

同时使它们都正确是一个错误,实际上我们得到的是三个有效状态,可以更好地表示为:

enum FooBarType { IsFoo, IsBar, IsNeither };

在示例中使用枚举而不是布尔确实有助于使方法调用更具可读性。但是,这替代了我在C#中最喜欢的愿望项目,即方法调用中的命名参数。这个语法:

var v = CallMethod(pData = data, pFileMode = WriteMode, pIsDirty = true);

完全可读,然后我们可以做程序员应该做的事情,即为方法中的每个参数选择最合适的类型,而不必考虑它在IDE中的外观。

C3.0允许在构造函数中使用命名参数。我不知道为什么他们也不能使用方法来做到这一点。

有时,使用重载为不同的行为建模更简单。继续示例将是:

file.appendData( data );  
file.overwriteData( data );

如果我们有多个参数(每个参数都允许一组固定的选项),则此方法会降低性能。例如,打开文件的方法可能具有以下几种排列方式:文件模式(打开/创建),文件访问(读/写),共享模式(无/读/写)。配置总数等于各个选项的笛卡尔乘积。当然,在这种情况下,多个重载是不合适的。

在某些情况下,枚举可以使代码更具可读性,尽管在某些语言(例如C)中验证确切的枚举值可能很困难。

通常,布尔参数会作为新的重载添加到参数列表中。 .NET中的一个示例是:

Enum.Parse(str);  
Enum.Parse(str, true); // ignore case

在.NET框架的更高版本中,后一个重载变得比第一个重载大。

如果我们知道只有两种选择,则布尔值可能会很好。枚举可以以不破坏旧代码的方式进行扩展,尽管旧库可能不支持新的枚举值,因此无法完全忽略版本控制。

编辑

在较新版本的Cit中,可以使用命名参数IMO,它可以以枚举相同的方式使调用代码更清晰。使用与上述相同的示例:

Enum.Parse(str, ignoreCase: true);

对我来说,既不使用布尔值也不使用枚举不是一个好的方法。罗伯特·C·马丁(Robert C. Martin)在他的"干净代码提示12:消除布尔参数"中非常清楚地说明了这一点:

Boolean arguments loudly declare that the function does more than one thing. They are confusing and should be eliminated.

如果一个方法做的不止一件事情,那么我们应该编写两种不同的方法,例如情况:file.append(data)file.overwrite(data)

使用枚举并不能使事情变得更清晰。它没有任何改变,仍然是一个标志参数。

同事可能会更好地遵循的一些规则是:

  • 不要对设计教条。
  • 选择最适合代码用户的内容。
  • 不要仅仅因为我们喜欢本月的形状就将星形钉砸向每个孔!

布尔值仅适用于" true" /" false"。因此尚不清楚它代表什么。 " Enum"可以具有有意义的名称,例如" OVERWRITE"," APPEND"等。因此,枚举更好。