本篇我们将介绍四种行为型模式,分别是

  • 解释器模式
  • 迭代器模式
  • 中介者模式
  • 备忘录模式

解释器模式

我国 IT 界历来有一个汉语编程梦,虽然各方对于汉语编程争论不休,甚至上升到民族大义的高度,本文不讨论其对与错,但我们不妨来尝试一下,定义一个简单的中文编程语法。

在设计模式中,解释器模式就是用来自定义语法的,它的定义如下。

解释器模式(Interpreter Pattern):给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

解释器模式较为晦涩难懂,但本文我们仍然深入浅出,通过一个简单的例子来学习解释器模式:使用中文编写出十以内的加减法公式。比如:

  • 输入“一加一”,输出结果 2
  • 输入“一加一加一”,输出结果 3
  • 输入“二加五减三”,输出结果 4
  • 输入“七减五加四减一”,输出结果 5
  • 输入“九减五加三减一”,输出结果 6

看到这个需求,我们很容易想到一种写法:将输入的字符串分割成单个字符,把数字字符通过switch-case转换为数字,再通过计算符判断是加法还是减法,对应做加、减计算,最后返回结果即可。

的确可行,但这实在太面向过程了。众所周知,面向过程编程会有耦合度高,不易扩展等缺点。接下来我们尝试按照面向对象的写法来实现这个功能。

按照面向对象的编程思想,我们应该为公式中不同种类的元素建立一个对应的对象。那么我们先分析一下公式中的成员:

  • 数字:零到九对应 0 ~ 9
  • 计算符:加、减对应+、-

公式中仅有这两种元素,其中对于数字的处理比较简单,只需要通过 switch-case 将中文名翻译成阿拉伯数字即可。

计算符怎么处理呢?计算符左右两边可能是单个数字,也可能是另一个计算公式。但无论是数字还是公式,两者都有一个共同点,那就是他们都会返回一个整数:数字返回其本身,公式返回其计算结果。

所以我们可以根据这个共同点提取出一个返回整数的接口,数字和计算符都作为该接口的实现类。在计算时,使用栈结构存储数据,将数字和计算符统一作为此接口的实现类压入栈中计算。

talk is cheap, show me the code.

数字和计算符公共的接口:

interface Expression {int interpret();
}

上文已经说到,数字和计算符都属于表达式的一部分,他们的共同点是都会返回一个整数。从表达式计算出整数的过程,我们称之为解释(interpret)。

对数字类的解释实现起来相对比较简单:

public class Number implements Expression {int number;public Number(char word) {switch (word) {case '零':number = 0;break;case '一':number = 1;break;case '二':number = 2;break;case '三':number = 3;break;case '四':number = 4;break;case '五':number = 5;break;case '六':number = 6;break;case '七':number = 7;break;case '八':number = 8;break;case '九':number = 9;break;default:break;}}@Overridepublic int interpret() {return number;}
}

在 Number 类的构造函数中,先将传入的字符转换为对应的数字。在解释时将转换后的数字返回即可。

无论是加法还是减法,他们都是对左右两个表达式进行操作,所以我们可以将计算符提取出共同的抽象父类:

abstract class Operator implements Expression {Expression left;Expression right;Operator(Expression left, Expression right) {this.left = left;this.right = right;}
}

在此抽象父类中,我们存入了两个变量,表达计算符左右两边的表达式。

加法类实现如下:

class Add extends Operator {Add(Expression left, Expression right) {super(left, right);}@Overridepublic int interpret() {return left.interpret() + right.interpret();}
}

减法类:

class Sub extends Operator {Sub(Expression left, Expression right) {super(left, right);}@Overridepublic int interpret() {return left.interpret() - right.interpret();}
}

加法类和减法类都继承自 Operator 类,在对他们进行解释时,将左右两边表达式解释出的值相加或相减即可。

数字类和计算符内都定义好了,这时我们只需要再编写一个计算类将他们综合起来,统一计算即可。

计算类:

class Calculator {int calculate(String expression) {Stack<Expression> stack = new Stack<>();for (int i = 0; i < expression.length(); i++) {char word = expression.charAt(i);switch (word) {case '加':stack.push(new Add(stack.pop(), new Number(expression.charAt(++i))));break;case '减':stack.push(new Sub(stack.pop(), new Number(expression.charAt(++i))));break;default:stack.push(new Number(word));break;}}return stack.pop().interpret();}
}

在计算类中,我们使用栈结构保存每一步操作。遍历 expression 公式:

  • 遇到数字则将其压入栈中;
  • 遇到计算符时,先将栈顶元素 pop 出来,再和下一个数字一起传入计算符的构造函数中,组成一个计算符公式压入栈中。

需要注意的是,入栈出栈过程并不会执行真正的计算,栈操作只是将表达式组装成一个嵌套的类对象而已。比如:

  • “一加一”表达式,经过入栈出栈操作后,生成的对象是 new Add(new Number('一'), new Number('一'))
  • “二加五减三”表达式,经过入栈出栈操作后,生成的对象是 new Sub(new Add(new Number('二'), new Number('五')), new Number('三'))

最后一步 stack.pop().interpret(),将栈顶的元素弹出,执行 interpret() ,这时才会执行真正的计算。计算时会将中文的数字和运算符分别解释成计算机能理解的指令。

测试类:

public class Client {@Testpublic void test() {Calculator calculator = new Calculator();String expression1 = "一加一";String expression2 = "一加一加一";String expression3 = "二加五减三";String expression4 = "七减五加四减一";String expression5 = "九减五加三减一";// 输出: 一加一 等于 2System.out.println(expression1 + " 等于 " + calculator.calculate(expression1));// 输出: 一加一加一 等于 3System.out.println(expression2 + " 等于 " + calculator.calculate(expression2));// 输出: 二加五减三 等于 4System.out.println(expression3 + " 等于 " + calculator.calculate(expression3));// 输出: 七减五加四减一 等于 5System.out.println(expression4 + " 等于 " + calculator.calculate(expression4));// 输出: 九减五加三减一 等于 6System.out.println(expression5 + " 等于 " + calculator.calculate(expression5));}
}

这就是解释器模式,我们将一句中文的公式解释给计算机,然后计算机为我们运算出了正确的结果。

分析本例中公式的组成,我们可以发现几条显而易见的性质:

  • 数字类不可被拆分,属于计算中的最小单元;
  • 加法类、减法类可以被拆分成两个数字(或两个公式)加一个计算符,他们不是计算的最小单元。

在解释器模式中,我们将不可拆分的最小单元称之为终结表达式,可以被拆分的表达式称之为非终结表达式。

解释器模式具有一定的拓展性,当需要添加其他计算符时,我们可以通过添加 Operator 的子类来完成。但添加后需要按照运算优先级修改计算规则。可见一个完整的解释器模式是非常复杂的,实际开发中几乎没有需要自定义解释器的情况。

解释器模式有一个常见的应用,在我们平时匹配字符串时,用到的正则表达式就是一个解释器。正则表达式中,表示一个字符的表达式属于终结表达式,除终结表达式外的所有表达式都属于非终结表达式。

迭代器模式

设想一个场景:我们有一个类中存在一个列表。这个列表需要提供给外部类访问,但我们不希望外部类修改其中的数据。

public class MyList {private List<String> data = Arrays.asList("a", "b", "c");
}

通常来说,将成员变量提供给外部类访问有两种方式:

  • 将此列表设置为 public 变量;
  • 添加 getData() 方法,返回此列表。

但这两种方式都有一个致命的缺点,它们无法保证外部类不修改其中的数据。外部类拿到 data 对象后,可以随意修改列表内部的元素,这会造成极大的安全隐患。

那么有什么更好的方式吗?使得外部类只能读取此列表中的数据,无法修改其中的任何数据,保证其安全性。

分析可知,我们可以通过提供两个方法实现此效果:

  • 提供一个 String next() 方法,使得外部类可以按照次序,一条一条的读取数据;
  • 提供一个 boolean hasNext() 方法,告知外部类是否还有下一条数据。

代码实现如下:

public class MyList {private List<String> data = Arrays.asList("a", "b", "c");private int index = 0;public String next() {// 返回数据后,将 index 加 1,使得下次访问时返回下一条数据return data.get(index++);}public boolean hasNext() {return index < data.size();}
}

客户端就可以使用一个 while 循环来访问此列表了:

public class Client {@Testpublic void test() {MyList list = new MyList();// 输出:abcwhile (list.hasNext()) {System.out.print(list.next());}}
}

由于没有给外部类暴露 data 成员变量,所以我们可以保证数据是安全的。

但这样的实现还有一个问题:当遍历完成后,hasNext() 方法就会一直返回 false,无法再一次遍历了,所以我们必须在一个合适的地方把 index 重置成 0。

在哪里重置比较合适呢?实际上,使用 next() 方法和 hasNext() 方法来遍历列表是一个完全通用的方法,我们可以为其创建一个接口,取名为 Iterator,Iterator 的意思是迭代器,迭代的意思是重复反馈,这里是指我们依次遍历列表中的元素。

public interface Iterator {boolean hasNext();String next();
}

然后在 MyList 类中,每次遍历时生成一个迭代器,将 index 变量放到迭代器中。由于每个迭代器都是新生成的,所以每次遍历时的 index 自然也就被重置成 0 了。代码如下:

public class MyList {private List<String> data = Arrays.asList("a", "b", "c");// 每次生成一个新的迭代器,用于遍历列表public Iterator iterator() {return new Itr();}private class Itr implements Iterator {private int index = 0;@Overridepublic boolean hasNext() {return index < data.size();}@Overridepublic String next() {return data.get(index++);}}
}

客户端访问此列表的代码修改如下:

public class Client {@Testpublic void test() {MyList list = new MyList();// 获取迭代器,用于遍历列表Iterator iterator = list.iterator();// 输出:abcwhile (iterator.hasNext()) {System.out.print(iterator.next());}}
}

这就是迭代器模式,《设计模式》一书中将其定义如下:

迭代器模式(Iterator Pattern):提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。

迭代器模式的核心就在于定义出 next() 方法和 hasNext() 方法,让外部类使用这两个方法来遍历列表,以达到隐藏列表内部细节的目的。

事实上,Java 已经为我们内置了 Iterator 接口,源码中使用了泛型使得此接口更加的通用:

public interface Iterator<E> {boolean hasNext();E next();
}

并且,本例中使用的迭代器模式是仿照 ArrayList 的源码实现的,ArrayList 源码中使用迭代器模式的部分代码如下:

public class ArrayList<E> {...public Iterator<E> iterator() {return new Itr();}private class Itr implements Iterator<E> {protected int limit = ArrayList.this.size;int cursor;public boolean hasNext() {return cursor < limit;}public E next() {...}}
}

我们平时常用的 for-each 循环,也是迭代器模式的一种应用。在 Java 中,只要实现了 Iterable 接口的类,都被视为可迭代访问的。Iterable 中的核心方法只有一个,也就是刚才我们在 MyList 类中实现过的用于获取迭代器的 iterator() 方法:

public interface Iterable<T> {Iterator<T> iterator();...
}

只要我们将 MyList 类修改为继承此接口,便可以使用 for-each 来迭代访问其中的数据了:

public class MyList implements Iterable<String> {private List<String> data = Arrays.asList("a", "b", "c");@NonNull@Overridepublic Iterator<String> iterator() {// 每次生成一个新的迭代器,用于遍历列表return new Itr();}private class Itr implements Iterator<String> {private int index = 0;@Overridepublic boolean hasNext() {return index < data.size();}@Overridepublic String next() {return data.get(index++);}}
}

客户端使用 for-each 访问:

public class Client {@Testpublic void test() {MyList list = new MyList();// 输出:abcfor (String item : list) {System.out.print(item);}}
}

这就是迭代器模式。基本上每种语言都会在语言层面为所有列表提供迭代器,我们只需要直接拿来用即可,这是一个比较简单又很常用的设计模式。

中介者模式

顾名思义,中介这个名字对我们来说实在太熟悉了。平时走在上班路上就会经常见到各种房产中介,他们的工作就是使得买家与卖家不需要直接打交道,只需要分别与中介打交道,就可以完成交易,用计算机术语来说就是减少了耦合度。

当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的关系变成星形。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系。

举个例子,在我们打麻将时,每两个人之间都可能存在输赢关系。如果每笔交易都由输家直接发给赢家,就会出现一种网状耦合关系。

我们用程序来模拟一下这个过程。

玩家类:

class Player {// 初始资金 100 元public int money = 100;public void win(Player player, int money) {// 输钱的人扣减相应的钱player.money -= money;// 自己的余额增加this.money += money;}
}

此类中有一个 money 变量,表示自己的余额。当自己赢了某位玩家的钱时,调用 win 方法修改输钱的人和自己的余额。

需要注意的是,我们不需要输钱的方法,因为在 win 方法中,已经将输钱的人对应余额扣除了。

客户端代码:

public class Client {@Testpublic void test() {Player player1 = new Player();Player player2 = new Player();Player player3 = new Player();Player player4 = new Player();// player1 赢了 player3 5 元player1.win(player3, 5);// player2 赢了 player1 10 元player2.win(player1, 10);// player2 赢了 player4 10 元player2.win(player4, 10);// player4 赢了 player3 7 元player4.win(player3, 7);// 输出:四人剩余的钱:105,120,88,97System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);}
}

在客户端中,每两位玩家需要进行交易时,都会增加程序耦合度,相当于每位玩家都需要和其他所有玩家打交道,这是一种不好的做法。

此时,我们可以引入一个中介类 —— 微信群,只要输家将自己输的钱发到微信群里,赢家从微信群中领取对应金额即可。网状的耦合结构就变成了星形结构:

此时,微信群就充当了一个中介者的角色,由它来负责与所有人进行交易,每个玩家只需要与微信群打交道即可。

微信群类:

class Group {public int money;
}

此类中只有一个 money 变量表示群内的余额。

玩家类修改如下:

class Player {public int money = 100;public Group group;public Player(Group group) {this.group = group;}public void change(int money) {// 输了钱将钱发到群里 或 在群里领取自己赢的钱group.money += money;// 自己的余额改变this.money += money;}
}

玩家类中新增了一个构造方法,在构造方法中将中介者传进来。每当自己有输赢时,只需要将钱发到群里或者在群里领取自己赢的钱,然后修改自己的余额即可。

客户端代码对应修改如下:

public class Client {@Testpublic void test(){Group group = new Group();Player player1 = new Player(group);Player player2 = new Player(group);Player player3 = new Player(group);Player player4 = new Player(group);// player1 赢了 5 元player1.change(5);// player2 赢了 20 元player2.change(20);// player3 输了 12 元player3.change(-12);// player4 输了 3 元player4.change(-3);// 输出:四人剩余的钱:105,120,88,97System.out.println("四人剩余的钱:" + player1.money + "," + player2.money + "," + player3.money + "," + player4.money);}
}

可以看到,通过引入中介者,客户端的代码变得更加清晰了。大家不需要再互相打交道,所有交易通过中介者完成即可。

事实上,这段代码还存在一点不足。因为我们忽略了一个前提:微信群里的钱不可以为负数。也就是说,输家必须先将钱发到微信群内,赢家才能去微信群里领钱。这个功能可以用我们在 Java 多线程王国奇遇记 中学到的 wait/notify 机制完成,与中介者模式无关,故这里不再给出相关代码,感兴趣的读者可以自行实现。

总而言之,中介者模式就是用于将类与类之间的多对多关系简化成多对一、一对多关系的设计模式,它的定义如下:

中介者模式(Mediator Pattern):定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。

中介者模式的缺点也很明显:由于它将所有的职责都移到了中介者类中,也就是说中介类需要处理所有类之间的协调工作,这可能会使中介者演变成一个超级类。所以使用中介者模式时需要权衡利弊。

备忘录模式

备忘录模式最常见的实现就是游戏中的存档、读档功能,通过存档、读档,使得我们可以随时恢复到之前的状态。

当我们在玩游戏时,打大 Boss 之前,通常会将自己的游戏进度存档保存,以保证自己打不过 Boss 的话,还能重新读档恢复状态。

玩家类:

class Player {// 生命值private int life = 100;// 魔法值private int magic = 100;public void fightBoss() {life -= 100;magic -= 100;if (life <= 0) {System.out.println("壮烈牺牲");}}public int getLife() {return life;}public void setLife(int life) {this.life = life;}public int getMagic() {return magic;}public void setMagic(int magic) {this.magic = magic;}
}

我们为玩家定义了两个属性:生命值和魔法值。其中有一个 fightBoss() 方法,每次打 Boss 都会扣减 100 点体力、100 点魔法值。如果生命值小于等于 0,则提示用户已“壮烈牺牲”。

客户端实现如下:

public class Client {@Testpublic void test() {Player player = new Player();// 存档int savedLife = player.getLife();int savedMagic = player.getMagic();// 打 Boss,打不过,壮烈牺牲player.fightBoss();// 读档,恢复到打 Boss 之前的状态player.setLife(savedLife);player.setMagic(savedMagic);}
}

客户端中,我们在 fightBoss() 之前,先去存档,把自己当前的生命值和魔法值保存起来。打完 Boss 发现自己牺牲之后,再回去读档,将自己恢复到打 Boss 之前的状态。

这就是备忘录模式…吗?不完全是,事情并没有这么简单。

还记得我们在 原型模式 中,买的那杯和周杰伦一模一样的奶茶吗?开始时,为了克隆一杯奶茶,我们将奶茶的各个属性分别赋值成和周杰伦买的那杯奶茶一样。但这样存在一个弊端:我们不可能为一千个粉丝写一千份挨个赋值操作。所以最终我们在奶茶类内部实现了 Cloneable 接口,定义了 clone() 方法,来实现一行代码拷贝所有属性。

备忘录模式也应该采取类似的做法。我们不应该采用将单个属性挨个存取的方式来进行读档、存档。更好的做法是将存档、读档交给需要存档的类内部去实现。

新建备忘录类:

class Memento {int life;int magic;Memento(int life, int magic) {this.life = life;this.magic = magic;}
}

在此类中,管理需要存档的数据。

玩家类中,通过备忘录类实现存档、读档:

class Player {...// 存档public Memento saveState() {return new Memento(life, magic);}// 读档public void restoreState(Memento memento) {this.life = memento.life;this.magic = memento.magic;}
}

客户端类对应修改如下:

public class Client {@Testpublic void test() {Player player = new Player();// 存档Memento memento = player.saveState();// 打 Boss,打不过,壮烈牺牲player.fightBoss();// 读档player.restoreState(memento);}
}

这才是完整的备忘录模式。这个设计模式的定义如下:

备忘录模式:在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。

备忘录模式的优点是:

  • 给用户提供了一种可以恢复状态的机制,使用户能够比较方便的回到某个历史的状态
  • 实现了信息的封装,使得用户不需要关心状态的保存细节

缺点是:

  • 消耗资源,如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

总体而言,备忘录模式是利大于弊的,所以许多程序都为用户提供了备份方案。比如 IDE 中,用户可以将自己的设置导出成 zip,当需要恢复设置时,再将导出的 zip 文件导入即可。这个功能内部的原理就是备忘录模式。

Ok,今天的设计模式就学到这里,剩下的几个设计模式我们在下一篇文章中学习。

设计模式(五) —— 行为型模式(中)相关推荐

  1. java设计模式中不属于创建型模式_23种设计模式第二篇:java工厂模式定义:工厂模式是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式...

    23种设计模式第二篇:java工厂模式 定义: 工厂模式是 Java 中最常用的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 工厂模式主要是为创建对象提供过渡接口, ...

  2. Java学习--设计模式之创建型模式

    一.简介 创建型模式:这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象.这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活.创建型模式包括:工 ...

  3. 备战面试日记(3.2) - (设计模式.23种设计模式之创建型模式)

    本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试. 记录日期:2022.1.6 大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习. 文章 ...

  4. Java23种设计模式——19.行为型模式之中介者模式

    Java中除去有设计原则之外,还有23中设计模式. 这些模式都是前辈们一点一点积累下来,一直在改进,一直在优化的,而这些设计模式可以解决一些特定的问题. 并且在这些模式中,可以说是将语言的使用体现的淋 ...

  5. 备战面试日记(3.4) - (设计模式.23种设计模式之行为型模式)

    本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试. 记录日期:2022.1.12 大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习. 文 ...

  6. 设计模式之创建型模式(工厂、原型、建造者)

    文章目录 创建型模式 2.1 工厂设计模式 2.1.1 简单工厂模式 2.1.2 工厂方法模式 2.1.3 抽象工厂 2.1.4 工厂模式总结 2.1.5 Spring中的工厂模式 2.1.6 工作中 ...

  7. 设计模式_行为型模式学习

    我们知道,创建型设计模式主要解决"对象的创建"问题,结构型设计模式主要解决"类或对象的组合或组装"问题,那行为型设计模式主要解决的就是"类或对象之间的 ...

  8. 设计模式之行为型模式(7种)

    目录 一.模版方法模式(template ) 概念 模式中的角色 模板模式UML类图 案例 使用前 使用后 钩子函数应用场景 注意事项和细节 应用 优点 模板方法模式与开闭原则 二.命令模式 概念: ...

  9. java real football_Java学习--设计模式之行为型模式(三)

    一.空对象模式(Null Object Pattern) 1.概念 在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查.Null 对象不是检查空值,而是 ...

  10. GoF的23种设计模式之创建型模式的特点和分类

    创建型模式的主要关注点是"怎样创建对象?",它的主要特点是"将对象的创建与使用分离".这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关 ...

最新文章

  1. 强势分享5款超级实用的办公软件,建议收藏!
  2. 液态大脑与固态大脑——圣塔菲最新群体智能文集
  3. python cookbook 2字符串 (1)
  4. python创意编程比赛-关于举办2019年青岛市青少年创意编程与智能设计大赛的通知...
  5. angular 自定义指令参数详解
  6. hprofile教程
  7. ionic助手 v1.9.0 一键式开发环境工具(告别命令行,超强功能)
  8. activeMQ在文件上传的应用
  9. oracle mysql 透明网关_如何在Oracle中建立透明网关
  10. 柱状图带立体效果_PS教程!手把手教你打造立体感欧美风人像大片效果(已打包好素材资料见文末)...
  11. mysqladmin命令常用参数实例讲解
  12. 计算机绘图说课视频,机械图识读与计算机绘图说课PPT课件.ppt
  13. 庄表伟:License之外,社区的规则与潜规则 | DEV. Together 2021 中国开发者生态峰会...
  14. 人工智能 2.知识表示
  15. python3--opencc安装方式
  16. 一个IT猎头关于跳与不跳的回复
  17. 顶会 INFOCOM 巴黎进行时,最高荣誉花落微软老将
  18. 在线零售的未来看起来就是网红的带货直播
  19. iOS打包pod spec
  20. java 加法计算器

热门文章

  1. ue4 物品随机生成
  2. 单片机shell命令_单片机的DB指令使用
  3. NEUZZ: Efficient Fuzzing with Neural Program Smoothing
  4. python爬虫爬取淘宝商品并保存至mongodb数据库
  5. 【Windows回收站】Windows删除文件总结
  6. 数据压缩|PNG文件格式分析
  7. 2003域服务器 d盘部分共享文件夹突然不见 但分大小没变化,服务器共享文件夹权限...
  8. 【KeyError:Caught KeyError in Dataloader worker process 0. KeyError:‘标签’】
  9. Elasticsearch Scripted Metric Aggregation 自定义聚合
  10. Cyclodextrin-PEG-Cellobiose 、Lentinan、starch ,CD环糊精修饰多糖