设计模式学习笔记 - 设计模式与范式 -行为型:8.状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

概述

本章学习状态模式。在实际的开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上看,它有点像我们之前讲到的组合模式。

状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有很多种,除了状态模式,比较常用的还有分支逻辑法和查表法。本章就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。


什么是有限状态机

有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称状态机。状态机有三个组成部分:状态(State)、事件(Event)、动作(Action)。

  • 事件也称为转移条件(Transaction Condition)。
  • 事件触发状态的转移及动作的执行。
  • 不过,动作不是必须得,也可能只转移状态,不执行任何动作。

对于刚刚给出的状态机定义,结合一个具体的例子进行解释。

“超级马里奥” 游戏不知道你玩过没有?在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应地增减积分。比如,初始状态是小马里奥,吃了蘑菇后就变成超级马里奥,并且增加 100 积分。

实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机种的 “状态”,游戏情节(比如吃了蘑菇)就是状态机种的 “事件”,加减积分就是状态机种的 “动作”。比如,吃蘑菇这个事件,会触发状态的转移(从小马里奥转移到超级马里奥),以及触发动作的执行(增加 100 积分)。

为方便讲解,我对游戏背景做了简化,只保留了部分状态和事件,简化之后的状态转移如下所示:

在这里插入图片描述
如何编程来实现上面的状态机呢?

我写了个骨架代码,如下所示。其中,obtainMushRoom()obtainCape()obtainFileFlower()meetMonster() 这个几个函数,能够根据当前的状态和事件,更新状态和增减积分。不过,具体的代码实现暂时没给出。你可以先试着自己补全一下。

public enum State {SMALL(0),SUPER(1),FIRE(2),CAPE(3),;private int value;State(int value) {this.value = value;}public int getValue() {return value;}
}public class MarioStateMachine {private int score;private State currentState;public MarioStateMachine() {this.score = 0;this.currentState = State.SMALL;}public void obtainMushRoom() {//TODO}public void obtainCape() {//TODO}public void obtainFireFlower() {//TODO}public void meetMonster() {//TODO}public int getScore() {return score;}public State getState() {return currentState;}
}public class ApplicationDemo {public static void main(String[] args) {MarioStateMachine mario = new MarioStateMachine();mario.obtainMushRoom();int score = mario.getScore();State state = mario.getState();System.out.println("mario score: " + score + "; state: " + state);}
}

状态机实现方式一:分支逻辑法

对于如何实现状态机,有三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-esle 或 switch-else 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法

按照这个思路,将上面的骨架代码补全一下。补全之后的代码如下所示:

public class MarioStateMachine {private int score;private State currentState;public MarioStateMachine() {this.score = 0;this.currentState = State.SMALL;}public void obtainMushRoom() {if (currentState == State.SMALL) {this.currentState = State.SUPER;this.score += 100;}}public void obtainCape() {if (currentState == State.SMALL || currentState == State.SUPER) {this.currentState = State.CAPE;this.score += 200;}}public void obtainFireFlower() {if (currentState == State.SMALL || currentState == State.SUPER) {this.currentState = State.FIRE;this.score += 300;}}public void meetMonster() {if (currentState == State.SUPER) {this.currentState = State.SMALL;this.score -= 100;}if (currentState == State.CAPE) {this.currentState = State.SMALL;this.score -= 200;}if (currentState == State.FIRE) {this.currentState = State.SMALL;this.score -= 300;}}public int getScore() {return score;}public State getState() {return currentState;}
}

对于简单的状态机来说,分支逻辑这种实现方式是可以接收的。但是,对于复杂的状态机来说,这种实现方式及其容易漏写或错写某个状态转移。此外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪些修改了状态机种的某个转移,我们要在冗长的分支逻辑中找到对应地代码进行修改,很容易改错,引入 BUG。

状态机实现方式二:查表法

实际上,上面的实现方式有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,看下如何利用查表法来补全骨架代码。

实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。这个二维表中,第一维表示当前的状态,第二维表示当前状态经过事件之后,转移到的新状态及其执行的动作。

E1(Got MushRoom)E2(Got Cape)E3(Got Fire Flower)E4(Meet Monster)
SmallSuper/+100Cape/+200Fire/+300/
Super/Cape/+200Fire/+300Small/-100
Cape///Small/-200
Fire///Small/-300

注:表中的斜杠表示不存在这种状态转移

相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transactionTableactionTable 两个二维数组即可。实际上,如果我们把二维数组存储在配置文件中,当需要修改状态机时,甚至可以不修改代码,只需要修改配置文件就可以了。具体代码如下所示:

public enum Event {GOT_MUSHROOM(0),GOT_CAPE(1),GOT_FIRE(2),MEET_MONSTER(3),;private int value;Event(int value) {this.value = value;}public int getValue() {return value;}
}public class MarioStateMachine {private int score;private State currentState;private static final State[][] transitionTable = {{SUPER, CAPE, FIRE, SMALL},{SUPER, CAPE, FIRE, SMALL},{CAPE, CAPE, CAPE, SMALL},{FIRE, FIRE, FIRE, SMALL}};private static final int[][] actionTable = {{100, 200, 300, 0},{0, 200, 300, -100},{0, 0, 0, -200},{0, 0, 0, -300},};public MarioStateMachine() {this.score = 0;this.currentState = State.SMALL;}public void obtainMushRoom() {executeEvent(Event.GOT_MUSHROOM);}public void obtainCape() {executeEvent(Event.GOT_CAPE);}public void obtainFireFlower() {executeEvent(Event.GOT_FIRE);}public void meetMonster() {executeEvent(Event.MEET_MONSTER);}private void executeEvent(Event event) {int stateValue = currentState.getValue();int eventValue = event.getValue();this.currentState = transitionTable[stateValue][eventValue];this.score += actionTable[stateValue][eventValue];}public int getScore() {return score;}public State getState() {return currentState;}
}

状态机实现方式三:状态模式

在查表法的代码实现中,事件触发的动作只能是简单的积分加减,所以,我们用一个 int 类型的二维数组 actionTable 就能表示,二维数组中的值表示积分的加减值。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。也就是说,查表法的实现方式有一定的局限性。

虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码的可读性和可维护性不好等。实际上,对于分支逻辑法存在的问题,可以使用状态模式来解决。

状态模式通过将事件触发的状态转移和动作执行,拆分到不同的类中,来避免分支判断逻辑。我们还是结合代码来理解这句话。

其中,IMario 是状态的接口,定义了所有事件。SmallMarioSuperMarioCapeMarioFireMarioIMario 接口的实现类,分别对应状态机种的 4 个状态。原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 中,现在,这些代码被拆分到了这 4 个状态类中。

public interface IMario {State getName();// 以下是定义的事件void obtainMushRoom();void obtainCape();void obtainFireFlower();void meetMonster();
}public class SmallMario implements IMario {private MarioStateMachine stateMachine;public SmallMario(MarioStateMachine stateMachine) {this.stateMachine = stateMachine;}@Overridepublic State getName() {return State.SMALL;}@Overridepublic void obtainMushRoom() {stateMachine.setCurrentState(new SuperMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 100);}@Overridepublic void obtainCape() {stateMachine.setCurrentState(new CapeMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 200);}@Overridepublic void obtainFireFlower() {stateMachine.setCurrentState(new FireMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster() {// do nothing...}
}public class SuperMario implements IMario {private MarioStateMachine stateMachine;public SuperMario(MarioStateMachine stateMachine) {this.stateMachine = stateMachine;}@Overridepublic State getName() {return State.SUPER;}@Overridepublic void obtainMushRoom() {// do nothing...}@Overridepublic void obtainCape() {stateMachine.setCurrentState(new CapeMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 200);}@Overridepublic void obtainFireFlower() {stateMachine.setCurrentState(new FireMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster() {stateMachine.setCurrentState(new SmallMario(stateMachine));stateMachine.setScore(stateMachine.getScore() - 100);}
}public class CapeMario implements IMario {private MarioStateMachine stateMachine;public CapeMario(MarioStateMachine stateMachine) {this.stateMachine = stateMachine;}@Overridepublic State getName() {return State.CAPE;}@Overridepublic void obtainMushRoom() {// do nothing...}@Overridepublic void obtainCape() {// do nothing...}@Overridepublic void obtainFireFlower() {stateMachine.setCurrentState(new FireMario(stateMachine));stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster() {stateMachine.setCurrentState(new SmallMario(stateMachine));stateMachine.setScore(stateMachine.getScore() - 200);}
}public class FireMario implements IMario {private MarioStateMachine stateMachine;public FireMario(MarioStateMachine stateMachine) {this.stateMachine = stateMachine;}@Overridepublic State getName() {return State.FIRE;}@Overridepublic void obtainMushRoom() {// do nothing...}@Overridepublic void obtainCape() {// do nothing...}@Overridepublic void obtainFireFlower() {// do nothing...}@Overridepublic void meetMonster() {stateMachine.setCurrentState(new SmallMario(stateMachine));stateMachine.setScore(stateMachine.getScore() - 300);}
}public class MarioStateMachine {private int score;private IMario currentState; // 不在使用枚举表示状态public MarioStateMachine() {this.score = 0;this.currentState = new SmallMario(this);}public void obtainMushRoom() {currentState.obtainMushRoom();}public void obtainCape() {currentState.obtainCape();}public void obtainFireFlower() {currentState.obtainFireFlower();}public void meetMonster() {currentState.meetMonster();}public int getScore() {return score;}public void setScore(int score) {this.score = score;}public void setCurrentState(IMario currentState) {this.currentState = currentState;}public IMario getState() {return currentState;}
}

上面的代码实现不难看懂,只需要注意一点,即 MarioStateMachine 和各个状态类之间是双向依赖关系。 MarioStateMachine 依赖各个类是理所当然的,但是反过来,各个状态类为什么要依赖 MarioStateMachine 呢? 这是因为,各个状态类需要更新 MarioStateMachine 中的属性, scorecurrentState

实际上,上面的代码还可以继续优化,可以将状态类设置成单例,比较状态类中不包含任何成员变量。但是,当状态类设计成单例之后,就无法通过构造函数来传递 MarioStateMachine 了,而状态类又要依赖 MarioStateMachine ,那该如何解决呢?

实际上,在《创建型:2.单例模式(中):为什么不推荐使用单例模式?又有何替代方案?》中,提到过集中解决方法,你可以回过头去查看下。在这里,可以通过函数参数将 MarioStateMachine 传递进状态类。根据这个设计思路,对上面的代码进行重构。重构之后的代码如下所示:

public interface IMario {State getName();// 以下是定义的事件void obtainMushRoom(MarioStateMachine stateMachine);void obtainCape(MarioStateMachine stateMachine);void obtainFireFlower(MarioStateMachine stateMachine);void meetMonster(MarioStateMachine stateMachine);
}public class SmallMario implements IMario {private static final SmallMario instance = new SmallMario();private SmallMario() {}public static SmallMario getInstance() {return instance;}@Overridepublic State getName() {return State.SMALL;}@Overridepublic void obtainMushRoom(MarioStateMachine stateMachine) {stateMachine.setCurrentState(SuperMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 100);}@Overridepublic void obtainCape(MarioStateMachine stateMachine) {stateMachine.setCurrentState(CapeMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 200);}@Overridepublic void obtainFireFlower(MarioStateMachine stateMachine) {stateMachine.setCurrentState(FireMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster(MarioStateMachine stateMachine) {// do nothing...}
}public class SuperMario implements IMario {private static final SuperMario instance = new SuperMario();private SuperMario() {}public static SuperMario getInstance() {return instance;}@Overridepublic State getName() {return State.SUPER;}@Overridepublic void obtainMushRoom(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void obtainCape(MarioStateMachine stateMachine) {stateMachine.setCurrentState(CapeMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 200);}@Overridepublic void obtainFireFlower(MarioStateMachine stateMachine) {stateMachine.setCurrentState(FireMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster(MarioStateMachine stateMachine) {stateMachine.setCurrentState(SmallMario.getInstance());stateMachine.setScore(stateMachine.getScore() - 100);}
}public class CapeMario implements IMario {private static final CapeMario instance = new CapeMario();private CapeMario() {}public static CapeMario getInstance() {return instance;}@Overridepublic State getName() {return State.CAPE;}@Overridepublic void obtainMushRoom(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void obtainCape(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void obtainFireFlower(MarioStateMachine stateMachine) {stateMachine.setCurrentState(FireMario.getInstance());stateMachine.setScore(stateMachine.getScore() + 300);}@Overridepublic void meetMonster(MarioStateMachine stateMachine) {stateMachine.setCurrentState(SmallMario.getInstance());stateMachine.setScore(stateMachine.getScore() - 200);}
}public class FireMario implements IMario {private static final FireMario instance = new FireMario();private FireMario() {}public static FireMario getInstance() {return instance;}@Overridepublic State getName() {return State.FIRE;}@Overridepublic void obtainMushRoom(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void obtainCape(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void obtainFireFlower(MarioStateMachine stateMachine) {// do nothing...}@Overridepublic void meetMonster(MarioStateMachine stateMachine) {stateMachine.setCurrentState(SmallMario.getInstance());stateMachine.setScore(stateMachine.getScore() - 300);}
}public class MarioStateMachine {private int score;private IMario currentState; // 不在使用枚举表示状态public MarioStateMachine() {this.score = 0;this.currentState = SmallMario.getInstance();}public void obtainMushRoom() {currentState.obtainMushRoom(this);}public void obtainCape() {currentState.obtainCape(this);}public void obtainFireFlower() {currentState.obtainFireFlower(this);}public void meetMonster() {currentState.meetMonster(this);}public int getScore() {return score;}public void setScore(int score) {this.score = score;}public void setCurrentState(IMario currentState) {this.currentState = currentState;}public IMario getState() {return currentState;}
}

实际上,像游戏这种比较复杂的状态机,包含的状态比较多,优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。

相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以更加推荐使用状态模式来实现。

总结

本章讲解了状态模式。虽然网上有各种各样的状态模式,但是你只要记住状态模式是状态机的一种实现方式即可

状态机又叫有限状态机,它由3部分组成:状态、事件、动作。

  • 其中事件也称为转移条件。
  • 事件触发状态的转移及动作的执行。
  • 不过动作不是必须的,也可能只转移状态,不执行任何动作。

针对状态机,本章总结了三种实现方式。

  • 第一种实现方式叫分支逻辑法。利用 if-else 或 switch-case 分支逻辑,参照状态转移图,将每个状态转移原模原样的直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
  • 第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维素组来表示状态转移图,能极大地提高代码的可读性和可维护性。
  • 第三张实现方式叫状态模式。对于状态不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,首选这种实现方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/302756.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习模型——GBDT和Xgboost

GBDT基本概念: GBDT(Gradient Boosting Decision Tree,简称GBDT)梯度提升决策树,是Gradient Boost 框架下使用较多的一种模型,且在GBDT中,其基学习器是分类回归树也就是CART,且使用…

【LeetCode: 2529. 正整数和负整数的最大计数 + 模拟 + 计数】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

【c 语言】结构体的定义格式及变量初始化

🎈个人主页:豌豆射手^ 🎉欢迎 👍点赞✍评论⭐收藏 🤗收录专栏:C语言 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步&…

【计算机考研】408算法大题怎么练?

先说结论:基础阶段学好各个数据结构与,重点是数组、链表、树、图。然后强化阶段突破算法提 在基础阶段,并不需要过于专门地练习算法。相反,基础阶段的重点应该放在对各种数据结构原理的深入理解上。在我个人的经验中,…

网络协议——VRRP(虚拟路由冗余协议)原理与配置

1. VRRP概述 单网关出现故障后下联业务中断,配置两个及以上的网关时由于IP地址冲突,导致通讯时断时续甚至通信中断。VRRP组播类的网络层协议 2. 协议版本 VRRP v2: 支持认证,仅适用于IPv4网络 VRRP v3: 不支持认证, 适用于IPv4和IPv6两种网…

数据结构—图

图的基本概念 图就是由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为:G(V,E),其中,G 表示一个图,V 表示顶点的集合,E 表示边的集合。 顶点 图中的数据元素,我们称之为顶点,图至少有…

Redis 八种常用数据类型常用命令和应用场景

5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 3 种特殊数据类型:HyperLogLog&#xff0…

C语言自定义类型:联合与枚举的奇幻之旅

** 前言 ** 在C语言的世界中,数据类型犹如魔术师手中的魔法道具,各有其神奇之处。今天,我们就来揭开其中两种自定义类型——联合(union)和枚举(enum)的神秘面纱,带你一起探索它们背…

《荒野大镖客》游戏提示emp.dll文件丢失如何解决?

emp.dll它作为一种动态链接库(DLL)文件,在Windows操作系统中扮演着重要角色。当打开一个程序时,操作系统会将程序的代码和数据加载到内存中,并创建一个进程来运行该程序。在这个过程中,emp.dll负责将这些代…

Hot100【十一】:合并区间

// 先排个序 // 这里巧用链表&#xff0c;可以快速的获取到last&#xff0c;通过last数组的第二个元素和当前数组的第一个元素对比&#xff0c;如果当前数组的第一个元素<last数组的第二个元素, 就需要合并 class Solution { public int[][] merge(int[][] intervals) { // …

实现几何对象按照一定距离向外缓冲

1、首先&#xff0c;确保你已经引入了Turf.js库。你可以通过在HTML文件中添加以下代码来引入 <script src"https://cdn.jsdelivr.net/npm/turf/turf6.5.0/turf.min.js"></script>2、使用turf.buffer实现几何对象按照设定距离扩充 let originalCoordinat…

Linux系统安装内网穿透实现固定公网地址访问本地MinIO服务

文章目录 前言1. 创建Buckets和Access Keys2. Linux 安装Cpolar3. 创建连接MinIO服务公网地址4. 远程调用MinIO服务小结5. 固定连接TCP公网地址6. 固定地址连接测试 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&am…

2024/4/1—力扣—主要元素

代码实现&#xff1a; 思路&#xff1a;摩尔投票算法 int majorityElement(int *nums, int numsSize) {int candidate -1;int count 0;for (int i 0; i < numsSize; i) {if (count 0) {candidate nums[i];}if (nums[i] candidate) {count;} else {count--;}}count 0;…

【资源分享】这个网站我愿称之为年度学术最伟大的发现

::: block-1 “时问桫椤”是一个致力于为本科生到研究生教育阶段提供帮助的不太正式的公众号。我们旨在在大家感到困惑、痛苦或面临困难时伸出援手。通过总结广大研究生的经验&#xff0c;帮助大家尽早适应研究生生活&#xff0c;尽快了解科研的本质。祝一切顺利&#xff01;—…

嵌入式学习51-单片机4

知识零碎&#xff1a; nop空指令 CRC校验 为了保证51单片与温度传感18b20 之间的高电平 采用一个上拉电阻改变电平的高低 温度寄存器原理

【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)

文章目录 前言AOP与Spring AOPAspect简单案例快速入门 一、Pointcutannotation 二、五种通知Advice1. Before前置通知2. After后置通知3. AfterRunning返回通知4. AfterThrowing异常通知5. Around环绕通知 总结 前言 在微服务流行的当下&#xff0c;在使用SpringCloud/Springb…

Vue和FastAPI实现前后端分离

前言 近期接触了一些开源大模型应用服务&#xff0c;发现很多用的都是FastAPI web框架&#xff0c;于是乎研究了一下它的优势&#xff0c;印象最深有两个&#xff1a;一个是它的异步处理性能比较好&#xff0c;二是它可以类似java swagger的API交互文档&#xff0c;这个对应前…

服务器远程桌面连接不上怎么办?

随着互联网的发展和远程办公的兴起&#xff0c;服务器远程桌面连接成为了许多企业和个人不可或缺的工具。偶尔我们可能会碰到服务器远程桌面连接不上的情况&#xff0c;这时候我们需要找到解决办法&#xff0c;确保高效地远程访问服务器。 天联组网——突破远程连接障碍 在我们…

性能优化 - 你知道dns-prefetch有什么用吗

难度级别:中级及以上 提问概率:50% 我们在HTML文档里写一个script标签,为src属性指定Javascript文件网络地址,这是一件再平凡不过的事情。当浏览器加载HTML文档,加载到这个script标签的时候,就会去下载Javascript文件。而在下载之前,就…

c# wpf LiveCharts 饼图 简单试验

1.概要 c# wpf LiveCharts 饼图 简单试验 2.代码 <Window x:Class"WpfApp3.Window5"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schem…