概览
位运算是指对二进制数的位(bit)进行操作的运算符。一般在介绍语法的书里比较基础的章节就会涉及,但是实际开发中用的又比较少。运算过程先撇开不谈,许多人的困惑主要是:为什么要用位运算以及什么时候需要用位运算。笔者提供一个实际的例子,用来展示位运算的价值。
目录
举例
考虑这样的需求:写一段代码用于统计新冠病毒感染的症状。已知病人是一个Patient类。
首先想到的是创造一个枚举类型Symptom,每一个症状都是其中的成员,如下:
private enum Symptom : uint
{
/// <summary>
/// 没有任何症状
/// </summary>
None = 0,
/// <summary>
/// 发烧
/// </summary>
Fever = 1,
/// <summary>
/// 咳嗽
/// </summary>
Cough = 2,
/// <summary>
/// 喉咙痛
/// </summary>
SoreThroat = 3,
/// <summary>
/// 头痛
/// </summary>
Headache = 4,
/// <summary>
/// 呕吐
/// </summary>
Emesis = 5,
/// <summary>
/// 腹泻
/// </summary>
Diarrhea = 6,
/// <summary>
/// 缺氧
/// </summary>
LowOxy = 7,
/// <summary>
/// 咳血
/// </summary>
CoughBlood = 8
}
再构造一个symptom变量,把症状存到变量里。
Patient xiaoMing = new Patient(Symptom.CoughBlood)
这种做法在处理单一症状的时候是正确的,那么如果一个患者可以同时有多个症状呢?
方案1——使用单变量实现需求
最Hack的方式是继续使用单变量,通过扩充我们的枚举类型,让枚举成员可以包含多个症状。
private enum Symptom : uint
{
/// <summary>
/// 没有任何症状
/// </summary>
None = 0,
///.......当中省略其他的枚举类型.......///
/// 咳血
/// </summary>
CoughBlood = 8,
//咳嗽又发烧
Fever_Cough = 9,
Cough_SoreThroat = 10,
//咳嗽发烧喉咙痛
Fever_Cough_SoreThroat = 11,
///.....省略后续的其他枚举类型........///
///.....非常多,总量会是2^n.........///
}
代码的可读性差,实用性也非常差。
方案2——使用多个bool变量来实现需求
为了不定义2^n个枚举,我们也可以考虑用多个bool值来代表各种各样的症状。
//Patient.cs
/// <summary>
/// 头疼
/// </summary>
public bool IsHeadache { get; set; }
/// <summary>
/// 发热
/// </summary>
public bool Fever { get; set; }
///----------省略一系列症状-----------
///
///<summary>
///喉咙痛
/// </summary>
public bool SoreThroat { get; set; }
这么做最大的问题是,”症状“不再是一个变量,它变成了若干个毫无关系的变量。
试想一个Patient还有其他的很多属性,如年龄,跑步速度,这些变量和这些”症状变量“在未来会全部混在一起。如果编程人员想要知道这个Patient当前有哪些症状,他甚至提供不了一个-GetSymptom()方法,他只有一系列的IsXXX方法(如IsHeadache()),想想一个类里有几十个IsXXX方法,他们之间却没有任何的分类与联系是多么可怕。
可以预见到的是随着复杂性上升,代码的可读性和可维护性越来越差。
方案3——使用数组来实现需求
—有没有好一点的方式呢?
—有的,我们可以考虑用数组。
/// <summary>
/// 病人类
/// </summary>
public class Patient
{
/// <summary>
/// 症状
/// </summary>
public Symptom[] Symptoms { get; set; }
/// <summary>
/// 是否是重症(咳血或者缺氧算重症)
/// </summary>
/// <returns></returns>
public bool IsSevereCase()
{
//判断是否是重症
bool isSevereCase = false;
foreach (Patient.Symptom symptom in Symptoms)
{
if (symptom == Patient.Symptom.CoughBlood || symptom == Patient.Symptom.LowOxy)
{
isSevereCase = true;
}
else
{
isSevereCase = false;
}
}
return isSevereCase;
}
/// <summary>
/// 判断病人是否有某个症状
/// </summary>
/// <param name="symptom"></param>
/// <returns></returns>
public bool HasSymptom(Symptom symptom)
{
foreach (Patient.Symptom _symptom in Symptoms)
{
if (_symptom == symptom)
{
return true;
}
}
return false;
}
/// <summary>
/// 判断病人是否有某几种症状
/// </summary>
/// <param name="symptom"></param>
/// <returns></returns>
public bool HasSymptoms(params Symptom[] symptoms)
{
foreach (Patient.Symptom cmpSympton in symptoms)
{
foreach (Patient.Symptom _symptom in Symptoms)
{
bool match = false;
if (cmpSympton == _symptom)
{
match = true;
break;
}
if (!match)
{
return false;
}
}
}
return true;
}
}
这个方案存在的问题有:
-
代码美观上尚可,但需要封装比较多的方法,如-HasSymptom(Symptom symptom)和HasSymptoms(params Symptom[] symptoms)。否则连最基础的判断是否有头疼症状,是否有缺氧情况都需要单独写个for循环方法。
-
改变了原本“症状的含义”,换个例子比较直观:Weather可以同时有刮风,有下雨,有日落的表现,但Weather只应该是一个变量,而不该成为Weathers数组。
-
执行效率差,原因是运行时内存分配较多。如判断一个患者是否有头疼+咳嗽的症状,我们会这么写:
bool isHeadacheOrCoach = xiaoHong.HasSymptoms(new Patient.Symptom[] { Patient.Symptom.Cough, Patient.Symptom.Headache });
这就意味着每次我们在进行判断的时候都重新生成了一个数组,并且执行了至少一次的for循环。
我们并不希望一个基础的HasSymptom()方法有这些开销。
方案4——使用位运算来实现需求
在提供解决方案前先介绍一下位运算的基础概念。
可以参考:c#位运算基本概念与计算过程
这里不做展开,重点回答一开始提到的疑问:
什么时候需要用位运算?
使用标志字(Flag word)可以很方便的表示一组开/关,所以非常适合用来管理一组开关。 例如:
- 一系列的用户设置开关,例如消息通知,自动更新,开启定位等
- 角色的一系列属性开关,如角色可飞行,角色科尔移动,角色可攻击等
- 各种天气表现的开关,如是否刮风,是否下雨等
- Unity的各个层开关
如本文的例子,每个症状可以视作一个开关,非常适合用位运算进行处理。由此我们便有了更好的解决方案:
代码实现
代码如下:
/// <summary>
/// 症状枚举
/// </summary>
[Flags]
public enum Symptom : uint
{
/// <summary>
/// 没有任何症状
/// </summary>
None = 1 << 0,
/// <summary>
/// 发烧
/// </summary>
Fever = 1 << 1,
/// <summary>
/// 咳嗽
/// </summary>
Cough = 1 << 2,
/// <summary>
/// 喉咙痛
/// </summary>
SoreThroat = 1 << 3,
/// <summary>
/// 头痛
/// </summary>
Headache = 1 << 4,
/// <summary>
/// 呕吐
/// </summary>
Emesis = 1 << 5,
/// <summary>
/// 腹泻
/// </summary>
Diarrhea = 1 << 6,
/// <summary>
/// 缺氧
/// </summary>
LowOxy = 1 << 7,
/// <summary>
/// 咳血
/// </summary>
CoughBlood = 1 << 8
}
/// <summary>
/// 病人类
/// </summary>
public class Patient
{
/// <summary>
/// 症状
/// </summary>
public Symptom Symptom { get; set; }
/// <summary>
/// 是否是重症(咳血或者缺氧算重症)
/// </summary>
/// <returns></returns>
public bool IsSevereCase()
{
return Symptom.HasFlag(Symptom.CoughBlood) || Symptom.HasFlag(Symptom.LowOxy);
}
/// <summary>
/// 判断病人是否有包含某些症状
/// </summary>
/// <param name="symptom"></param>
/// <returns></returns>
public bool HasSymptom(Symptom symptom)
{
return (Symptom & symptom) != 0x00;
}
/// <summary>
/// 判断病人是否就是某些症状
/// </summary>
/// <param name="symptom"></param>
/// <returns></returns>
public bool IsSymptom(Symptom symptom)
{
return (Symptom == symptom);
}
}
对比前面的数组方案,代码简洁又高效。
验证:
//小明咳嗽又发烧
Patient xiaoMing = new Patient("小明", Symptom.Cough | Symptom.Fever);
//小红咳血,发烧,喉咙痛,头疼
Patient xiaoHong = new Patient("小红", Symptom.CoughBlood | Symptom.Fever | Symptom.SoreThroat | Symptom.Headache);
Console.WriteLine("题设:小明咳嗽又发烧,小红咳血,发烧,喉咙痛,头疼");
Console.WriteLine("---------结果-----------");
Console.WriteLine("{0}{1}|{2}{3}", xiaoMing, xiaoMing.IsSevereCase() ? "重症" : "轻症", xiaoHong, xiaoHong.IsSevereCase() ? "重症" : "轻症");
Console.WriteLine("{0}{1}", xiaoMing, xiaoMing.HasSymptom(Symptom.CoughBlood) ? "有咳血" : "无咳血");
Console.WriteLine("{0}{1}", xiaoHong, xiaoHong.HasSymptom(Symptom.Fever) ? "有发烧" : "无发烧");
Console.WriteLine("{0}{1}", xiaoMing, xiaoMing.IsSymptom(Symptom.Fever | Symptom.Cough) ? "是咳嗽又发烧" : "不是咳嗽又发烧");
结果是:
题设:小明咳嗽又发烧,小红咳血,发烧,喉咙痛,头疼
---------结果-----------
小明轻症|小红重症
小明无咳血
小红有发烧
小明是咳嗽又发烧
附录
十六进制
十六进制 (hexadecimal) 是一种逢 16 进 1 的进制系统。它使用 0 到 9 和 A 到 F (或 a 到 f) 这 16 个数字表示数值。位模式和十六进制表示法非常契合,一般采用十六进制而不是十进制来处理位模式。
因此上文的枚举可以用十六进制表示成
/// <summary>
/// 症状枚举
/// </summary>
[Flags]
public enum Symptom : uint
{
/// <summary>
/// 没有任何症状
/// </summary>
None = 0x01,
/// <summary>
/// 发烧
/// </summary>
Fever = 0x02,
/// <summary>
/// 咳嗽
/// </summary>
Cough = 0x04,
/// <summary>
/// 喉咙痛
/// </summary>
SoreThroat = 0x08,
/// <summary>
/// 头痛
/// </summary>
Headache = 0x10,
/// <summary>
/// 呕吐
/// </summary>
Emesis = 0x20,
/// <summary>
/// 腹泻
/// </summary>
Diarrhea = 0x40,
/// <summary>
/// 缺氧
/// </summary>
LowOxy = 0x80,
/// <summary>
/// 咳血
/// </summary>
CoughBlood = 0x100
}