首页 > 其他分享 >使用位运算来优化代码

使用位运算来优化代码

时间:2023-01-10 21:44:07浏览次数:60  
标签:症状 Patient 运算 代码 bool symptom 优化 public Symptom

概览

位运算是指对二进制数的位(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
}

进制转换工具

在线进位转换工具

标签:症状,Patient,运算,代码,bool,symptom,优化,public,Symptom
From: https://www.cnblogs.com/wenqu/p/17041453.html

相关文章