设计模式在FileBrowser中的几个应用
设计模式是代码重构的最终目标,在程序设计中有效的运用这项技术,可以大大提高代码的可读性和可维护性。使整个程序设计结构趋向精致完美。在我维护的FileBrowser模块中可以针对以下方面
应用相应的模式。
1. 使用策略模式来处理文件夹扫描操作
作为网络文件浏览器,FileBrowser中自然有很多对文件夹的操作。包括计算文件夹信息,删除文件夹等,这些操作采用的都是标准的文件夹遍历的代码,只是对于各文件/文件夹进行不同的处理而已,基本代码如下:
HANDLE fp = FindFirstFile(szLocalFName, &fd);
while(TRUE)
{
if(fd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)
{
Handle the folder…
}
Else
{
Handle the file…
}
if(!FindNextFile(fp, &Fd))
break;
}
显而易见,这里很适合使用策略模式:定义一个通用的扫描函数,定义策略类架构,使每个策略对象对应一个文件处理方法。这样可以避免模块中大量的相似的文件夹遍历的代码,使之简洁明了,而且更加容易扩充。
我们看一下策略模式的定义:
结构图:
基于这个思想,我设计了以下文件夹扫描结构:
class folder_handler
{
public:
folder_handler(const tstring& root) : m_root(root){}
virtual ~folder_handler(){}
virtual bool handle_folder
(const tstring&, const WIN32_FIND_DATA&){ return true; }
virtual bool handle_file(const tstring&, const WIN32_FIND_DATA&)
{ return true; }
virtual bool folder_first(){ return true; }
protected:
tstring m_root;
};
void scan_folder(const tstring& folder_path, folder_handler& h)
{
tstring child_path(_T(""));
string find_name = folder_path + _T(“//*.*”);
WIN32_FIND_DATA fd = {0};
HANDLE hFindFile = FindFirstFile(find_name.c_str(), &fd);
if (INVALID_HANDLE_VALUE == hFindFile)
{
cout << "Cann't access folder " << folder_path << endl;
return;
}
// 先处理父文件夹
if (h.folder_first())
{
h.handle_folder(folder_path);
}
do
{
if (_T('.') == fd.cFileName[0])
{
continue;
}
child_path = folder_path + _T("//") + fd.cFileName;
if (FILE_ATTRIBUTE_DIRECTORY & fd.dwFileAttributes)
{
scan_folder(child_path, h);
}
else
{
m_numfiles++;
m_foldersize+=
MAKELONGLONG(fd.nFileSizeLow, fd.nFileSizeHigh);
h.handle_file(child_path, fd);
}
} while(FindNextFile(hFindFile, &fd));
FindClose(hFindFile);
// 后处理父文件夹
if (!h.folder_first())
{
h.handle_folder(folder_path);
}
}
这样我们想对文件夹做某种特定操作的时候,只需要定义一个新类,指定对子文件/子文件夹的特定操作即可,而不需要关注其遍历过程的细节,耦合度非常的低。例如我们想删除文件夹和扫描文件夹同步信息的话那么就可以定义以下两个类。
class folder_deleter : public folder_handler
{
public:
folder_deleter(const tstring& parent_folder)
: folder_handler(parent_folder)
{
}
virtual bool handle_folder
(const tstring& folder_path, const WIN32_FIND_DATA&)
{
if (!::RemoveDirectory(folder_path.c_str()))
{
_tcout << _T("Fail to remove the folder ") << folder_path << endl;
_tcout << _T("The error code ") << ::GetLastError() << endl;
return false;
}
return true;
}
virtual bool handle_file
(const tstring& file_path, const WIN32_FIND_DATA&)
{
if (!::DeleteFile(file_path.c_str()))
{
_tcout << _T("Fail to delete the file ") << file_path << endl;
_tcout << _T("The error code ") << ::GetLastError() << endl;
return false;
}
return true;
}
virtual bool folder_first(){ return false; }
};
class folder_inforecorder : public folder_handler
{
typedef struct
{
DWORD attrib;
LONGLONG size;
LONGLONG modified_time;
tstring path;
}file_info;
typedef vector<file_info> file_infos;
typedef file_infos::iterator fis_iter;
typedef file_infos::const_iterator fis_citer;
public:
folder_inforecorder(const tstring& parent_folder)
: folder_handler(parent_folder) {}
~folder_inforecorder(){}
virtual bool handle_file
(const tstring& folder_name, const WIN32_FIND_DATA& fd)
{
file_info fi;
fi.attrib = fd.dwFileAttributes;
fi.size = MAKELONGLONG(fd.nFileSizeLow, fd.nFileSizeHigh);
fi.modified_time =
MAKELONGLONG(fd.ftLastWriteTime.dwLowDateTime,
fd.ftLastWriteTime.dwHighDateTime);
tstring file_path = folder_name + _T("//") + fd.cFileName;
const tstring parentfolder = m_root + _T("//");
tstring::size_type pos = file_path.find(parentfolder);
if (tstring::npos == pos)
fi.path = file_path.c_str();
else
fi.path = file_path.substr(pos + parentfolder.size(),
tstring::npos).c_str();
m_FileInfos.push_back(fi);
return true;
}
public:
file_infos m_FileInfos;
};
使用的时候调用如下代码即可:
scanfolder scan
// 删除路径
scan.run_scan(folder_path, folder_deleter());
folder_inforecorder fir;
// 扫描路径文件信息
scan.run_scan(folder_path, fir);
2. 使用Command命令模式实现文件操作的Undo/Redo
FileBrowser对于文件操作的目前不支持Undo/Redo功能,用户所有的操作都是不可逆的,所以对于用户来说存在一定的操作风险以及不便。我们可以利用Command命令模式来实现这个功能:
将用户的命令封装成不同的对象,支持统一的接口Execute /UnExecute;定义一个命令管理类,支持Undo/Redo接口,维护一个执行命令对象链;每当用户完成一个操作之后,便生成相应的命令对象,将其加入命令对象链尾部,并将当前命令对象迭代器指向它。执行Undo/Redo功能时,根据当前命令对象迭代器调用当前命令对象Execute /UnExecute接口,并将当前命令对象迭代器在命令对象链中做相应的移动。
命令对象链操作示意图
当前命令对象迭代器 |
命令对象链 |
Figrure 3-1
命令对象类体系图
大致代码如下:
class CCommand
{
public:
CCommand(){}
virtual ~CCommand(){}
virtual void Execute() = 0;
virtual void UnExecute() = 0;
};
class CCommandManager
{
public:
CCommandManager() : m_CurPos(m_cCommandList.end()){}
virtual ~CCommandManager(){ Clear(); }
void AddCommand(CCommand* pcCommand)
{
ASSERT(pcCommand);
m_cCommandList.erase(m_cCommandList.begin(), m_CurPos);
m_cCommandList.insert(m_CurPos, pcCommand);
m_CurPos--;
}
BOOL CanUndo()
{
return (m_cCommandList.size() > 0 &&
m_CurPos != m_cCommandList.end());
}
BOOL CanRedo();
{
return (m_cCommandList.size() > 0
&& m_CurPos != m_cCommandList.begin());
}
CCommand* Undo()
{
CCommand* pcCommand = NULL;
if (CanUndo())
{
pcCommand = *m_CurPos;
ASSERT(pcCommand);
pcCommand->UnExecute();
m_CurPos++;
}
return pcCommand;
}
CCommand* Redo()
{
CCommand* pcCommand = NULL;
if (CanRedo())
{
m_CurPos--;
pcCommand = *m_CurPos;
ASSERT(pcCommand);
pcCommand->Execute();
}
return pcCommand;
}
void Clear();
private:
typedef list<CCommand*> commands;
typedef commands::iterator commands_iter;
commands m_cCommandList;// 命令对象指针容器
commands_iter m_CurPos;// 当前命令对象迭代器
};
比如我们想支持文件名命名操作Undo/Redo的话,定义以下类即可
class CCommand_Rename
{
public:
CCommand_Rename (){}
virtual ~CCommand_Rename (){}
virtual void Execute()
{
_trename(m_newfilename, m_oldfilename)
}
virtual void UnExecute()
{
_trename(m_oldfilename, m_newfilename);
}
private:
tstring m_oldfilename, m_newfilename;
};
3. 使用智能指针来管理内存(代理模式).
在程序中维护大量的指针,是非常头疼的问题。我们经常会忘记释放某些已分配内存的指针,特别是在一些复杂逻辑和很多出口的函数中,从而造成一些内存漏洞。而且有的地方我们为了保证释放内存,不得不写很多冗余的代码或者借助于一些不良的语法。
我们来看如下逻辑:
1) 先对指针A分配内存,A分配内存如果成功,那么对指针B分配内存,如果失败返回.
2) 如果B分配失败那么释放指针A返回,如果成功。那么对指针C分配内存.
3) 如果C分配失败那么释放指针A,B返回。如果成功对指针D分配内存.
4) 如果D分配失败那么释放指针A,B,C返回
…
一般的代码可能是这样
CA* A = new CA;
If (!A) return;
A->f1()
CB* B = new CB;
If (!B)
{
delete A;
return 0;
}
B-> f2();
CC* C = new CC;
If (!C)
{
delete A;
delete B;
return 0;
}
C-> f3();
CD* D = new CD;
If (!D)
{
delete A;
delete B;
delete C;
return 0;
}
…
这样的代码看上去非常的罗嗦,增加了代码阅读的困难,而且很容易遗漏一些内存释放的语句。
而有的程序员为了避免啰嗦,不得不借助于“goto”语句。
CA* A = NULL;
CB* B = NULL;
CC* C = NULL;
CD* D = NULL;
A = new CA;
if (!A) return 0;
A-> f1();
B = new CB;
if (!B)
{
goto CLEANUP;
}
B-> f2();
C = new CC;
if (!C)
{
goto CLEANUP;
}
C-> f3();
D = new CD;
if (!D)
{
goto CLEANUP;
}
…
CLEANUP:
if(A) delete A;
if(B) delete B;
if(C) delete C;
return 0;
但是goto语句早就已经是臭名昭著,其危害性很多文章里都有详细的描述,因此应该尽量避免,我们这里可以利用智能指针来避免这种尴尬。
auto_ptr<CA> A(new CA);
if (!&A) return 0;
A->f1();
auto_ptr<CB> B(new CB);
if (!&B) return 0;
B->f2();
auto_ptr<CC> C(new CC);
if (!&C) return 0;
C->f3();
auto_ptr<CD> D(new CD);
if (!&D) return 0;
…
这样代码简洁得多,而且由于它是在函数退出时,弹栈调用析构函数对所代理的指针进行删除,所以不会存在任何内存漏洞。
单件模式结构图:
4. 使用单件模式来管理资源
作为一个GUI程序,FileBrowser自然会和很多资源打交道,
目前这部分逻辑实现的不是很理想。有的地方通过全局变量有的是通过全局函数来访问资源.感觉上程序风格不是很统一,我们应该尽量避免使用全局变量。另外调用全局函数则每次都要加载释放资源动态库,比较的冗余低效。因为资源这块对于模块来讲只有唯一一个实例,而且可以被整个系统调用。所以可以使用单件模式来管理它。
定义一个资源管理管理类,保证其只有一个实例,模块中所有资源调用都是通过调用它的接口,结构图如下:
大致代码如下:
class CResManager
{
protected:
CResManager (){ LoadResouce(); }
~ CResManager (){FreeResouce (); }
public:
static CResManager& Instance()
{
static CResManager instance;
return instance;
}
void FsLoadString(UINT idString, LPTSTR lptsBuffer, DWORD dwLen);
void FsLoadBitMap(…);
void FsLoadMenu(HMENU* phMenu, UINT idMenu);
…
private:
void LoadResouce(){…}
void FreeResouce(){…}
private:
HINSTANCE m_hRes;
}
这样用户想读取一个字符串的话,调用以下语句即可
CResManager::Instance().FsLoadString(…);
整个模块的资源调用就显得统一简洁高效。
5. 使用备忘录模式来实现本地/远程目录的“前进/后退”功能
Windows自带的浏览器支持前进/后退的功能,用户可以来回浏览刚才访问过的路径,而不需要记住并敲入路径名,很是方便。其实利用备忘录模式,可以很容易的在FileBrowser中加入这一功能。
备忘录模式的机制在于定义一个对象状态的结构,当对对象进行操作之前,保存对象状态,撤销操作时根据保存的对象状态恢复此对象。这个描述看上去很像命令模式,不过命令模式保存的是对对象的操作而不是对象状态。
由于我们只要保存的文件夹路径这一单一信息,所以直接可以设置一个字符串链表即可,每次用户浏览新的路径,将当前路径名字符串保存到路径链表,并设置当前路径迭代器指向此字符串。用户前进/后退时,设置浏览器当前路径为路径迭代器所指向路径,并对路径迭代器做相应的调整。
路径链表操作示意图:
当前路径迭代器 |
路径字符串链表 |
大致代码如下:
class CPathControl
{
public:
CPathControl(void) : m_bInitialize(false) {}
~CPathControl(void){}
void CPathControl::Initialize(const tstring& path)
{
ASSERT(m_cPathList.empty());
m_cPathList.push_back(path);
m_CurPos = m_ PathList.begin();
m_bInitialize = true;
}
void AddNewPath(const tstring& path)
{
ASSERT(m_bInitialize);
m_CurPos = m_ cPathList.insert(++m_CurPos, path);
}
void GetCurPath(tstring& path)
{
ASSERT(m_bInitialize);
if (m_cPathList.size() > 0 && m_CurPos != m_cPathList.end())
(*m_CurPos).swap(path);.
}
BOOL CanMoveLast(void)
{
return (m_cPathList.size() > 0 && m_CurPos != m_cPathList.begin());
}
BOOL CanMoveNext(void)
{
list<st_viewinfo>::iterator iter = m_CurPos;
return (m_cPathList.size() > 0 && ++iter != m_cPathList.end());
}
void LastView(void)
{
if (!CanMoveLast())
m_CurPos--;
}
void NextView(void)
{
if (CanMoveNext())
m_CurPos++;
}
public:
list<tstring> m_cPathList;
list<tstring>::iterator m_CurPos;
private:
bool m_bInitialize;
};
class CFsSpDoc
{
…
Public:
void SetNewPath(const tstring& path)
{
m_cPathControl. AddNewPath(path);
BrowsePath(path);//浏览该路径
}
void Forward()
{
m_cPathControl.NextView();
tstring path(_T(“”));
m_cPathControl.GetPath(path);
BrowsePath(path); //浏览该路径
}
void Backward()
{
m_cPathControl.LastView();
tstring path(_T(“”));
m_cPathControl.GetPath(path);
BrowsePath(path);
}
…
private:
CPathControl m_cPathControl;
}
相关文章:
设计模式在FileBrowser中的几个应用
设计模式是代码重构的最终目标,在程序设计中有效的运用这项技术,可以大大提高代码的可读性和可维护性。使整个程序设计结构趋向精致完美。在我维护的FileBrowser模块中可以针对以下方面 应用相应的模式。 1. 使用策略模式来处理文件夹扫描操作 作为网…...

【JavaEE进阶】——Spring AOP
目录 🚩Spring AOP概述 🚩Spring AOP快速⼊⻔ 🎓引入AOP依赖 🎓编写AOP程序 🚩Spring AOP 详解 🎓Spring AOP核⼼概念 🎓通知类型 🎓PointCut 🎓切⾯优先级 Ord…...
Python - conda使用大全
如何使用Conda? 环境 创建环境 conda create -n spider_env python3.10.11查看环境 conda env listconda info -e激活环境 conda activate spider_env退出环境 conda deactivate删除环境 conda env remove -n spider_env包 导出包 说明:导出当前虚拟…...
ASPICE在汽车软件开发中的作用
ASPICE是一个专门为汽车软件开发过程而设计的评估和改进框架。它基于ISO/IEC 15504标准,为汽车供应商提供了一个评估和改进其软件开发流程的方法。ASPICE的目标是确保软件开发过程的一致性和可预测性,从而提高软件的质量和可靠性。 ASPICE的实施对汽车软…...

亚马逊云科技 re:Inforce 2024中国站大会
亚马逊云科技 re:Inforce 2024中国站大会 - 生成式AI时代的全面安全,将于7月25日本周四在北京富力万丽酒店揭幕...
Lottie:动态动画的魔法棒
文章目录 引言官网链接Lottie 的原理基础使用1. 导出动画2. 引入 Lottie 库3. 加载和播放动画 高级使用1. 动画控制2. 交互性3. 自定义动画例子:交互式按钮动画 优缺点优点缺点 结语 引言 Lottie 是 Airbnb 开源的一个动画库,它允许设计师在 Adobe Afte…...
IPython使用技巧整理
IPython 是一个增强的 Python 交互式 shell,它提供了许多便利的功能,比如自动补全、魔术命令、对象内省等。以下是 IPython 的一些使用技巧和示例,结合您提供的列表数据,我将给出一些相关的使用示例。 1. 自动补全(Tab…...

C#数组复习
一、一维数组 using System.Collections; using System.Collections.Generic; using UnityEngine;public class ShuZu : MonoBehaviour {#region 知识点一 基本概念//数组是存储一组相同类型数据的集合//数组分为 一维、二维、交错数组//一般情况 一维数组 就简称为数组#en…...
无人机之在农业上的用途
随着无人机技术的发展,农业现代化也迎来了崭新局面,田间随处可见无人机矫健的身影。当农业遇上科技,变革正悄然进行。农业无人机主要应用于农业、种植业、林业等行业。在使用过程中,其功能和作用并不单一,一般用于种植…...
opengaussdb在oepnEuler上安装
安装前提: 软件环境:openEuler 20.03LTS 个人开发者最低配置2核4G,推荐配置4核8G 数据库版本:openGauss-5.0.2-openEuler-64bit-all.tar.gz 数据库下载地址: https://docs-opengauss.osinfra.cn/zh/docs/5.0.0/docs/In…...

一些和颜色相关网站
1.中国传统色 2.网页颜色选择器 3.渐变色网站 4.多风格色卡生成 5.波浪生成 6.半透明磨砂框 7.色卡组合...
Linux系统编程-文件系统
目录 什么是Linux文件系统 文件系统的职责 存储介质抽象 inode:文件系统的核心 文件分配策略 目录结构 文件系统布局 日志和恢复机制 目录权限 粘滞位(t位): 硬链接和符号链接 硬链接的特点: 创建硬链接: 符号链接的…...

【解决】ubuntu20.04 root用户无法SSH登陆问题
Ubuntu root用户无法登录的问题通常可以通过修改SSH配置文件和系统登录配置来解决。 修改SSH配置文件 sudo vim /etc/ssh/sshd_config 找到 PermitRootLogin 设置,并将其值更改为 yes 以允许root用户通过SSH登录 保存并关闭文件之后,需要重启SSH服务…...

(前缀和) LeetCode 238. 除自身以外数组的乘积
一. 题目描述 原题链接 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&…...

【JVM基础05】——组成-能不能解释一下方法区?
目录 1- 引言:方法区概述1-1 方法区是什么?(What)1-2 为什么用方法区?方法区的作用 (Why) 2- ⭐核心:详解方法区(How)2-1 能不能解释一下方法区?2-2 元空间内存溢出问题2-3 什么是常量池?2-4 运行时常量池 …...

前端:Vue学习-3
前端:Vue学习-3 1. 自定义指令2. 插槽2.1 插槽 - 后备内容(默认值)2.2 插槽 - 具名插槽2.3 插槽 - 作用域插槽 3. Vue - 路由3.1 路由模块封装3.2 声明式导航 router-link 高亮3.3 自定义匹配的类名3.4 声明式导肮 - 跳转传参3.5 Vue路由 - 重…...

npm 安装报错(已解决)+ 运行 “wue-cli-service”不是内部或外部命令,也不是可运行的程序(已解决)
首先先说一下我这个项目是3年前的一个项目了,中间也是经过了多个人的修改惨咋了布置多少个人的思想,这这道我手里直接npm都安装不上,在网上也查询了多种方法,终于是找到问题所在了 问题1: 先是npm i 报错在下面图片&…...

江苏科技大学24计算机考研数据速览,有专硕复试线大幅下降67分!
江苏科技大学(Jiangsu University of Science and Technology),坐落在江苏省镇江市,是江苏省重点建设高校,江苏省人民政府与中国船舶集团有限公司共建高校,国家国防科技工业局与江苏省人民政府共建高校 &am…...

20分钟上手新版Skywalking 9.x APM监控系统
Skywalking https://skywalking.apache.org/ Skywalking是专为微服务、云原生和基于容器的(Kubernetes)架构设计的分布式系统性能监控工具。 Skywalking关键特性 ● 分布式跟踪 ○ 端到端分布式跟踪。服务拓扑分析、以服务为中心的可观察性和API仪表板。…...

【07】LLaMA-Factory微调大模型——微调模型导出与微调参数分析
上文介绍了如何对微调后的模型进行使用与简单评估。本文将介绍对微调后的模型进行导出的过程。 一、llama-3微调后的模型导出 首先进入虚拟环境,打开LLaMA-Factory的webui页面 conda activate GLM cd LLaMA-Factory llamafactory-cli webui 之后,选择…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...

docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...

2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...

高考志愿填报管理系统---开发介绍
高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发,采用现代化的Web技术,为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## 📋 系统概述 ### 🎯 系统定…...

Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...