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

(编辑:jimmy 日期: 2024/12/23 浏览:2)

在高级语言中,基本上都提供了像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 "color: rgb(255, 0, 0);">说说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 "htmlcode">
//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 "htmlcode">
//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();
	}
}

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