MFC之RTTI与动态创建
本人能力、精力有限,所言所感都基于自身的实践和有限的阅读、查阅,如有错误,欢迎拍砖,敬请赐教——博客园:钱智慧。
在说RTTI之前需要明白c++中类静态成员的初始化特点:类的静态数据成员需要在类体外显式初始化(const 类型的静态数据成员才能在类体内直接初始化)。关于数据(全局变量,局部变量,静态变量,类数据成员)的初始化特点可参考《C++primer》相关章节。
还要知道,类的静态成员的初始化发生于main之前,MFC的RTTI机制正是利用了这一特点,从而在进入main函数之前提前把一个信息链表建立起来,而这个链表每个节点便是一个CruntimeClass对象。那么,在main之前就得把链表构建好,这意味着在main之前就得执行代码,试问:什么代码能在main之前得以执行呢?这便是全局对象或者静态对象的构造函数中的代码。比如,有个全局对象Person p;那么,p的造函数会在main执行之前执行,从而我们可以把一些提前完成的任务放在Person的构造函数里,Person本身甚至完全不重要,我们只是想利用它的构造函数来完成自己想要的功能而已,MFC的RTTI也利用了这一点,比如其中的AFX_CLASSINIT类(结构体),它存在的唯一目的就是为了利用其构造函数往链表中添加节点,再比如接下来的模拟例子中的AFX_ClassNode类也是如此。下面的例子仅仅是模拟了RTTI中构建链表的过程,并不是完整的RTTI机制模拟,没有候捷的模拟复杂,所以更容易看懂,代码如下:
- #include <iostream>
- //链表节点类
- struct ClassNode
- {
- ClassNode* next;
- //注意,节点类有个静态指针,因为一个链表只需要有一个首指针
- static ClassNode* first;//first本身是Node类的静态数据成员,需要在类体外初始化
- };
- ClassNode* ClassNode::first=;
- //继承体系
- /* Fruit
- |
- -----------
- | |
- Orange Apple
- |
- BigApple
- */
- struct AFX_LISTINIT//这个类的存在只是为了利用其构造函数来给链表增加节点
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("添加一个节点构建链表\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- public:
- static ClassNode node;
- };
- //下面两行代码把Fruit的静态数据成员node节点加入链表(此时链表一个节点都没有)
- ClassNode Fruit::node={};//Fruit类的指针初始化为0,就是null
- AFX_LISTINIT initFruit(&Fruit::node);
- //Orange
- class Orange:public Fruit
- {
- public:
- static ClassNode node;
- };
- ClassNode Orange::node={};//Node是结构体,所以可以这样初始化
- AFX_LISTINIT initOrange(&Orange::node);
- //Apple
- class Apple:public Fruit
- {
- public:
- static ClassNode node;
- };
- ClassNode Apple::node={};
- AFX_LISTINIT initApple(&Apple::node);
- //BigApple
- class BigApple:public Apple
- {
- public:
- static ClassNode node;
- };
- ClassNode BigApple::node={};
- AFX_LISTINIT initBigApple(&BigApple::node);
- int main()
- {
- return ;
- }
最终可以看到有四句打印语句。
如同MFC中RTTI中的链表一样,本例的链表也是倒着添加节点的,即先添加的节点将会在链表靠后的位置。
引入宏定义:
我们的模拟很简陋,仅仅是模拟出了创建链表的机制,但是这个链表是粗糙的,每个节点几乎没有保存跟特定类相关的信息,比如类名,比如指向基类节点的指针,比如用来创建类实例的函数指针等。而且我们每定义一个新的类,如果想加入链表都需要两句类似的代码:初始化节点,构建一个AFX对象以添加节点,这无疑体力和键盘的消磨,真正的RTTI中用宏避免了手动敲击重复代码,宏并不神秘,代码替换而已。我们一步步完善这个模拟。我们的链表中节点类型是ClassNode,MFC中是CRuntmeClass类型(结构体),而且MFC中每个类的静态数据成员CRuntimeClass对象有不同的名称,格式是:class+类名,比如classOrange,而我们的类中的节点变量名称都叫node,我们就来改进这一点,让每个的节点变量名具备class+类名的格式,利用C语言一种特殊的预处理器语法 a##b得到ab,注意,a和b可以是任意类型的变量名称,通过这种方式可以得到一种新的变量名ab,从而我们的改进方式是:直接将使用node变量名的地方换成class##Fruit,class##Apple,class##Orange……然而这是不行的,因为##语法是预处理器负责的,VC6.0给的错误是:preprocessor command must start as first nonwhite space。这就是在逼着我们用宏定义了,最终改善的代码:
- #include <iostream>
- #define DECLARE_DYNAMIC(class_name) \
- public: \
- static ClassNode class##class_name;
- #define IMPLEMENT_DYNAMIC(class_name) \
- ClassNode class_name::class##class_name={}; \
- AFX_LISTINIT init##class_name(&class_name::class##class_name);
- //链表节点类
- struct ClassNode
- {
- ClassNode* next;
- static ClassNode* first;
- };
- ClassNode* ClassNode::first=;
- struct AFX_LISTINIT
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("构建链表:添加一个节点\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- DECLARE_DYNAMIC(Fruit)
- };
- IMPLEMENT_DYNAMIC(Fruit);
- //Orange
- class Orange:public Fruit
- {
- DECLARE_DYNAMIC(Orange)
- };
- IMPLEMENT_DYNAMIC(Orange)
- //Apple
- class Apple:public Fruit
- {
- DECLARE_DYNAMIC(Apple)
- };
- IMPLEMENT_DYNAMIC(Apple)
- //BigApple
- class BigApple:public Apple
- {
- DECLARE_DYNAMIC(BigApple)
- };
- IMPLEMENT_DYNAMIC(BigApple)
- int main()
- {
- return ;
- }
引入类名:
- #include <iostream>
- //注意宏定义中的#和##语法,前者可把变量名转成字符串
- #define DECLARE_DYNAMIC(class_name) \
- public: \
- static ClassNode class##class_name;
- #define IMPLEMENT_DYNAMIC(class_name) \
- char _sz##class_name[]=#class_name; \
- ClassNode class_name::class##class_name={_sz##class_name,}; \
- AFX_LISTINIT init##class_name(&class_name::class##class_name);
- //链表节点类
- struct ClassNode
- {
- char* m_szClassName;
- ClassNode* next;
- static ClassNode* first;
- };
- ClassNode* ClassNode::first=;
- struct AFX_LISTINIT
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("构建链表:添加一个节点:");
- printf(pNode->m_szClassName);
- printf("\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- DECLARE_DYNAMIC(Fruit)
- };
- IMPLEMENT_DYNAMIC(Fruit);
- //Orange
- class Orange:public Fruit
- {
- DECLARE_DYNAMIC(Orange)
- };
- IMPLEMENT_DYNAMIC(Orange)
- //Apple
- class Apple:public Fruit
- {
- DECLARE_DYNAMIC(Apple)
- };
- IMPLEMENT_DYNAMIC(Apple)
- //BigApple
- class BigApple:public Apple
- {
- DECLARE_DYNAMIC(BigApple)
- };
- IMPLEMENT_DYNAMIC(BigApple)
- int main()
- {
- return ;
- }
引入继承关系:
引入继承关系,便是在节点类中增加一个指向父类结点的指针,问题是如何初始这个指针呢,这要求我们必须提供获得某个类的节点指针的方法,从而我们需要定义RUNTIME_CLASS宏,它接收类名,返回类的节点指针。还要注意,终极父类Fruit并没有父类,所以不能像其他类那样对其直接使用实现宏,而是要特殊处理,即把其父类置空(毕竟RUNTIME_CLASS宏不能接收NULL),事实上RUNTIME_CLASS宏与即将实现的GetRuntimClass成员函数功能是一样的,只不过一个是宏,一个是成员函数,只能通过类对象调用。最终代码如下:
- #include <iostream>
- //注意宏定义中的#和##语法,前者可把变量名转成字符串
- #define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
- #define DECLARE_DYNAMIC(class_name) \
- public: \
- static ClassNode class##class_name; \
- ClassNode* GetRuntimeClass();
- #define IMPLEMENT_DYNAMIC(class_name,base_class_name) \
- char _sz##class_name[]=#class_name; \
- ClassNode class_name::class##class_name={RUNTIME_CLASS(base_class_name),_sz##class_name,}; \
- AFX_LISTINIT init##class_name(&class_name::class##class_name); \
- ClassNode* class_name::GetRuntimeClass(){return &class_name::class##class_name;}
- //链表节点类
- struct ClassNode
- {
- ClassNode* m_baseClass;
- char* m_szClassName;
- ClassNode* next;
- static ClassNode* first;
- };
- ClassNode* ClassNode::first=;
- struct AFX_LISTINIT
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("构建链表:添加一个节点:");
- printf(pNode->m_szClassName);
- printf("\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- DECLARE_DYNAMIC(Fruit)
- };
- //IMPLEMENT_DYNAMIC(Fruit);特殊处理最终父类,MFC中便是上帝CObject
- char _szFruit[]="Fruit";
- ClassNode Fruit::classFruit={/*RUNTIME_CLASS(base_class_name)*/NULL,_szFruit,};
- AFX_LISTINIT initFruit(&Fruit::classFruit);
- //Orange
- class Orange:public Fruit
- {
- DECLARE_DYNAMIC(Orange)
- };
- IMPLEMENT_DYNAMIC(Orange,Fruit)
- //Apple
- class Apple:public Fruit
- {
- DECLARE_DYNAMIC(Apple)
- };
- IMPLEMENT_DYNAMIC(Apple,Fruit)
- //BigApple
- class BigApple:public Apple
- {
- DECLARE_DYNAMIC(BigApple)
- };
- IMPLEMENT_DYNAMIC(BigApple,Apple)
- int main()
- {
- printf("***********************\n");
- //**测试链表中基类信息
- //打印出各个类及父类名称
- for(ClassNode* cursor=ClassNode::first;cursor!=NULL;)
- {
- ClassNode * baseClass=cursor->m_baseClass;
- if(baseClass!=NULL)
- {
- printf("%s : %s\n",cursor->m_szClassName,baseClass->m_szClassName);
- }
- else
- {
- printf(cursor->m_szClassName);
- printf("\n");
- }
- cursor=cursor->next;
- }
- printf("***********************\n");
- //**用GetRumtimeClass方法测试
- //找出ba所属类型的所有祖宗
- BigApple ba;
- for(ClassNode* findBase=ba.GetRuntimeClass()->m_baseClass;findBase!=NULL;)
- {
- printf(findBase->m_szClassName);
- printf("\n");
- findBase=findBase->m_baseClass;
- }
- return ;
- }
引入类型识别函数IsKindOf函数:
把IsKindOf定义在根父类Fruit中,这样后代类都能继承到。IsKindOf是一个普通的成员函数,其参数是一个ClassNode指针(MFC中则是CRuntimeClass指针),它的思想很简单:先用本类的ClassNode指针与参数比较,若不相等再用本类的父类的ClassNode指针去比较……一旦相等,便返回真。比较两指针的值是完全没问题的,我们知道,若两指针的值相等,便能说明二者指向同一个对象,此处若两指针相等则说明二者指向同一个ClassNode对象。
尤其要注意的是:之前我们的GetRuntimeClass对象不是虚函数,但到这一步,它必须得是虚函数,因为IsKindOf调用了GetRuntimeClass函数,看代码:
BigApple ba;
printf("%d\n",ba.isKindOf(RUNTIME_CLASS(Apple)));
若没用虚函数,则执行到isKindOf中时,不能做到动态绑定而调用ba自己的GetRuntimeClass函数,从而出现逻辑错误。完整代码:
- #include <iostream>
- //注意宏定义中的#和##语法,前者可把变量名转成字符串
- #define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
- #define DECLARE_DYNAMIC(class_name) \
- public: \
- static ClassNode class##class_name; \
- virtual ClassNode* GetRuntimeClass();//这里必须是虚函数!!
- #define IMPLEMENT_DYNAMIC(class_name,base_class_name) \
- char _sz##class_name[]=#class_name; \
- ClassNode class_name::class##class_name={RUNTIME_CLASS(base_class_name),_sz##class_name,}; \
- AFX_LISTINIT init##class_name(&class_name::class##class_name); \
- ClassNode* class_name::GetRuntimeClass(){return &class_name::class##class_name;}
- //链表节点类
- struct ClassNode
- {
- ClassNode* m_baseClass;
- char* m_szClassName;
- ClassNode* next;
- static ClassNode* first;
- };
- ClassNode* ClassNode::first=;
- struct AFX_LISTINIT
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("构建链表:添加一个节点:");
- printf(pNode->m_szClassName);
- printf("\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- public:
- bool isKindOf(ClassNode* pNode);
- DECLARE_DYNAMIC(Fruit)
- };
- //IMPLEMENT_DYNAMIC(Fruit);特殊处理最终父类,MFC中便是上帝CObject
- char _szFruit[]="Fruit";
- ClassNode Fruit::classFruit={/*RUNTIME_CLASS(base_class_name)*/NULL,_szFruit,};
- AFX_LISTINIT initFruit(&Fruit::classFruit);
- ClassNode* Fruit::GetRuntimeClass(){return &Fruit::classFruit;}
- bool Fruit::isKindOf(ClassNode* pNode)
- {
- for(ClassNode* cursor=this->GetRuntimeClass();cursor!=NULL;)
- {
- if(cursor==pNode)
- {
- return true;
- }
- cursor=cursor->m_baseClass;
- }
- return false;
- }
- //Orange
- class Orange:public Fruit
- {
- DECLARE_DYNAMIC(Orange)
- };
- IMPLEMENT_DYNAMIC(Orange,Fruit)
- //Apple
- class Apple:public Fruit
- {
- DECLARE_DYNAMIC(Apple)
- };
- IMPLEMENT_DYNAMIC(Apple,Fruit)
- //BigApple
- class BigApple:public Apple
- {
- DECLARE_DYNAMIC(BigApple)
- };
- IMPLEMENT_DYNAMIC(BigApple,Apple)
- int main()
- {
- BigApple ba;
- //**测试isKindOf(此处最值得注意的地方便是Fruit类的GetRuntimeClass必须得是虚函数)
- printf("%d\n",ba.isKindOf(RUNTIME_CLASS(Apple)));
- printf("%d\n",ba.isKindOf(RUNTIME_CLASS(Orange)));
- return ;
- }
另外,本示例代码的全局变量不像候捷的示例代码那样让全局变量都为静态的,这并无大碍,但要明白全局的静态变量只能在本文件中使用,虽然其生命周期是整个程序的运行期,非静态的全局变量才能通过extern声明被外部文件使用,全局函数也是这样。
引入动态创建:
思想就是为每个类加入一个静态的createObject函数,为ClassNode节点加入一个函数指针成员,构建链表时中每个节点时,把指针指向每个类自己的createObject函数,从而将来有了相应类的节点指针(即CRuntimClass指针)便能调用相应的createObject方法。需要注意的是,我们也为ClassNode引入一个普通的成员方法CreateObject,这个方法是将来动态创建时真正要调用的方法,它的存在不过是让功能更优雅一些,其实没有它也可以,不过调用者需要自己判断ClassNode对象里的函数指针是不是为空,不为空才能调用该指针指向的类的静态createObject方法。至今,为了省事,我们的根级父类Fruit定义中一直还有动态声明宏,但这里要把它拿掉而用具体的代码了,因为我们不让Fruit具备动态创建功能,即不给其添加createObject方法,其节点中的m_pfnCreateObject指针也置空。另外为了测试方便,我们给根父类Fruit加入了虚方法sayHello,子类都覆写了这个方法,候捷的的动态创建示例代码中有Load方法,其实它完成的便是本例的main函数代码,候哥这么做是为了契合后续的持久化机制。完整代码:
- #include <iostream>
- //注意宏定义中的#和##语法,前者可把变量名转成字符串
- #define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
- #define DECLARE_DYNAMIC(class_name) \
- public: \
- static ClassNode class##class_name; \
- virtual ClassNode* GetRuntimeClass(); \
- static Fruit* createObject();
- #define IMPLEMENT_DYNAMIC(class_name,base_class_name) \
- char _sz##class_name[]=#class_name; \
- ClassNode class_name::class##class_name={class_name::createObject,\
- RUNTIME_CLASS(base_class_name),_sz##class_name,}; \
- AFX_LISTINIT init##class_name(&class_name::class##class_name); \
- ClassNode* class_name::GetRuntimeClass(){return &class_name::class##class_name;} \
- Fruit* class_name::createObject() {return new class_name;}
- //**链表节点类
- //提前用到了Fruit类,需要前向声明
- class Fruit;
- struct ClassNode
- {
- Fruit* CreateObject();
- Fruit* (*m_pfnCreateObject)();
- ClassNode* m_baseClass;
- char* m_szClassName;
- ClassNode* next;
- static ClassNode* first;
- };
- Fruit* ClassNode::CreateObject()
- {
- if(m_pfnCreateObject==NULL)
- {
- printf("这个类不支持动态创建\n");
- return NULL;
- }
- return m_pfnCreateObject();
- }
- ClassNode* ClassNode::first=;
- struct AFX_LISTINIT
- {
- AFX_LISTINIT(ClassNode* pNode)
- {
- printf("构建链表:添加一个节点:");
- printf(pNode->m_szClassName);
- printf("\n");
- pNode->next=ClassNode::first;
- ClassNode::first=pNode;
- }
- };
- //Fruit
- class Fruit
- {
- public:
- virtual void sayHello(){printf("hello Fruit\n");}
- bool isKindOf(ClassNode* pNode);
- static ClassNode classFruit;
- virtual ClassNode* GetRuntimeClass();
- };
- //IMPLEMENT_DYNAMIC(Fruit);特殊处理最终父类,MFC中便是上帝CObject
- char _szFruit[]="Fruit";
- ClassNode Fruit::classFruit={NULL,/*RUNTIME_CLASS(base_class_name)*/NULL,_szFruit,};
- AFX_LISTINIT initFruit(&Fruit::classFruit);
- ClassNode* Fruit::GetRuntimeClass(){return &Fruit::classFruit;}
- bool Fruit::isKindOf(ClassNode* pNode)
- {
- for(ClassNode* cursor=this->GetRuntimeClass();cursor!=NULL;)
- {
- if(cursor==pNode)
- {
- return true;
- }
- cursor=cursor->m_baseClass;
- }
- return false;
- }
- //Orange
- class Orange:public Fruit
- {
- public:
- void sayHello(){printf("hello Orange\n");}
- DECLARE_DYNAMIC(Orange)
- };
- IMPLEMENT_DYNAMIC(Orange,Fruit)
- //Apple
- class Apple:public Fruit
- {
- public:
- void sayHello(){printf("hello Apple\n");}
- DECLARE_DYNAMIC(Apple)
- };
- IMPLEMENT_DYNAMIC(Apple,Fruit)
- //BigApple
- class BigApple:public Apple
- {
- public:
- void sayHello(){printf("hello BigApple\n");}
- DECLARE_DYNAMIC(BigApple)
- };
- IMPLEMENT_DYNAMIC(BigApple,Apple)
- int main()
- {
- //**动态创建测试
- char className[];
- scanf("%s",&className);
- for(ClassNode* cursor=ClassNode::first;cursor!=NULL;)
- {
- if(strcmp(cursor->m_szClassName,className)==)
- {
- cursor->CreateObject()->sayHello();
- break;
- }
- cursor=cursor->next;
- }
- return ;
- }
到此,本示例算是完全结束了,为了简单省事,本示例对候哥的例子做了尽可能的简化。
MFC之RTTI与动态创建的更多相关文章
- 《深入浅出MFC》系列之动态创建
/*************************************************************************************************** ...
- MFC原理第四讲.动态创建机制
MFC原理第四讲.动态创建机制 一丶要学习的知识点以及简介 动态创建是什么意思? 动态创建其实就是跟C++的new一样.都是创建对象.但是规避了C++语法的缺陷. 例如: char * ClassNa ...
- MFC六大核心机制之三:动态创建
MFC中很多地方都使用了动态创建技术.动态创建就是在程序运行时创建指定类的对象.例如MFC的单文档程序中,文档模板类的对象就动态创建了框架窗口对象.文档对象和视图对象.动态创建技术对于希望了解MFC底 ...
- MFC之动态创建按钮
打开VS 创建MFC基于对话框的工程,在对话框初始化方法中动态创建一个按钮实例: 1> CButton *pMyButton = new CButton();CEdit *pMyEdit = n ...
- MFC动态创建对话框中的按钮控件并创建其响应消息
转自:http://www.cnblogs.com/huhu0013/p/4626686.html 动态按钮(多个)的创建: 1.在类中声明并定义按钮控件的ID #define IDC_D_BTN 1 ...
- 【转载】MFC动态创建控件及其消息响应函数
原文:http://blog.sina.com.cn/s/blog_4a08244901014ok1.html 这几天专门调研了一下MFC中如何动态创建控件及其消息响应函数. 参考帖子如下: (1)h ...
- MFC动态创建控件及其消息响应函数
这几天专门调研了一下MFC中如何动态创建控件及其消息响应函数. 参考帖子如下: (1)http://topic.csdn.net/u/20101204/13/5f1b1e70-2f1c-4205-ba ...
- MFC小程序003------MFC使用WebBrowser组件,在对话框中创建滚动视图,动态创建一个静态文本控件并设置鼠标单击的消息响应
MFC小程序截图: 一.在MFC中简单使用WebBrowser的ActiveX插件的方法: 见博文: http://blog.csdn.net/supermanking/article/detail ...
- MFC动态创建按钮,并在按钮上实现位图的切换显示
动态创建按钮,并在按钮中添加位图,通过单击按钮显示不同的位图,可设置为显示按钮按下和弹起两种状态.只要判断a值从而输入不同的响应代码. 1.在头文件中添加: CButton *pBtn; 2.在初始化 ...
随机推荐
- 基于Emgu CV 的手势识别实现PPT的控制放映
Emgu CV 简介 众所周知,Emgu CV是.NET平台下对OpenCV图像处理库的封装,也就是.NET版的OpenCV.开发者可以很方便的通过C#,VB等语言调用OpenCV函数 ...
- .net中的Array,ArrayList和List
Array:任意类型,定长 ArrayList:任意类型,不定长 List:特定类型,不定长 Array和ArrayList是通过存储object类型实现可以存储任意类型数据,使用时需要拆箱和装箱
- 用python实现k近邻算法
用python写程序真的好舒服. code: import numpy as np def read_data(filename): '''读取文本数据,格式:特征1 特征2 -- 类别''' f=o ...
- ASP.Net 添加 Interop for Word, excel 插件
1:在服务器上安装office的Excel软件. 2:在"开始"->"运行"中输入dcomcnfg.exe启动"组件服务" 3:依次双 ...
- noj [1482] 嘛~付钱吧!(完全背包)
http://ac.nbutoj.com/Problem/view.xhtml?id=1482 [1482] 嘛~付钱吧! 时间限制: 1000 ms 内存限制: 65535 K 问题描述 大白菜带着 ...
- new 的用法
在C#中,new关键字有三种用法: 1.new 运算符,用于创建对象和调用构造函数. 2.new 修饰符,在用作修饰符时,new关键字可以显式隐藏从基类继承的成员. 3.new 约束 ,用于在泛型 ...
- Seay工具分享
百度网盘:http://pan.baidu.com/share/home?uk=4045637737&view=share#category/type=0
- PHP之序列化与反序列化讲解
serialize() 把变量和它们的值编码成文本形式 unserialize() 恢复原先变量 eg: $stooges = array('Moe','Larry','Curly');$new = ...
- conky 配置变量表
转自conky 配置变量表 项目主页:http://conky.sourceforge.net/ 文档说明:http://conky.sourceforge.net/docs.html Variabl ...
- 当 ITOA 遇上 OneAlert,企业可以至少每年节省 3600 小时!
每个工作日,一家大型企业都可能存在一两件优先级为 1 级的事件,五六件优先级为 2 级的事件和百来件优先级为 3 级的事件.试想一下,如果公司所有支持人员都要收到每个事件的通知--不想了,我好方!还能 ...