细化XML语句构建器,完善静态SQL解析
xsobi 2024-11-24 23:35 1 浏览
原文链接:链接:https://juejin.cn/post/7101145336021778462
一、前言
你只是在解释过程,而他是在阐述高度!
如果不是长时间的沉淀、积累和储备,我一定也没有办法用更多的维度和更多的视角来对一个问题进行多方面阐述。就像你我;越过峭壁山川,才知枕席还师的通达平坦。领略过雷声千嶂落,雨色万峰来,才闻到八表流云澄夜色,九霄华月动春城的宁静。
所以引申到编程开发,往简单了说就是写写代码,改改bug。但如果就局限在只是写写代码,其实很难领略到那些众多设计思想和复杂问题中,庖丁解牛般的酣畅淋漓。而这些酣畅的体验,都需要你对技术的拓展学习和深度探索,从众多的优秀源码框架中吸收经验。反复揣摩、反复尝试,终有那么一个时间点,你会有种领悟了的感觉。而这些一个个感觉的积累,就能帮助你以后在面试、述职、答辩、分享、汇报等场景中,说出更有深度的技术思想和类比设计对照,站在更高的角度俯视业务场景的走向和给出长远的架构方案。
二、目标
实现到本章节前,关于 Mybatis ORM 框架的大部分核心结构已经逐步体现出来了,包括;解析、绑定、映射、事务、执行、数据源等。但随着更多功能的逐步完善,我们需要对模块内的实现进行细化处理,而不单单只是完成功能逻辑。这就有点像吧 CRUD 使用设计原则进行拆分解耦,满足代码的易维护和可扩展性。而这里我们首先要着手处理的就是关于 XML 解析的问题,把之前粗糙的实现进行细化,满足我们对解析时一些参数的整合和处理。
- 这一部分的解析,就是在我们本章节之前的 XMLConfigBuilder#mapperElement 方法中的操作。看上去虽然能实现功能,但总会让人感觉它不够规整。就像我们平常开发的游戏一样一样 CRUD 罗列到一块的逻辑一样,什么流程都能处理,但什么流程都会越来越混乱。
- 就像我们在 ORM 框架 DefaultSqlSession 中调用具体执行数据库操作的方法,需要进行 PreparedStatementHandler#parameterize 参数时,其实并没有准确的定位到参数的类型,jdbcType和javaType的转换关系,所以后续的属性填充就会显得比较混乱且不易于扩展。当然,如果你硬写也是写得出来的,不过这也不是一个好的设计!
- 所以接下来小傅哥会带着读者,把这部分解析的处理,使用设计原则将流程和职责进行解耦,并结合我们的当前诉求,优先处理静态 SQL 内容。待框架结构逐步完善,再进行一些动态SQL和更多参数类型的处理,满足读者以后在阅读 Mybatis 源码,以及需要开发自己的 X-ORM 框架的时候,有一些经验积累。
三、设计
参照设计原则,对于 XML 信息的读取,各个功能模块的流程上应该符合单一职责,而每一个具体的实现又得具备迪米特法则,这样实现出来的功能才能具有良好的扩展性。通常这类代码也会看着很干净 那么基于这样的诉求,我们则需要给解析过程中,所属解析的不同内容,按照各自的职责类进行拆解和串联调用。整体设计如图 9-2
- 与之前的解析代码相对照,不再是把所有的解析都在一个循环中处理,而是在整个解析过程中,引入 XMLMapperBuilder、XMLStatementBuilder 分别处理映射构建器和语句构建器,按照不同的职责分别进行解析。
- 与此同时也在语句构建器中,引入脚本语言驱动器,默认实现的是 XML语言驱动器 XMLLanguageDriver,这个类来具体操作静态和动态 SQL 语句节点的解析。这部分的解析处理实现方式很多,即使自己使用正则或者 String 截取也是可以的。所以为了保持与 Mybatis 的统一,我们直接参照源码 Ognl 的方式进行处理。对应的类是 DynamicContext
- 这里所有的解析铺垫,通过解耦的方式实现,都是为了后续在 executor 执行器中,能更加方便地处理 setParameters 参数的设置。后面参数的设置,也会涉及到前面我们实现的元对象反射工具类的使用。
四、实现
1. 工程结构
mybatis-step-08
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ ├── builder
│ │ ├── xml
│ │ │ ├── XMLConfigBuilder.java
│ │ │ ├── XMLMapperBuilder.java
│ │ │ └── XMLStatementBuilder.java
│ │ ├── BaseBuilder.java
│ │ ├── ParameterExpression.java
│ │ ├── SqlSourceBuilder.java
│ │ └── StaticSqlSource.java
│ ├── datasource
│ ├── executor
│ │ ├── resultset
│ │ │ ├── DefaultResultSetHandler.java
│ │ │ └── ResultSetHandler.java
│ │ ├── statement
│ │ │ ├── BaseStatementHandler.java
│ │ │ ├── PreparedStatementHandler.java
│ │ │ ├── SimpleStatementHandler.java
│ │ │ └── StatementHandler.java
│ │ ├── BaseExecutor.java
│ │ ├── Executor.java
│ │ └── SimpleExecutor.java
│ ├── io
│ ├── mapping
│ │ ├── BoundSql.java
│ │ ├── Environment.java
│ │ ├── MappedStatement.java
│ │ ├── ParameterMapping.java
│ │ ├── SqlCommandType.java
│ │ └── SqlSource.java
│ ├── parsing
│ │ ├── GenericTokenParser.java
│ │ └── TokenHandler.java
│ ├── reflection
│ ├── session
│ │ ├── defaults
│ │ │ ├── DefaultSqlSession.java
│ │ │ └── DefaultSqlSessionFactory.java
│ │ ├── Configuration.java
│ │ ├── ResultHandler.java
│ │ ├── SqlSession.java
│ │ ├── SqlSessionFactory.java
│ │ ├── SqlSessionFactoryBuilder.java
│ │ └── TransactionIsolationLevel.java
│ ├── transaction
│ └── type
│ ├── JdbcType.java
│ ├── TypeAliasRegistry.java
│ ├── TypeHandler.java
│ └── TypeHandlerRegistry.java
└── test
├── java
│ └── cn.bugstack.mybatis.test.dao
│ ├── dao
│ │ └── IUserDao.java
│ ├── po
│ │ └── User.java
│ └── ApiTest.java
└── resources
├── mapper
│ └──User_Mapper.xml
└── mybatis-config-datasource.xml
复制代码
工程源码:github.com/fuzhengwei/…
XML 语句解析构建器,核心逻辑类关系,如图 9-3 所示
- 解耦原 XMLConfigBuilder 中对 XML 的解析,扩展映射构建器、语句构建器,处理 SQL 的提取和参数的包装,整个核心流图以 XMLConfigBuilder#mapperElement 为入口进行串联调用。
- 在 XMLStatementBuilder#parseStatementNode 方法中解析 <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">...</select> 配置语句,提取参数类型、结果类型,而这里的语句处理流程稍微较长,因为需要用到脚本语言驱动器,进行解析处理,创建出 SqlSource 语句信息。SqlSource 包含了 BoundSql,同时这里扩展了 ParameterMapping 作为参数包装传递类,而不是仅仅作为 Map 结构包装。因为通过这样的方式,可以封装解析后的 javaType/jdbcType 信息
2. 解耦映射解析
提供单独的 XML 映射构建器 XMLMapperBuilder 类,把关于 Mapper 内地 SQL 进行解析处理。提供了这个类以后,就可以把这个类的操作方法放到 XML 配置构建器,XMLConfigBuilder#mapperElement 中进行使用了。具体我们看下如下代码。
源码详见:cn.bugstack.mybatis.builder.xml.XMLMapperBuilder
public class XMLMapperBuilder extends BaseBuilder {
/**
* 解析
*/
public void parse() throws Exception {
// 如果当前资源没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
configurationElement(element);
// 标记一下,已经加载过了
configuration.addLoadedResource(resource);
// 绑定映射器到namespace
configuration.addMapper(Resources.classForName(currentNamespace));
}
}
// 配置mapper元素
// <mapper namespace="org.mybatis.example.BlogMapper">
// <select id="selectBlog" parameterType="int" resultType="Blog">
// select * from Blog where id = #{id}
// </select>
// </mapper>
private void configurationElement(Element element) {
// 1.配置namespace
currentNamespace = element.attributeValue("namespace");
if (currentNamespace.equals("")) {
throw new RuntimeException("Mapper's namespace cannot be empty");
}
// 2.配置select|insert|update|delete
buildStatementFromContext(element.elements("select"));
}
// 配置select|insert|update|delete
private void buildStatementFromContext(List<Element> list) {
for (Element element : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);
statementParser.parseStatementNode();
}
}
}
复制代码
在 XMLMapperBuilder#parse 的解析中,主要体现在资源解析判断、Mapper解析和绑定映射器到;
- configuration.isResourceLoaded 资源判断避免重复解析,做了个记录。
- configuration.addMapper 绑定映射器主要是把 namespace cn.bugstack.mybatis.test.dao.IUserDao 绑定到 Mapper 上。也就是注册到映射器注册机里。
- configurationElement 方法调用的 buildStatementFromContext,重在处理 XML 语句构建器,下文中单独讲解。
配置构建器,调用映射构建器,源码详见:cn.bugstack.mybatis.builder.xml.XMLMapperBuilder
public class XMLConfigBuilder extends BaseBuilder {
/*
* <mappers>
* <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
* <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
* <mapper resource="org/mybatis/builder/PostMapper.xml"/>
* </mappers>
*/
private void mapperElement(Element mappers) throws Exception {
List<Element> mapperList = mappers.elements("mapper");
for (Element e : mapperList) {
String resource = e.attributeValue("resource");
InputStream inputStream = Resources.getResourceAsStream(resource);
// 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
mapperParser.parse();
}
}
}
复制代码
- 在 XMLConfigBuilder#mapperElement 中,把原来流程化的处理进行解耦,调用 XMLMapperBuilder#parse 方法进行解析处理。
3. 语句构建器
XMLStatementBuilder 语句构建器主要解析 XML 中 select|insert|update|delete 中的语句,当前我们先以 select 解析为案例,后续再扩展其他的解析流程。
源码详见:cn.bugstack.mybatis.builder.xml.XMLStatementBuilder
public class XMLStatementBuilder extends BaseBuilder {
//解析语句(select|insert|update|delete)
//<select
// id="selectPerson"
// parameterType="int"
// parameterMap="deprecated"
// resultType="hashmap"
// resultMap="personResultMap"
// flushCache="false"
// useCache="true"
// timeout="10000"
// fetchSize="256"
// statementType="PREPARED"
// resultSetType="FORWARD_ONLY">
// SELECT * FROM PERSON WHERE ID = #{id}
//</select>
public void parseStatementNode() {
String id = element.attributeValue("id");
// 参数类型
String parameterType = element.attributeValue("parameterType");
Class<?> parameterTypeClass = resolveAlias(parameterType);
// 结果类型
String resultType = element.attributeValue("resultType");
Class<?> resultTypeClass = resolveAlias(resultType);
// 获取命令类型(select|insert|update|delete)
String nodeName = element.getName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 获取默认语言驱动器
Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);
SqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);
MappedStatement mappedStatement = new MappedStatement.Builder(configuration, currentNamespace + "." + id, sqlCommandType, sqlSource, resultTypeClass).build();
// 添加解析 SQL
configuration.addMappedStatement(mappedStatement);
}
}
复制代码
- 整个这部分内容的解析,就是从 XMLConfigBuilder 拆解出来关于 Mapper 语句解析的部分,通过这样这样的解耦设计,会让整个流程更加清晰。
- XMLStatementBuilder#parseStatementNode 方法是解析 SQL 语句节点的过程,包括了语句的ID、参数类型、结果类型、命令(select|insert|update|delete),以及使用语言驱动器处理和封装SQL信息,当解析完成后写入到 Configuration 配置文件中的 Map<String, MappedStatement> 映射语句存放中。
4. 脚本语言驱动
在 XMLStatementBuilder#parseStatementNode 语句构建器的解析中,可以看到这么一块,获取默认语言驱动器并解析SQL的操作。其实这部分就是 XML 脚步语言驱动器所实现的功能,在 XMLScriptBuilder 中处理静态SQL和动态SQL,不过目前我们只是实现了其中的一部分,待后续这部分框架都完善后在进行扩展,避免一次引入过多的代码。
4.1 定义接口
源码详见:cn.bugstack.mybatis.scripting.LanguageDriver
public interface LanguageDriver {
SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);
}
复制代码
- 定义脚本语言驱动接口,提供创建 SQL 信息的方法,入参包括了配置、元素、参数。其实它的实现类一共有3个;XMLLanguageDriver、RawLanguageDriver、VelocityLanguageDriver,这里我们只是实现了默认的第一个即可。
4.2 XML语言驱动器实现
源码详见:cn.bugstack.mybatis.scripting.xmltags.XMLLanguageDriver
public class XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
}
复制代码
- 关于 XML 语言驱动器的实现比较简单,只是封装了对 XMLScriptBuilder 的调用处理。
4.3 XML脚本构建器解析
源码详见:cn.bugstack.mybatis.scripting.xmltags.XMLScriptBuilder
public class XMLScriptBuilder extends BaseBuilder {
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(element);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
return new RawSqlSource(configuration, rootSqlNode, parameterType);
}
List<SqlNode> parseDynamicTags(Element element) {
List<SqlNode> contents = new ArrayList<>();
// element.getText 拿到 SQL
String data = element.getText();
contents.add(new StaticTextSqlNode(data));
return contents;
}
}
复制代码
- XMLScriptBuilder#parseScriptNode 解析SQL节点的处理其实没有太多复杂的内容,主要是对 RawSqlSource 的包装处理。其他小细节可以阅读源码进行学习
4.4 SQL源码构建器
源码详见:cn.bugstack.mybatis.builder.SqlSourceBuilder
public class SqlSourceBuilder extends BaseBuilder {
private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
public SqlSourceBuilder(Configuration configuration) {
super(configuration);
}
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
// 返回静态 SQL
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
// 构建参数映射
private ParameterMapping buildParameterMapping(String content) {
// 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
Map<String, String> propertiesMap = new ParameterExpression(content);
String property = propertiesMap.get("property");
Class<?> propertyType = parameterType;
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
return builder.build();
}
}
}
复制代码
- 关于以上文中提到的,关于 BoundSql.parameterMappings 的参数就是来自于 ParameterMappingTokenHandler#buildParameterMapping 方法进行构建处理的。
- 具体的 javaType、jdbcType 会体现到 ParameterExpression 参数表达式中完成解析操作。这个解析过程直接是 Mybatis 自己的源码,整个过程功能较单一,直接对照学习即可
5. DefaultSqlSession 调用调整
因为以上整个设计和实现,调整了解析过程,以及细化了 SQL 的创建。那么在 MappedStatement 映射语句中,则使用 SqlSource 替换了 BoundSql,所以在 DefaultSqlSession 中也会有相应的调整。
源码详见:cn.bugstack.mybatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
@Override
public <T> T selectOne(String statement, Object parameter) {
MappedStatement ms = configuration.getMappedStatement(statement);
List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
return list.get(0);
}
}
复制代码
- 这里的使用调整也不大,主要体现在获取SQL的操作;ms.getSqlSource().getBoundSql(parameter) 这样获取后,后面的流程就没有多少变化了。在我们整个解析框架逐步完善后,就会开始对各个字段的属性信息添加进行设置操作。
五、测试
1. 事先准备
1.1 创建库表
创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:
CREATE TABLE
USER
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
userId VARCHAR(9) COMMENT '用户ID',
userHead VARCHAR(16) COMMENT '用户头像',
createTime TIMESTAMP NULL COMMENT '创建时间',
updateTime TIMESTAMP NULL COMMENT '更新时间',
userName VARCHAR(64),
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');
复制代码
1.2 配置数据源
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
复制代码
- 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
- 在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。
1.3 配置Mapper
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
</select>
复制代码
- 这部分暂时不需要调整,目前还只是一个入参的类型的参数,后续我们全部完善这部分内容以后,则再提供更多的其他参数进行验证。
2. 单元测试
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 测试验证
User user = userDao.queryUserInfoById(1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
}
复制代码
- 这里的测试不需要调整,因为我们本章节的开发内容,主要以解耦 XML 的解析,只要能保持和之前章节一样,正常输出结果就可以。
测试结果
07:26:15.049 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 1138410383.
07:26:15.192 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
Disconnected from the target VM, address: '127.0.0.1:54797', transport: 'socket'
Process finished with exit code 0
复制代码
- 从测试结果和调试的截图可以看到,我们的 XML 解析处理拆解后,已经可以顺利的支撑我们的使用。
六、总结
- 本章节我们就像是去把原本 CRUD 的代码,通过设计原则进行拆分和解耦,运用不用的类来承担不同的职责,完整整个功能的实现。这包括;映射构建器、语句构建器、源码构建器的综合使用,以及对应的引用;脚本语言驱动和脚本构建器解析,处理我们的 XML 中的 SQL 语句。
- 通过这样的重构代码,也能让我们对平常的业务开发中的大片面向过程的流程代码有所感悟,当你可以细分拆解职责功能到不同的类中去以后,你的代码会更加的清晰并易于维护。
- 后续我们将继续按照现在的扩展结构底座,完成其他模块的功能逻辑开发,因为了这些基础内容的建造,再继续补充功能也会更加容易。当然这些代码还是需要你熟悉以后才能驾驭,在学习的过程中可以尝试断点调试,看看每一个步骤都在完成哪些工作。
相关推荐
- js向对象中添加元素(对象,数组) js对象里面添加元素
-
一、添加一个元素对象名["属性名"]=值(值:可以是一个值,可以是一个对象,也可以是一个数组)这样添加进去的元素,就是一个值或对象或数组...
- JS小技巧,如何去重对象数组?(一)
-
大家好,关于数组对象去重的业务场景,想必大家都遇到过类似的需求吧,这对这样的需求你是怎么做的呢。下面我就先和大家分享下如果是基于对象的1个属性是怎么去重实现的。方法一:使用.filter()和....
- 「C/C++」之数组、vector对象和array对象的比较
-
数组学习过C语言的,对数组应该都不会陌生,于是这里就不再对数组进行展开介绍。模板类vector模板类vector类似于string,也是一种动态数组。能够在运行阶段设置vector对象的长度,可以在末...
- 如何用sessionStorage保存对象和数组
-
背景:在工作中,我将[{},{}]对象数组形式,存储到sessionStorage,然后ta变成了我看不懂的形式,然后我想取之用之,发现不可能了~记录这次深刻的教训。$clickCouponIndex...
- JavaScript Array 对象 javascript的array对象
-
Array对象Array对象用于在变量中存储多个值:varcars=["Saab","Volvo","BMW"];第一个数组元素的索引值为0,第二个索引值为1,以此类推。更多有...
- JavaScript中的数组Array(对象) js array数组
-
1:数组Array:-数组也是一个对象-数组也是用来存储数据的-和object不同,数组中可以存储一组有序的数据,-数组中存储的数据我们称其为元素(element)-数组中的每一个元素都有一...
- 数组和对象方法&数组去重 数组去重的5种方法前端
-
列举一下JavaScript数组和对象有哪些原生方法?数组:arr.concat(arr1,arr2,arrn);--合并两个或多个数组。此方法不会修改原有数组,而是返回一个新数组...
- C++ 类如何定义对象数组?初始化数组?linux C++第43讲
-
对象数组学过C语言的读者对数组的概念应该很熟悉了。数组的元素可以是int类型的变量,例如int...
- ElasticSearch第六篇:复合数据类型-数组,对象
-
在ElasticSearch中,使用JSON结构来存储数据,一个Key/Value对是JSON的一个字段,而Value可以是基础数据类型,也可以是数组,文档(也叫对象),或文档数组,因此,每个JSON...
- 第58条:区分数组对象和类数组对象
-
示例设想有两个不同类的API。第一个是位向量:有序的位集合varbits=newBitVector;bits.enable(4);bits.enable([1,3,8,17]);b...
- 八皇后问题解法(Common Lisp实现)
-
如何才能在一张国际象棋的棋盘上摆上八个皇后而不致使她们互相威胁呢?这个著名的问题可以方便地通过一种树搜索方法来解决。首先,我们需要写一个函数来判断棋盘上的两个皇后是否互相威协。在国际象棋中,皇后可以沿...
- visual lisp修改颜色的模板函数 怎么更改visual studio的配色
-
(defunBF-yansemokuai(tuyuanyanse/ss)...
- 用中望CAD加载LISP程序技巧 中望cad2015怎么加载燕秀
-
1、首先请加载lisp程序,加载方法如下:在菜单栏选择工具——加载应用程序——添加,选择lisp程序然后加载,然后选择添加到启动组。2、然后是添加自定义栏以及图标,方法如下(以...
- 图的深度优先搜索和广度优先搜索(Common Lisp实现)
-
为了便于描述,本文中的图指的是下图所示的无向图。搜索指:搜索从S到F的一条路径。若存在,则以表的形式返回路径;若不存在,则返回nil。...
- 两个有助于理解Common Lisp宏的例子
-
在Lisp中,函数和数据具有相同的形式。这是Lisp语言的一个重大特色。一个Lisp函数可以分析另一个Lisp函数;甚至可以和另一个Lisp函数组成一个整体,并加以利用。Lisp的宏,是实现上述特色的...
- 一周热门
- 最近发表
- 标签列表
-
- grid 设置 (58)
- 移位运算 (48)
- not specified (45)
- patch补丁 (31)
- strcat (25)
- 导航栏 (58)
- context xml (46)
- scroll (43)
- element style (30)
- dedecms模版 (53)
- vs打不开 (29)
- nmap (30)
- webgl开发 (24)
- parse (24)
- c 视频教程下载 (33)
- android 开发环境 (24)
- paddleocr (28)
- listview排序 (33)
- firebug 使用 (31)
- transactionmanager (30)
- characterencodingfilter (33)
- getmonth (34)
- commandtimeout (30)
- hibernate教程 (31)
- label换行 (33)