嘁,都2020了,你咋还在单纯的使用if-else

在高级语言中,基本上都提供了像if-elseswitch-case 这样的条件语句,方便大伙进行判断——引导程序走向。我们在写程序时,常常需要指明两条或者更多的执行路径,使得程序执行时,能够选择其中一条路径,去执行相应的语句,产生对应的结果 —— 这也是条件语句在程序中的作用。



if-else的例子

各位在初学C语言时,应该都写过这样一个程序:输出每个月的天数

//C语言代码片段
int Days(int months, int years){
	int days;
	if(months==1 || months==3 || months==5 || months==7 || months==8 || months==10 || months==12){
		days=31;
	}else if(months==2){
		if((years%4==0 && years%100!=0) || years%400==0){
			days=29;
		}else{
			days=28;
		}
	}else if(months==4 || months==6 || months==9 || months==11){
		days=30;
	}else{
		printf("输入错误!请重新输入:\n");
		Days(months,years);
	}
	return days;
}

这个程序虽是“耳熟能详”的,但后来看着未免感觉有些【繁琐】,多层if-else的嵌套不仅使得可读性降低,还会大大影响程序运行的效率。。。



if-else的问题

从上面就可以看出,if-else判断语句使用起来非常简单,但是在稍微复杂的逻辑场景下,对if-else的频繁使用(或说:滥用)就会容易导致整个项目的可读性和可维护性大大降低。

我们可以试想一下,如果项目中出现了一种新的情况,那么我们要在原有的代码基础上继续增加if-else。但是需求是不会减少的。这样恶性循环下去,原本的几个if-else可能在更新了几个版本后变成了几十个,这可真是令人哭笑不得的事。
(当然,现在也许你的公司会有硬性要求,或者开发模板,那就恭喜你了…)

从设计模式的角度考虑,if-else简直具有了“坏”代码具有的一切:

  • 数据和实现逻辑强耦合
  • 扩展麻烦,维护性低

改善if-else

if-else并非是需要全部被代替的,确切的说,我们现在只能去不断的改善它,使他运行的更为【流畅】。

短路符号和三元表达式
前几天笔者还在群里说这两个:短路符号,又叫“逻辑运算符”,在一些简单的场景下,我们完全可以用它来代替if-else(尤其是那些需要“几个条件同时满足”的场景下):
比如这个——判断一个数是不是2的幂

//c++代码片段
class Solution {
public:
  bool isPowerOfTwo(int n) {
  	//如果一个数是 2 的次方数的话,那么它的二进数必然是最高位为1,其它都为 0 ,
  	//那么如果此时我们减 1 的话,则最高位会降一位,其余为 0 的位现在都为变为 1,
  	//那么我们把两数相与,就会得到 0
    return (n > 0) && (!(n & (n - 1)));
  }
};

我们也可以用三元符号来代替if-else,它是几乎最合适的计算机判断符号(笔者自认为!),尤其适用于多条件复合判断(一层嵌套一层)。不过需要注意的是,大量的三元运算符却容易影响代码的可读性:

比如——判断 n! 结果尾数中零的数量

//java代码片段
public class Solution {
  public int trailingZeroes(int n) {
  	//不断递归
    return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
  }
}

当然,我们还有一种改进方法:如果每种条件下代码逻辑比较多,也可以考虑提前跳出来结束函数——这是借鉴了for循环。

说说switch-case

switch-case是语言自身提供的另一种条件语句,它和if在本质上并没有什么区别,只是代码看上去会更简洁。比如——判断年龄:

goodswitch(age){
	case 10:
		break;
	case 20:
		break;
	case 30:
		break;
	//...
}

但是switch-case无法从根本上解决多个相似条件下需要多次重复的问题。



表驱动法

这个是笔者最为推崇的一种写法,它几乎在大数据量判断、范围区别处理等问题上都有解决方案!

现在让我们再来看文章开头那道题:输出每个月有多少天
我们不妨转换一下思路,每个月份对应一个数字,而月份都是按顺序排列的,所以我们是否可以用一个数组来存储天数,然后用下标来访问?

//javascript 语法片段
const month=new Date().getMonth(),
	year=new Date().getFullYear(),
	isLeapYear=year%4==0 && year%100!=0 || year%400==0;

const monthDays=[31,isLeapYear ? 29 : 28,31,30,31,30,31,31,30,31,30,31];
const days=monthDays[month];

哦,这个代码运行起来可简单多了——至少看起来是这样。

还有上面判断年龄的代码,我们也可以这样写:

//JavaScript 语法片段
ages=[10,20,...];
funs=['a1','a2',...];
for(let i in ages){
	if(age==ages[i]){
		funs[i]();
	}
}
function a1(){
}
function a2(){
}
//...

看了两个例子,想必你对【表驱动法】有了了解:

表驱动法就是一种编程模式,从表里面查找信息而不使用逻辑语句。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。——《代码大全》

使用表驱动可不像if-else那样“轻松”,我们需要先思考两个问题:

如何从表中查询数据?如果if-else判断的是范围,该怎么查?查什么?(数据?索引?)

基于这两个问题,有人将依据表驱动的查询分为三种:

直接访问索引访问阶梯访问

1、直接访问表
笔者最近按照母亲的“旨意”跑了一趟保险公司,发现这个保险费率非常麻烦——它会根据年龄、性别、婚姻状态等不同情况变化。看着上面输出日期的程序想一下,如果你用逻辑控制解构(if or switch)来表示不同费率,那会有多麻烦!(事实上,你的代码可能会像八爪鱼一样…)

我们能够知道,这里的【年龄】是个范围!没法用数组或者对象来做映射。这有两种解决方案:直接访问表 or 阶梯访问表。笔者决定先试试“直接访问表”的方式,并找到了两种方法:

复制信息从而能够直接使用键值:我们可以给 1-17 年龄范围的每个年龄都复制一份信息,然后直接用 age 来访问,同理对其他年龄段的也都一样。这种方法在于操作很简单,表的结构也很简单。但有个缺点就是会浪费空间,毕竟生成了很多冗余信息。(不建议使用)转换键值,如果我们把年龄范围转换成键呢?这样就可以直接来访问了,唯一需要考虑的问题就是有些情境下年龄如何转换为键值。

对于第二种方法,有人可能疑惑了:还要用if-else转换? 当然。前面已经说过:简单的if-else不会有什么问题的,表驱动只是为了优化复杂的逻辑判断,使其更灵活、易扩展。

//TypeScript 语法片段
const Age={
	0:"unadult",
	1:"adult"
}
const Gender={
	0:"female",
	1:"male"
}
const Marry={
	0:"unmarried",
	1:"married"
}

const rateMap={
	[Age[0]+Gender[0]+Marry[0]]:0.1,
	[Age[0]+Gender[0]+Marry[1]]:0.2,
	[Age[0]+Gender[1]+Marry[1]]:0.3,
	[Age[0]+Gender[1]+Marry[0]]:0.4,
	[Age[1]+Gender[0]+Marry[0]]:0.5,
	[Age[1]+Gender[0]+Marry[1]]:0.6,
	[Age[1]+Gender[1]+Marry[1]]:0.7,
	[Age[1]+Gender[1]+Marry[0]]:0.8
}
const isAdult=(age:number)=>age>=18 ? 1: 0
const getDate=(age,hasMarried,gender)=>{
	age=isAdult(age)
	return rateMap[Age[age]+Gender[gender]+Marry[marry]]
}

这样才是正确的打开方式嘛!

哦对,刚刚好像还说了一种方法:
2、阶梯访问表
同样是为了解决上面那个年龄范围的问题,阶梯访问没有索引访问直接,但是会更节省空间。
为了使用阶梯方法,你需要把每个区间的上限写入一张表中,然后通过循环来检查年龄所在的区间,所以在使用阶梯访问的时候一定要注意检查区间的端点。

//TypeScript 语法片段
const ageRanges:number[]=[17,65,100],
	keys:string[]=['<18','18-65','>65'];
const getKey=(age:number):string=>{
	for(let i in keys){
		//console.log(i);
		//console.log(ageRanges[i]);
		if(age<=ageRanges[i]){
			return keys[i];
		}
	}
	return keys[keys.length-1];
}

3、索引访问表

实际中的保险费率问题,在处理年龄范围的时候很头疼,这种范围往往不像上面第一种方法中那么容易得到 ‘key'。
我们当时提到了复制信息从而能够直接使用键值,但是这种方法浪费了很多空间,因为每个年龄都会保存着一份数据。
但是如果我们只是保存索引,通过这个索引来查询数据呢?
假设人刚出生是0岁,最多能活到 100 岁,那么我们需要创建一个长度为 101 的数组,数组的下标对应着人的年龄,这样在 0-17 的每个年龄我们都储存 ‘<18',在18-65储存 ‘18-65', 在65以上储存 ‘>65'。这样我们通过年龄就可以拿到对应的索引,再通过索引来查询对应的数据。
看起来这种方法要比上面的直接访问表更复杂,但是在一些很难通过转换键值、数据占用空间很大的场景下可以试试通过索引来访问:

//Typescript 代码片段
const ages:string[]=['<18','<18','<18',...'18-65','18-65','18-65',...'>65','>65','>65',...'>65'];
const ageKey:string=ages[age];

这样虽然在造表的时候稍有些麻烦,但是在处理数据时却是异常简便!



表驱动的典型应用

表驱动最大的意义就是将条件判断(数据)和逻辑剥离分开,将条件用可配置的表(对象 or 数组)来管理

将0-360°划分为8个不同的空间,但不要总是用if-else实现:

//JavaScript 代码片段
const keys=['A','B','C','D','E','F','G','H'],
	range=[45,90,135,180,225,270,315,360];
const degreeTkey=(rage)=>{
	for(let i in range){
		if(rage<=range[i]){
			return keys[i];
		}
	}
}
const map={
	'A':()=>{
		//...
	},
	'B':()=>{
		//...
	},
	//...
}

//调用如:
map[degreeTkey(46)]();

枚举解决if-else对应关系复杂的问题

啥角色干啥事,这是一个很明显的对应关系,所以学过的“枚举”为啥不用?
其实枚举和上面提到的【表搜索】很像:我们举一个“系统管理员操作权限”的问题
首先定义一个公用接口 RoleOperation,表示不同角色所能做的操作:

public interface RoleOperation {
  String op();//表示某个角色可以做哪些op操作
}

接下来我们将不同角色的情况全部交由枚举类来做,定义一个不同角色有不同权限的枚举类 RoleEnum

public enum RoleEnum implements Role0peration {
  //系统管理员(有A操作权限)
	ROLE_ ROOT_ _ADMIN {
		@Override
		public String op() {
			return "ROLE_ ROOT_ ADMIN:" + " has AAA permission";
		}
	},
	//订单管理员(有B操作权限)
	ROLE_ ORDER_ ADMIN {
		@override
		public String op() {
			return "ROLE_ ORDER_ _ADMIN:" + " has BBB permission";
		}
	},
	//普通用户(有C操作权限)
	ROLE_ NORMAL {
	@Override
		public String op() {
			return "ROLE_ NORMAL:" + "has CCC permission";
		}
	};
}

而且这样一来,以后假如我想扩充条件,只需要去枚举类中加代码即可,而不是去改以前的代码,这岂不很稳!

public class JudgeRole {
	public String judge( String roleName ) {
		//一行代码搞定!之前的if/else没了!
		return RoleEnum.va1ue0f(roleName).op();
	}
}

工厂模式解决if-else“分支过多”问题

不同分支做不同的事情,很明显就提供了使用工厂模式的契机,我们只需要将不同情况单独定义好,然后去工厂类里面聚合即可。

首先,针对不同的角色,可以单独定义其业务类:

//系统管理员(有A操作权限)
public class RootAdminRole implements Role0peration {
	private String roleName ;
	public RootAdminRole( String roleName){
		this.roleName = roleName ;
	}
	@Override
	public String op() {
		return roleName + "has AAA permission" ;
	}
}
//订单管理员(有B操作权限)
public class OrderAdminRole implements RoleOperation {
	private String roleName ;
	public OrderAdminRole( String roleName ) {
		this.roleName = roleName ;
	}
	@Override
	public String op() {
		return roleName + "has BBB permission";
	}
}
//普通用户(有C操作权限)
public class NormalRole implements RoleOperation {
	private String roleName ;
	public NormalRole( String roleName){
		this.roleName = roleName;
	}
	@Override
	public String op() {
		return roleName + "has CCC permission";
	}
}

接下来再写一个工厂类 RoleFactory对上面不同角色进行聚合:

public class RoleFactory {
	static Map<String, Role0peration> roleOperationMap = new HashMap<>();
	//在静态块中先把初始化工作全部做完
	static {
		role0perationMap.put( "ROLE_ ROOT_ ADMIN", new RootAdminRole("ROLE_ _ROOT_ ADMIN") ) :
		roleOperationMap.put( "ROLE_ ORDER_ ADMIN", new OrderAdminRole("ROLE_ ORDER_ ADMIN") );
		role0perationMap.put( "ROLE_ NORMAL", new NormalRole("ROLE_ NORMAL") );
	}
	pub1ic static RoleOperation getOp( String roleName ) {
		return roleOperationMap.get( roleName ) ;
	}
}

接下来借助上面这个工厂,业务代码调用也只需一行代码, if/else同样被消除了:

public class JudgeRole {
	public String judge(String roleName){
		//一行代码搞定!  之前的if/else也没了!
		return RoleFactory.get0p(roleName).op();
	}
}

这样的话以后想扩展条件也很容易,只需要增加新代码,而不需要动以前的业务代码,非常符合“开闭原则”。

到此这篇关于嘁,都2020了,你咋还在单纯的使用if-else的文章就介绍到这了,更多相关if-else使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • python中if及if-else如何使用

    if 结构 if 结构允许程序做出选择,并根据不同的情况执行不同的操作 基本用法 比较运算符 根据 PEP 8 标准,比较运算符两侧应该各有一个空格,比如:5 == 3. PEP8 标准 ==(相等):如果该运算符两侧的值完全相同则返回 True !=(不等):与相等相反 print(5 == '5') print(True == '1') print(True == 1) print('Eric'.lower() == 'eric'.lower()) >(大于):左侧大于右侧则输出 True

  • 详解C++编程中的条件判断语句if-else与switch的用法

    if-else 语句 控制条件分支. 语法 if ( expression ) statement1 [else statement2] 备注 如果 expression 的值不为零,执行 statement1 .如果选项 else 存在,如果 expression 的值为零,执行 statement2. 表达式必须是算术或指针类型,或者必须是定义明确的整型或指针类型转换的类类型.有关转换器的信息,请参见标准转换. 在两个形式的 if 语句和 expression 语句中计算,可以具有除结构以外

  • 嘁,都2020了,你咋还在单纯的使用if-else

    在高级语言中,基本上都提供了像if-else 和switch-case 这样的条件语句,方便大伙进行判断--引导程序走向.我们在写程序时,常常需要指明两条或者更多的执行路径,使得程序执行时,能够选择其中一条路径,去执行相应的语句,产生对应的结果 -- 这也是条件语句在程序中的作用. if-else的例子 各位在初学C语言时,应该都写过这样一个程序:输出每个月的天数: //C语言代码片段 int Days(int months, int years){ int days; if(months==1

  • 都2019年了,还问http中GET和POST的区别

    1.前言 最近看了一些同学的面经,发现无论什么技术岗位,还是会问到 get 和 post 的区别,而搜索出来的答案并不能让我们装得一手好逼,那就让我们从 HTTP 报文的角度来撸一波,从而搞明白他们的区别. 2.标准答案 在开撸之前吗,让我们先看一下标准答案长什么样子 w3school: GET 对比 POST.标准答案很美好,但是在面试的时候把下面的表格甩面试官一脸,估计会装逼不成反被*. 分类 GET POST 后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交).

  • JetBrains IntelliJ IDEA 2020安装与使用教程详解

    对于JetBrains IntelliJ IDEA 2020的认识 IntelliJ IDEA 2020是一款JAVA编程软件,捷克IntelliJ公司研发推出.该软件提供了一个非常强大的JAVA集成开发环境,不仅添加了对Records的完整代码洞察支持,JDK自动下载构建,还能够让开发者更快速的生成构造函数和组件,对代码进行分析检查等功能,大大提高了开发者的代码编写效率,减轻了对代码查漏补缺的压力.该软件提供一站式服务,包括软件开发的环境.开发的过程.开发的维护和管理等等,能完美契合开发者的需

  • IntelliJ IDEA 2020安装使用教程详解

    对于JetBrains IntelliJ IDEA 2020的认识 IntelliJ IDEA 2020是一款JAVA编程软件,捷克IntelliJ公司研发推出.该软件提供了一个非常强大的JAVA集成开发环境,不仅添加了对Records的完整代码洞察支持,JDK自动下载构建,还能够让开发者更快速的生成构造函数和组件,对代码进行分析检查等功能,大大提高了开发者的代码编写效率,减轻了对代码查漏补缺的压力.该软件提供一站式服务,包括软件开发的环境.开发的过程.开发的维护和管理等等,能完美契合开发者的需

  • iOS中让多个cell上都出现倒计时的分析与实现

    前言 以前就有人问过这样一个问题:如果一个tableView的很多或者所有cell上都显示一个倒计时,该怎么实现? 今天自己恰好也遇到了这样的需求:很多产品,每个都有一个时限,在时限内才可以申购,过了申购功能就会关闭.简单描述就是,每个cell上有个倒计时,时间结束与否,点击cell响应的事件是不一样的.那么怎么实现呢?下面谈谈自己的思考过程. 1.Cell内部加一个定时器 既然每个cell都有一个倒计时,时间还可能不一样.根据"高内聚,低耦合"的思想,我首先想着直接让cell自己来实

  • XP/win2003下发现1G的内存比512M还慢的解决方法

    内存越大,机器越快?我想大多数人的回答都是肯定的,从256M到512M的提示是明显的,所以想当然的,从512M到1G的提升也会很明显吧.但是我用我的亲身经历告诉你,1G的内存比512M还慢,当然前提是你什么都不设置. 前几天我把我的Compaq N410c扩展到了1G的内存,2×512M pc133的SD,结果扩展之后发现机器不但还跟原来一样,有点卡,而且休眠时间变长了,这是肯定的啦,休眠就是把内存里的东西拷贝到硬盘上,内存越大,休眠时间肯定越长了,这让我很郁闷,难道我花了500多升级我的机器,

  • java this引用逃逸详解

    1.什么是This逃逸? 在构造器构造还未彻底完成前(即实例初始化阶段还未完成),将自身this引用向外抛出并被其他线程复制(访问)了该引用,可能会问到该还未被初始化的变量,甚至可能会造成更大严重的问题. 废话不多说,看一下代码 /** * 模拟this逃逸 * @author Lijian * */ public class ThisEscape { //final常量会保证在构造器内完成初始化(但是仅限于未发生this逃逸的情况下,具体可以看多线程对final保证可见性的实现) final

  • Java的springcloud Sentinel是什么你知道吗

    目录 Sentinel 是什么? 概述 Sentinel 的历史: 历史 Sentinel 分为两个部分: 两部分 基本概念及作用 基本概念: 主要作用: Sleuth 概述 zipkin分布式监控客户端 基本概念 总结 Sentinel 是什么? 概述 分布式系统的流量防卫兵 随着微服务的流行,服务和服务之间的稳定性变得越来越重要.Sentinel 以流量为切入点,从流量控制.熔断降级系统负载保护等多个维度保护服务的稳定性. Sentinel 的历史: 历史 2012 年,Sentinel 诞

  • 搭建Consul服务发现与服务网格

    目录 第一部分:Consul 基础 1,Consul 介绍 2,安装 Consul Ubuntu/Debian 系统 Centos/RHEL 系统 检查安装 3,运行 Consul Agent 启动 agent 发现数据中心成员 查看 UI 4,在 Consul Service Discovery 中注册服务 端口 定义服务 使用配置启动服务 如何重新加载配置文件 5,查询服务 通过 HTTP API 通过 UI 查询 6,DNS 知识与查询基础知识 基础知识 通过 DNS 查询 Consul

  • 一文掌握python中的时间包

    目录 python中的时间包 datetime时间包 获取当前时间 获取时间间隔 时间对象转时间字符串 时间字符串转时间对象 扩展–时间格式字符 时间戳–timestamp time时间包 获取当前时间戳 获取本地时间 暂停函数sleep time中的strftime和strptime python中的时间包 datetime时间包 datetime包是日期与时间的结合体(date&time) 可以获取当前时间 可以获取时间间隔 可以将时间对象转换成时间字符串 可以将字符串转成时间类型 导入da

随机推荐