一、函数参数
在实际的开发中,函数的应用是再普通不过的了。但对于大多数的开发者来说,对于函数参数的设计,并不会引起多大的重视,经常是想起来就增减没有什么规划性。对于基础类型来说,这样做也谈不上什么大问题,即使有效率损失一般也不会产生什么大的不良后果。但是,如果面对一些对象参数的传递或者数组的传递,就有可能引起很多不必要的麻烦。
二、函数参数的封装
不同的设计者在如何处理函数的参数的封装上,有着非常大的认知不同。有的设计者愿意使用对象本身,有的愿意使用指针而有的则愿意使用各种框架中的封装好的数据结构。其实正如兵法所云“兵无常势,水无常形”。参数的传递是一个灵活性非常强的应用过程,而基于此的封装也同样就变得灵活多样。单纯对于基础类型来说,没有什么技巧和手段可言,但到了对象封装的层次则有了更多的视角和方案。
函数的参数常见的封装状态为:
1、数组
这种其实可以说是勉强划到封装这一部分,其实可以理解为数组只是对数据层次的一种封装而不是面向对象编程中的封装,也就是本文提到的封装,流等方式也可以划到这种方式
2、对象
这种对象包括大家知道的面向对象中的封装的对象,也包括一些大家可能不当成回事的一些POD或者说新标准下的trivial数据类型
3、对象指针
这个就比较常见了,而且有各种情况,比如普通对象变量的指针、函数指针以及其它各种指针
4、STL数据结构
此处非常多如std::vector,std::list,std::map,std::set等等。包手新标准中的std::variant,std::tuple等等
5、函数本身
含各类函数的变形体如仿函数、std::functional等。这种应用比较特殊,算是对象封装的一种特殊应用
6、其它
一些可能不常见或者大家有不同看法的封装状态,如变参
三、具体的选择
那么在实际的开发中如何选择参数的封装形态呢?需要考虑以下几个问题:
1、简单性
这个是开发中为大家最重视的,简单即王道。比如是使用一个简单的重载还是使用模板来实现不同对象的处理
2、容易性
不要把容易和简单混在一起,简单的实现未必容易,比如一个连续的if判断很容易实现,但使用函数映射简单但未必容易实现
3、性能
这个在某些场景下非常重要,比如一个大对象的传递,是采用指针方式还是引用方式。或者使用流进行转换,假如在不同的框架中进行传输呢?要考虑兼容性呢?凡此种种,都需要考虑
4、易扩展性
易扩展性更容易理解了,对于一个好的设计而言,好多功能易于实现但如果想留下一些扩展的冗余就考验设计和实现者了,比如上面刚刚提到的模板和重载的问题。另外重要的就是数据传输中是不是易于与其它程序进行交互数据,是使用json还是xml,如果这是一个强需求,就需要认真考虑
5、兼容性
其实兼容性更适合于在既有项目上的开发,或者说有明确的目的需求情况下的开发。这个其实就是既考虑内部需求也要考虑外部需求
6、安全性
安全性就是这种封装方法,要保证数据类型的确定性和字节对齐等的稳定性,而数据的安全性往往代表着内存的安全性,特别是对外的接口参数的对象封装更是如此
7、可维护性
可维护性这个就比较要求高了,它需要处理当前的实际场景,既包括技术人员的技术、开发思想等也包括成本甚至社会工程等
在实际的项目开发中,往往要对上述的几个方面进行一个平衡取舍而不是简单的认为哪个更有优势。比如在实际的开发中,一个很简单的模块,不涉及其它的应用,那么就可以重点考虑容易性而对其它方面要求进行适当降低。另外一种情况可能易于实现的方法可能会导致安全性的降低,最常见的就是线程函数的参数是void的封装,而void这种指针类型的转换安全性开发者应该都知晓。
所以在实际的开发中,不能拘泥于哪种情况好哪种情况不好,同样的一种方式,可能在不同的需求下就完全变了样。要根据实际情况,来确定哪种情况更适合于参数的传递,这才是最终的目的。
四、实例分析
看一个线程常看到的线程函数参数封装:
#include <stdio.h>
#include <pthread.h>
struct ThreadArgs{
int d = 0;
double step = 1.0;
};
// 参数void*
void* threadFunc(void* arg) {
ThreadArgs* v = (ThreadArgs*)arg;
printf("parms value: %d\n", v);
return NULL;
}
int main() {
pthread_t tid;
ThreadArgs arg;
// 创建线程
int ret = pthread_create(
&tid,
NULL,
threadFunc,
&arg //指针封装
);
if (ret != 0) {
perror("create thread err!");
return 1;
}
pthread_join(tid, NULL);
printf("main thread quit.\n");
return 0;
}
要想安全传封装参数,可以使用STL中的std::thread的参数传递或者直接使用Lambada表达式,参数传递就安全方便了。看下面的例子:
#include <thread>
#include <iostream>
struct ThreadArgs
{
int d = 0;
double step = 1.0;
};
int main() {
ThreadArgs value ;
// 通过捕获列表传递参数
std::thread t([value]() {
std::cout << "get value: " << value.d << std::endl;
});
t.join();
return 0;
}
再看一个使用STL的容器的例子:
bool SetData(const std::vector<int> &v){
for (auto &d:v){
std::cout<<"vector d:"<<d<<std::endl;
}
return true;
}
int main(){
std::vector<int> vec{1,2,3,4,5,6};
SetData(vec);
return 0;
}
使用模板和重载的例子:
#include <iostream>
struct ThreadArgs{
int d = 0;
double step = 1.0;
};
void print(ThreadArgs arg) {
std::cout << "step is: " << arg.step << std::endl;
}
void print(double d) {
std::cout << " print value: " << d << std::endl;
}
template <typename T>
void print(T value) {
std::cout << "template: " << value << std::endl;
}
int main() {
ThreadArgs args;
print(args);
print(1.16);
print("test");
return 0;
}
再看一个函数指针的例子:
template <typename R, typename T, typename... Args> using Fn = std::function<std::optional<R>(T t, Args... args)>;
using CBSend = Fn<int32_t, const int32_t &, const int32_t &, const std::string &>;
bool init(CBSend f, std::function<int()> fs) {
this->m_cbFuncData = f;
this->m_cbFuncCmd = fs;
}
都是简单例子,大家一看就明白了。
五、总结
学习都是从简单开始,然后到一般再到复杂。但大家可能都知道,大道至简。其实,所有的东西都是如此。每种方法在不同的场景和不同的人眼中,可能都有不同的看法。所以,如何根据设计者本身和实际需求设计出最合适于当下场景的应用,这才是一个优秀的设计者的基本能力。