热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从零开始学重构——重构的流程及基础重构手法

重构的流程重构手法正如上一次所讲的那样,重构有两个基本条件,一是要保持代码在重构前后的行为基本不变,二是整个过程是受控且尽可能少地产生错误。尤其是对于第二点,产生了一系列的重构手




重构的流程

重构手法


  正如上一次所讲的那样,重构有两个基本条件,一是要保持代码在重构前后的行为基本不变,二是整个过程是受控且尽可能少地产生错误。尤其是对于第二点,产生了一系列的重构手法,每种重构手法都是一系列简单而机械的操作步骤,通过遵循这一系列的操作来实现代码的结构性调整。因此,重构的整个过程就是不断运用不同重构手法的过程,是一个相对有章可循的流程。
  重构手法有大有小,大的重构手法一般由若干小的基础重构组成,进而聚沙成塔实现对代码结构大幅度的调整。完整的重构列表请参见《重构,改善既有代码的设计》一书。
  例如,replace conditional with polymorphism这项复杂重构手法,就至少需要使用self encapsulate, extract method, move method, pull down method这四种基础重构手法。因此在学习类级别的复杂重构手法前,需要先掌握行级别和方法级别的基础重构手法。
  
  重构手法的分解


重构步骤


  重构的宏观步骤一般有如下两种:自上而下式和自下而上式。
  自上而下的重构在重构前,心中已经大致知道重构后的代码将会是什么形态,然后至上而下地将步骤分解出来,并使用相应的重构步骤一一实现,最终达到重构后的形态。其流程为:
  
1. 识别代码中的坏味道
2. 运用设计原则,构思出修改后的目标状态
3. 将目标状态分解为一或多项重构步骤
4. 运用重构步骤


  自下而上的重构则对重构后的代码没有一个完整而清晰的认识。一般而言,每种重构手法都有助于我们解决某种类型的代码坏味,而自下而上的重构则针对每个发现的代码坏味直接运用对应的重构手法,直到没有明显的坏味,此时的代码即能自动满足某种设计模式。是一种迭代的思路,也是所谓重构到模式的思路。其流程为:
  
1. 识别代码中的坏味道
2. 运用一项或多项重构步骤,消除坏味
3. 重复1-2,直到没有明显坏味


  在一般的情况下,这两种重构流程并不是互斥的,经常交错进行或互相包含。如先运用自上而下的方法识别出代码中的坏味,然后根据设计原则重构到某个实现,再运用自下而上的方法重新寻找新的坏味,迭代重构。


基础重构手法

  由于基础重构手法比较多,而且相对比较简单。因此先列出常用的基础重构手法和简单介绍,并在最后的实践案例中结合基础重构手法来重构代码。


rename(重命名变量/方法/类)



  • 坏味:含义不清的命名

  • 说明:变量名应当体现出变量的作用和含义、方法名应当表现出方法的效果、类名也应提示类的职责和在继承体系中的位置。

  • 操作方法:IntelliJ Shift+F6


reorder(调整语句顺序)



  • 坏味:变量的申请和使用分离太远

  • 说明:变量的使用应当尽可能离使用近一些,否则会扩大变量的作用域,在重构时也会产生困难。

  • 操作方法:IntelliJ Alt+Shift+↑↓ 针对无副作用的语句,直接调整语句位置。


split for/block(拆分for循环/代码块)



  • 坏味:一个循环或代码块中同时操作了多个变量或执行了多个职责

  • 说明:一个循环中若有太多变量要计算,不利于将此循环提取为单独方法。

  • 操作方法:



    1. 将循环复制一次

    2. 每个循环中只保留一个变量的计算

    3. 将循环提取为独立方法

    4. 将所有循环的出现替换为方法的调用



guard clauses(卫语句)



  • 坏味:过深的条件嵌套

  • 说明:先判断跳出/过滤的条件,并直接return或continue,可除去多余的else嵌套深度。

  • 操作方法:



    1. IntelliJ 在if语句上Alt+Enter,选择invert if,可倒转if和else语句

    2. IntelliJ 在else语句上Alt+Enter,选择remove redundant else



extract variable(提取变量)



  • 坏味:单条语句过长,含义不清

  • 说明:将部分语句提取出变量,并为变量起一个能够解释变量含义的名称来替代注释

  • 操作说明:IntelliJ Ctrl+Alt+V


extract method(提取方法)



  • 坏味:单个方法过长,含义不清

  • 说明:将做同一件事的代码提取出方法(一般为计算某个变量,或进行单个复杂操作),并为方法起一个能够解释”这件事”的名称来替代注释

  • 操作说明:IntelliJ Ctrl+Alt+M,需要考虑返回和参数的列表,返回不能超过1个变量


inline method(内联方法)



  • 坏味:方法只有一行代码,且内容本身已经很明确(多行也可以,但若原方法有返回值,则会比较复杂,不推荐)

  • 说明:方法的作用是聚合操作并提供注释信息,若方法内容已经明确,则方法本身就起不到作用,反而增加复杂度

  • 操作说明:将方法内容复制后,替换方法调用的部分。再删除方法本身


add parameter(方法增加参数)



  • 坏味:方法主体只有部分变量不同

  • 说明:可以提取变化的部分成为参数,从而合并两个相似的方法

  • 操作说明:



    1. 将变量的部分提取为变量,并提到方法的最开始处

    2. 将方法剩余的内容提取为一个新的方法,新方法会含有新的参数

    3. 将原来的老方法内联



案例实践

重构前


private Set channelColumns;

public String generateSql() {
String channelColumnClauseTemp = StringUtil.flat(channelColumns, ",", "", "");
String channelColumnClause;
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
channelColumnClause = StringUtil.flat(columns, ",", "", "");

String channelColumnsReviewTemp = "";
String channelColumnsReview = "";
if (!channelColumns.isEmpty()) {
channelColumnsReviewTemp = channelColumnClauseTemp +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
channelColumnsReview = channelColumnClause +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
channelColumnsReviewTemp = idTypeColumn + ",batch_id";
channelColumnsReview = idTypeColumn + ",batch_id";
}

StringBuffer vsql = new StringBuffer();
vsql.append("insert into ").append(Constant.DB_SCHEMA).append(".").append(tableName)
.append(" (").append(channelColumnsReview).append(")")
.append(" select distinct ").append(channelColumnsReviewTemp.replace(Constant.CITYNAME_COLUMN, "isnull(" + Constant.CITYNAME_COLUMN + ",'未知城市') as " + Constant.CITYNAME_COLUMN))
.append(" from ").append(Constant.DB_SCHEMA).append(".").append(sourceTableName).append(";\n");

return reviewTempTableSql + vsql.toString();
}

  代码中的坏味有:1. 过长的方法,超过了20行或一屏,2. 变量的命名含义不清,读者无法理解channelColumnClauseTemp, channelColumnClause以及它们之间的关系,3. if-else中存在重复代码。
  下来我们就来使用一系列基础重构手法来整理这段代码。


调整变量申明位置


  仔细观察,发现channelColumnClauseTemp变量只在if语句中使用,因此将channelColumnClauseTemp变量的申请放到if中去:


if (!channelColumns.isEmpty()) {
String channelColumnClauseTemp = StringUtil.flat(channelColumns, ",", "", "");
channelColumnsReviewTemp = channelColumnClauseTemp +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
channelColumnsReview = channelColumnClause +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
}

  同样,channelColumnClause也只在if块中使用,但其中还涉及了columns及for循环部分,也一并移动到if块中:


if (!channelColumns.isEmpty()) {
String channelColumnClauseTemp = StringUtil.flat(channelColumns, ",", "", "");
channelColumnsReviewTemp = channelColumnClauseTemp +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

String channelColumnClause;
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
channelColumnClause = StringUtil.flat(columns, ",", "", "");
channelColumnsReview = channelColumnClause +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
}

重命名变量


  仔细观察channelColumnsReviewTemp和channelColumnsReview两个变量的使用场景,发现它们是所拼接的sql语句的select xxx和insert (yyy)这两个部分,因此实际上是源表的列名和目标表的列名。从而将channelColumnsReviewTemp命名为sourceColumnsStr,将channelColumnsReview命名为targetColumnsStr。
  同样,观察channelColumnClauseTemp和channelColumnClause,它们分别用于计算channelColumnsReviewTemp和channelColumnsReview,因此对应的命名为sourceColumnsWithoutBatchId和targetColumnsWithoutBatchId:


String sourceColumnsStr = "";
String targetColumnsStr = "";
if (!channelColumns.isEmpty()) {
String sourceColumnsWithoutBatchId = StringUtil.flat(channelColumns, ",", "", "");
sourceColumnsStr = sourceColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

String targetColumnsWithoutBatchId;
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");
targetColumnsStr = targetColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
sourceColumnsStr = idTypeColumn + ",batch_id";
targetColumnsStr = idTypeColumn + ",batch_id";
}

拆分代码块


  再观察,发现if-else代码块中同时操作了sourceColumnsStr和targetColumnsStr两个变量,不利于后续运用提取方法的重构手法。因此需要运用split block手法,将这两个变量的计算拆分到两个代码块中。先完整拷贝一份if-else代码,并在第一份中保留对sourceColumnsStr的计算,在第二份中保留对targetColumnsStr的计算,并且调整一下这两个变量申明的位置,到if-else计算逻辑的前面:


String sourceColumnsStr = "";
if (!channelColumns.isEmpty()) {
String sourceColumnsWithoutBatchId = StringUtil.flat(channelColumns, ",", "", "");
sourceColumnsStr = sourceColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
sourceColumnsStr = idTypeColumn + ",batch_id";
}

String targetColumnsStr = "";
if (!channelColumns.isEmpty()) {
String targetColumnsWithoutBatchId;
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");
targetColumnsStr = targetColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
targetColumnsStr = idTypeColumn + ",batch_id";
}

提取方法


  至此,可以提取两个方法:computeSourceColumnsStr()以及computeTargetColumnsStr():


private String computeTargetColumnsStr() {
String targetColumnsStr = "";
if (!channelColumns.isEmpty()) {
String targetColumnsWithoutBatchId;
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");
targetColumnsStr = targetColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
targetColumnsStr = idTypeColumn + ",batch_id";
}
return targetColumnsStr;
}

private String computeSourceColumnsStr() {
String sourceColumnsStr = "";
if (!channelColumns.isEmpty()) {
String sourceColumnsWithoutBatchId = StringUtil.flat(channelColumns, ",", "", "");
sourceColumnsStr = sourceColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
sourceColumnsStr = idTypeColumn + ",batch_id";
}
return sourceColumnsStr;
}

  观察一下提取出来的两个方法,发现他们的不同之处在于computeTargetColumnsStr()中多了一个对columns集合的计算逻辑,于是将columns的计算逻辑再封装一下:


private String computeTargetColumnsStr() {
String targetColumnsStr = "";
if (!channelColumns.isEmpty()) {
Set columns = getTargetColumns();
String targetColumnsWithoutBatchId;
targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");
targetColumnsStr = targetColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
targetColumnsStr = idTypeColumn + ",batch_id";
}
return targetColumnsStr;
}

private Set getTargetColumns() {
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
return columns;
}

  经过对比,还有


String targetColumnsWithoutBatchId;
targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");

  这两行与String sourceColumnsWithoutBatchId = StringUtil.flat(channelColumns, ",", "", "");存在不一致。但可以使用变量的内联重构成一样的形式。
  经过整理后代码的形式如下:


public String generateSql() {

String sourceColumnsStr = computeSourceColumnsStr();

String targetColumnsStr = computeTargetColumnsStr();

StringBuffer vsql = new StringBuffer();
vsql.append("insert into ").append(Constant.DB_SCHEMA).append(".").append(tableName)
.append(" (").append(targetColumnsStr).append(")")
.append(" select distinct ").append(sourceColumnsStr.replace(Constant.CITYNAME_COLUMN, "isnull(" + Constant.CITYNAME_COLUMN + ",'未知城市') as " + Constant.CITYNAME_COLUMN))
.append(" from ").append(Constant.DB_SCHEMA).append(".").append(sourceTableName).append(";\n");

return reviewTempTableSql + vsql.toString();
}

private String computeTargetColumnsStr() {
String targetColumnsStr = "";
if (!channelColumns.isEmpty()) {
Set columns = getTargetColumns();
String targetColumnsWithoutBatchId = StringUtil.flat(columns, ",", "", "");
targetColumnsStr = targetColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
targetColumnsStr = idTypeColumn + ",batch_id";
}
return targetColumnsStr;
}

private String computeSourceColumnsStr() {
String sourceColumnsStr = "";
if (!channelColumns.isEmpty()) {
String sourceColumnsWithoutBatchId = StringUtil.flat(channelColumns, ",", "", "");
sourceColumnsStr = sourceColumnsWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
sourceColumnsStr = idTypeColumn + ",batch_id";
}
return sourceColumnsStr;
}

private Set getTargetColumns() {
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
return columns;
}

提炼参数


  观察两个方法,发现只有columns和channleColumns不同,其它均相同。因此彩提炼参数的方法,为其增加参数,从而合并为一个方法。做法是先将channleColumns再重新提取一个名为columns的变量,并提到方法最开始处。再把方法中的局部变量重命名一下,就变成了:


private String computeSourceColumnsStr() {
Set columns = this.channelColumns;
String columnsStr = "";
if (!channelColumns.isEmpty()) {
String columnsStrWithoutBatchId = StringUtil.flat(columns, ",", "", "");
columnsStr = columnsStrWithoutBatchId +
(this.channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
columnsStr = idTypeColumn + ",batch_id";
}
return columnsStr;
}

同样,将computeTargetColumnsStr()方法也处理一下:


private String computeTargetColumnsStr() {
Set columns = getTargetColumns();
String columnsStr = "";
if (!channelColumns.isEmpty()) {
String columnsStrWithoutBatchId = StringUtil.flat(columns, ",", "", "");
columnsStr = columnsStrWithoutBatchId +
(channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";
} else {
columnsStr = idTypeColumn + ",batch_id";
}
return columnsStr;
}

再将两个方法的剩下内容提取为一个新的方法,新方法含有columns作为参数:


private String computeTargetColumnsStr() {
Set columns = getTargetColumns();
return transformToString(columns);
}

private String computeSourceColumnsStr() {
Set columns = this.channelColumns;
return transformToString(columns);
}

private String transformToString(Set columns) {
String columnsStr = "";
if (!channelColumns.isEmpty()) {
String columnsStrWithoutBatchId = StringUtil.flat(columns, ",", "", "");
columnsStr = columnsStrWithoutBatchId +
(this.channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
columnsStr = idTypeColumn + ",batch_id";
}
return columnsStr;
}

内联方法,并整理


  原先提取出来的两个方法就只剩下2行内容了,其中一行中变量的申明,可以将变量内联:


private String computeTargetColumnsStr() {
return transformToString(getTargetColumns());
}

private String computeSourceColumnsStr() {
return transformToString(this.channelColumns);
}

  再将这两个方法内联,最终形成如下的形式:


public String generateSql() {

String sourceColumnsStr = transformToString(this.channelColumns);
String targetColumnsStr = transformToString(getTargetColumns());

StringBuffer vsql = new StringBuffer();
vsql.append("insert into ").append(Constant.DB_SCHEMA).append(".").append(tableName)
.append(" (").append(targetColumnsStr).append(")")
.append(" select distinct ").append(sourceColumnsStr.replace(Constant.CITYNAME_COLUMN, "isnull(" + Constant.CITYNAME_COLUMN + ",'未知城市') as " + Constant.CITYNAME_COLUMN))
.append(" from ").append(Constant.DB_SCHEMA).append(".").append(sourceTableName).append(";\n");

return reviewTempTableSql + vsql.toString();
}

private String transformToString(Set columns) {
String columnsStr = "";
if (!channelColumns.isEmpty()) {
String columnsStrWithoutBatchId = StringUtil.flat(columns, ",", "", "");
columnsStr = columnsStrWithoutBatchId +
(this.channelColumns.contains(idTypeColumn) ? "" : ("," + idTypeColumn)) + ",batch_id";

} else {
columnsStr = idTypeColumn + ",batch_id";
}
return columnsStr;
}

private Set getTargetColumns() {
Set columns = new TreeSet<>();
for (String str : channelColumns) {
if (ChannelId.CLT_BUS_EML_ADR.toString().equals(str)) {
columns.add(ChannelId.CLT_EML_ADR.toString());
} else {
columns.add(str);
}
}
return columns;
}

再一次提炼方法


  至此,整个代码中没有重复代码,每个方法长度得到控制,命名也比较恰当,重构可以至此结束。但对于高要求的风格而言,应该要求方法中的每个子方法都在同一抽象粒度上。然而transformToString()方法与后续的sql拼接并不在一个抽象粒度上,因此可以将sql拼接再提取到一个新方法中,从而增加可读性。
  做法是先将vsql.toString()提取为变量:


StringBuffer vsql = new StringBuffer();
vsql.append("insert into ").append(Constant.DB_SCHEMA).append(".").append(tableName)
.append(" (").append(targetColumnsStr).append(")")
.append(" select distinct ").append(sourceColumnsStr.replace(Constant.CITYNAME_COLUMN, "isnull(" + Constant.CITYNAME_COLUMN + ",'未知城市') as " + Constant.CITYNAME_COLUMN))
.append(" from ").append(Constant.DB_SCHEMA).append(".").append(sourceTableName).append(";\n");
String insertSql = vsql.toString();
return reviewTempTableSql + insertSql;

  再将return之前的部分提取到新方法generateInsertSql()中:


public String generateSql() {

String sourceColumnsStr = transformToString(this.channelColumns);
String targetColumnsStr = transformToString(getTargetColumns());

String insertSql = generateInsertSql(sourceColumnsStr, targetColumnsStr);
return reviewTempTableSql + insertSql;
}

总结


  重构过程到此结束。
  整个重构过程中,使用了reorder, rename, extract variable, extract method, inline method, split for/code block, add parameter等手法。观察一下每个步骤都是可控的,如果重构在每个步骤后停止,代码依然可以运行。更重要的是,每个步骤都能被证明保持了原有代码的行为。这也是重构最重要的两个条件。
  重构的案例代码:case1
  







推荐阅读
  • 本文详细探讨了JDBC(Java数据库连接)的内部机制,重点分析其作为服务提供者接口(SPI)框架的应用。通过类图和代码示例,展示了JDBC如何注册驱动程序、建立数据库连接以及执行SQL查询的过程。 ... [详细]
  • 利用决策树预测NBA比赛胜负的Python数据挖掘实践
    本文通过使用2013-14赛季NBA赛程与结果数据集以及2013年NBA排名数据,结合《Python数据挖掘入门与实践》一书中的方法,展示如何应用决策树算法进行比赛胜负预测。我们将详细讲解数据预处理、特征工程及模型评估等关键步骤。 ... [详细]
  • 本文详细介绍了Java中org.w3c.dom.Text类的splitText()方法,通过多个代码示例展示了其实际应用。该方法用于将文本节点在指定位置拆分为两个节点,并保持在文档树中。 ... [详细]
  • 毕业设计:基于机器学习与深度学习的垃圾邮件(短信)分类算法实现
    本文详细介绍了如何使用机器学习和深度学习技术对垃圾邮件和短信进行分类。内容涵盖从数据集介绍、预处理、特征提取到模型训练与评估的完整流程,并提供了具体的代码示例和实验结果。 ... [详细]
  • 对象自省自省在计算机编程领域里,是指在运行时判断一个对象的类型和能力。dir能够返回一个列表,列举了一个对象所拥有的属性和方法。my_list[ ... [详细]
  • 本文介绍如何使用 Python 提取和替换 .docx 文件中的图片。.docx 文件本质上是压缩文件,通过解压可以访问其中的图片资源。此外,我们还将探讨使用第三方库 docx 的方法来简化这一过程。 ... [详细]
  • Scala 实现 UTF-8 编码属性文件读取与克隆
    本文介绍如何使用 Scala 以 UTF-8 编码方式读取属性文件,并实现属性文件的克隆功能。通过这种方式,可以确保配置文件在多线程环境下的一致性和高效性。 ... [详细]
  • 本文将详细探讨Linux pinctrl子系统的各个关键数据结构,帮助读者深入了解其内部机制。通过分析这些数据结构及其相互关系,我们将进一步理解pinctrl子系统的工作原理和设计思路。 ... [详细]
  • 在Oracle数据库中,使用Dbms_Output.Put_Line进行输出调试时,若单行字符超过255个,则会遇到ORA-20000错误。本文介绍了一种有效的方法来处理这种情况,通过创建自定义包和视图,实现对长字符串的分割和正确输出。 ... [详细]
  • 本文介绍了一个SQL Server自定义函数,用于从字符串中提取仅包含数字和小数点的子串。该函数通过循环删除非数字字符来实现,并附带创建测试表、存储过程以演示其应用。 ... [详细]
  • 本题探讨了在大数据结构背景下,如何通过整体二分和CDQ分治等高级算法优化处理复杂的时间序列问题。题目设定包括节点数量、查询次数和权重限制,并详细分析了解决方案中的关键步骤。 ... [详细]
  • 本文介绍 SQL Server 的基本概念和操作,涵盖系统数据库、常用数据类型、表的创建及增删改查等基础操作。通过实例帮助读者快速上手 SQL Server 数据库管理。 ... [详细]
  • 2018-2019学年第六周《Java数据结构与算法》学习总结
    本文总结了2018-2019学年第六周在《Java数据结构与算法》课程中的学习内容,重点介绍了非线性数据结构——树的相关知识及其应用。 ... [详细]
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
  • 由二叉树到贪心算法
    二叉树很重要树是数据结构中的重中之重,尤其以各类二叉树为学习的难点。单就面试而言,在 ... [详细]
author-avatar
hya641520
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有