嘁,都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使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2020-10-15

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

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

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

简单了解Python中的几种函数

几个特殊的函数(待补充) python是支持多种范型的语言,可以进行所谓函数式编程,其突出体现在有这么几个函数: filter.map.reduce.lambda.yield lambda >>> g = lambda x,y:x+y #x+y,并返回结果 >>> g(3,4) 7 >>> (lambda x:x**2)(4) #返回4的平方 16 lambda函数的使用方法: 在lambda后面直接跟变量 变量后面是冒号 冒号后面是表达式,表达式计算

基于python中的TCP及UDP(详解)

python中是通过套接字即socket来实现UDP及TCP通信的.有两种套接字面向连接的及无连接的,也就是TCP套接字及UDP套接字. TCP通信模型 创建TCP服务器 伪代码: ss = socket() # 创建服务器套接字 ss.bind() # 套接字与地址绑定 ss.listen() # 监听连接 inf_loop: # 服务器无限循环 cs = ss.accept() # 接受客户端连接 comm_loop: # 通信循环 cs.recv()/cs.send() # 对话(接收/发

python中文件变化监控示例(watchdog)

在python中文件监控主要有两个库,一个是pyinotify ( https://github.com/seb-m/pyinotify/wiki),一个是watchdog(http://pythonhosted.org/watchdog/).pyinotify依赖于Linux平台的inotify,后者则对不同平台的的事件都进行了封装.因为我主要用于Windows平台,所以下面着重介绍watchdog(推荐大家阅读一下watchdog实现源码,有利于深刻的理解其中的原理). watchdog在不

python中模块的__all__属性详解

python模块中的__all__属性,可用于模块导入时限制,如: from module import * 此时被导入模块若定义了__all__属性,则只有__all__内指定的属性.方法.类可被导入. 若没定义,则导入模块内的所有公有属性,方法和类 # kk.py class A(): def __init__(self,name,age): self.name=name self.age=age class B(): def __init__(self,name,id): self.nam

python中requests使用代理proxies方法介绍

学习网络爬虫难免遇到使用代理的情况,下面介绍一下如何使用requests设置代理: 如果需要使用代理,你可以通过为任意请求方法提供 proxies 参数来配置单个请求: import requests proxies = { "http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080", } requests.get("http://examp

python中requests爬去网页内容出现乱码问题解决方法介绍

最近在学习python爬虫,使用requests的时候遇到了不少的问题,比如说在requests中如何使用cookies进行登录验证,这可以查看这篇文章.这篇博客要解决的问题是如何避免在使用requests的时候出现乱码. import requests res=requests.get("https://www.baidu.com") print res.content 以上就是使用requests进行简单的网页请求数据的方式.但是很容易出现乱码的问题. 我们可以通过在网页上右击查看

python中 logging的使用详解

日志是用来记录程序在运行过程中发生的状况,在程序开发过程中添加日志模块能够帮助我们了解程序运行过程中发生了哪些事件,这些事件也有轻重之分. 根据事件的轻重可分为以下几个级别: DEBUG: 详细信息,通常仅在诊断问题时才受到关注.整数level=10 INFO: 确认程序按预期工作.整数level=20 WARNING:出现了异常,但是不影响正常工作.整数level=30 ERROR:由于某些原因,程序 不能执行某些功能.整数level=40 CRITICAL:严重的错误,导致程序不能运行.整数

基于python中staticmethod和classmethod的区别(详解)

例子 class A(object): def foo(self,x): print "executing foo(%s,%s)"%(self,x) @classmethod def class_foo(cls,x): print "executing class_foo(%s,%s)"%(cls,x) @staticmethod def static_foo(x): print "executing static_foo(%s)"%x a=A(

在python中使用正则表达式查找可嵌套字符串组

在网上看到一个小需求,需要用正则表达式来处理.原需求如下: 找出文本中包含"因为--所以"的句子,并以两个词为中心对齐输出前后3个字,中间全输出,如果"因为"和"所以"中间还存在"因为""所以",也要找出来,另算一行,输出格式为: 行号 前面3个字 *因为* 全部 &所以& 后面3个字(标点符号算一个字) 2 还不是 *因为* 这里好, &所以& 没有人 实现方法如下: #e

浅谈python中copy和deepcopy中的区别

在下是个编程爱好者,最近将魔爪伸向了Python编程.....遇到copy和deepcopy感到很困惑,现在针对这两个方法进行区分,一种是浅复制(copy),一种是深度复制(deepcopy). 首先说一下deepcopy,所谓的深度复制,在这里我理解的是完全复制然后变成一个新的对象,复制的对象和被复制的对象没有任何关系,彼此之间无论怎么改变都相互不影响. 然后说一下copy,在这里我分为两类来说,一种是字典数据类型的copy函数,一种是copy包的copy函数. 一.字典数据类型的copy函数