erlang是函数式编程语言,最初主要用在电信软件开发,他是面向并发编程的,和主流语言相比,主流语言并不能很好的利用多核CPU的资源,采取加锁的方式使得编程易出错,且锁也是耗资源的。学习erlang的过程中,发现erlang和主流语言的语法和思想差别很大,可能并不容易上手,但是作为一个程序员,越不容易才越有意思对不对?先从基本语法学起吧。
erlang是在虚拟机上运行的,需要安装erlang环境,Windows和Linux下都可以安装,教程网上很多,就不记录了。Windows下安装完成后,在命令行输入erl进入erlang shell,可以开始执行erlang语句。
1>每条语句执行需要以点号结尾;
2>变量必须以大写字母或者下划线开头;
3>变量赋值后不能更改,声明未赋值的变量称为自由变量;
4>"="等于符号表示模式匹配,而非赋值。
5>原子是已小写字母开头的字符,或者单引号括起来的字符,原子的值就是原子本身,函数名就是个原子;
6>erlang数据计算不会溢出,没有范围限制;
7>除法结果:
/ 永远返回浮点数
div 返回除的整数
2.3.1元组
元组类似于struct,一般以原子作为标识,P={point,1,2},元组可以嵌套。
1>提取元组中的数据
使用模式匹配符=,将Point变量的数据匹配到了Q和W两个变量
2>将元组转化为列表
将元组Point转化为有三个数据的一个列表。
3>访问元组中的指定数据
返回了元组Point中第三个数据。
4>在现有元组基础上改变其中某个数据得到一个新的元组
把Point元组的第三个数据改成了5,返回了新的元组{point,1,5}。
5>得到元组的数据个数
2.3.2 列表
列表存储数目可变的数据,用[]括起来,列表中元素类型可不同,列表的第一个元素叫列头,其他的部分叫列尾,用 | 分割列头和列尾,行为类似于栈。
1>提取列表元素
使用模式匹配和 | 进行提取,也可以通过这种方式向list中插入元素。
2> list几个简单操作
5> length(List). %%返回list数据个数
5
6> is_list(List). %%判断是否是列表
true
7> list_to_binary(List). %%返回转化为二进制后的list
<<1,2,3,4,5>>
8> list_to_bitstring(List). %%返回转化为bitstring的list
<<1,2,3,4,5>>
9> hd(List). %%取到列表头
1
10> tl(List). %%取到列表尾
[2,3,4,5]
3> lists库常用函数
lists:foreach(Fun, List). ->ok
lists:foreach(fun(X) -> io:format("~p",[X+1]) end, List).
23456ok
对于List这个列表中的每一个元素执行Fun函数,上面是把[1,2,3,4,5]列表中的元素加一输出出来。
lists:foldl(Fun,Acc0,List) ->Acc1
lists:foldl(fun(X,Sum) -> X+ Sum end, 0, List).
15
遍历List中的元素执行fun函数,每次把结果再传给下一次的Sum,完成累加,最后将累加结果返回。
lists:flatten(DeepList) -> List
lists:flatten([[1],[1,2,3],[4,[5,6],7]] ).
[1,1,2,3,4,5,6,7]
将一个复杂的嵌套的llist,扁平化尾简单list。
lists:reverse(List1) -> List2
lists:reverse(List).
[5,4,3,2,1]
反转列表。
lists:member(Elem, List) -> boolean()
lists:member(5,List).
true
lists:member(6,List).
false
查找Elem是否在List中存在。
lists:merge(List1, List2) -> List3
List1和List2分别是一个列表,这个函数的功能是将这两个列表合并成一个列表。
lists:all(Pred, List) -> boolean()
如果List中的每个元素作为Pred函数的参数执行,结果都返回true,那么all函数返回true,否则返回false。
lists:keystore(Key, N, TupleList1, NewTuple) -> TupleList2
替换list中touple的N位置为Key的tuple,返回新的TupleList。没找不到将NewTuple附加到原有TupleList后面并返回。
lists:keystore(apple,1,[{pear,1,1},{banana,2,2},{apple,3,3},{apple,4,4}],{apple,5,5}).
[{pear,1,1},
{banana,2,2},
{apple,5,5},
{apple,4,4}]
lists:split(N, List1) -> {List2, List3}
将List1分成List2和List3
其中List2包括List1的前N个元素,List3包含剩余的。
lists:foldr(Fun, Acc0, List) -> Acc1
foldr这个函数和foldl用法相似,Fun执行时,遍历List的顺序从后往前。
lists:concat(List) -> string()
list转字符串
lists:concat([1,avc,'/',';',ww]).
"1avc/;ww"
lists:keysort(N, TupleList1) -> TupleList2
对TupleList1中的Tuple按照Touple的第N个元素进行排序,然后返回一个新的顺序的TupleList。
lists:ukeymerge(N, TupleList1, TupleList2) -> TupleList3
将TupleList1和TupleList2合并,合并的规则是按照元组的第N个元素,如果第N个元素有相同的,那么保留TupleList1中的,删除TupleList2中的。
lists:sublist(List1, Len) -> List2
返回从第一个元素到第Len个元素的列表,这个Len大于List1的长度时,返回全部。
其他lists库包含的函数:lists和其他erlang库
效率比较高的lists函数
lists函数有一些已经被优化成bif函数(erlang内建函数)。有以下几个:
lists:member/2, lists:reverse/2, lists:keymember/3, lists:keysearch/3, lists:keyfind/3
当list元素多时效率低的lists函数
1>lists:foldr/3
非尾递归实现,替换方案:lists:reverse/1后lists:foldl/3。
2>lists:append/2
实现为append(L1, L2) -> L1 ++ L2. 其中,L1 ++ L2会遍历 L1,如果一定要使用就把短的list放左边
3>lists:subtract/2
实现为subtract(L1, L2) -> L1 -- L2. 其中,--的复杂度和它的操作数的长度的乘积成正比
4>lists:flatten/1
这个是list扁平化函数,这个存在性能开销
lists:map/2, lists:flatmap/2, lists:zip/2, lists:delete/2, lists:sublist/2, lists:sublist/3, lists:takewhile/2, lists:concat/1
lists:flatten/1, lists:keydelete/3, lists:keystore/4, lists:zf/2, lists:mapfoldl/3, lists:mapfoldr/3, lists:foldr/3
4>列表推导式
列表推导式类似于数学中的集合,表达形式:[X *2| | X <- [1,2,3],X rem 2 =:= 0].
模块是有名字的文件,包含一组函数,把处理类似事情的函数放在同一个模块中,模块名和文件名必须一致。
1> 定义模块中可导出(可被其他模块访问)的函数:-export([Functionname/参数数量])
2>函数:FunctionName(Args)->Body. Body由一个或者多个用逗号分隔的erlang表达式组成,自动返回最后一个表达式的执行结果,无需return关键字
3>注释:只能单行,%开头
4>定义宏:和define类似,用来定义简短的函数和常量,eg:-define(HOUR,3600). 使用HOUR这个宏表示3600,编译前HOUR宏被替换成3600,函数宏:-define(sub(X,Y),X-Y). 宏调用:?sub(23,47);
-ifdef(DEBUGMODE).
-define(DEBUG(S),io:format("dbg: "++s)).
-else.
-define(DEBUG(S), ok).
-endif.
5>元数据:模块名:module_info(),查看模块的各项元数据
6>环形依赖:避免环形依赖,A依赖了B,B不应该再依赖A。
7>运行一个模块需要先编译,编译结束后的文件结尾.beam
其他语言里ifelse的表达在erlang里使用模式匹配,多个函数字句来实现,字句间用分号分隔,结束用实心点。
~号用来指示一个标记符,io:format函数格式化输出通过替换字符串中的标记符完成
卫语句:添加在函数头的语句,用于模式匹配,eg:old_enough(X) when X>= 16, X=<104 -> true;
if类似于卫语句,case...of的语法:
heh_fine() ->
if 1=:= 1-> works;
true -> always_does %%这是erlang if的else
end.
case...of
beach(X) ->
case X of
Pattern Guards -> ...
end.
erlang是 动态强类型,提供了一系列类型转换函数,命名typeA_to_typeB,eg:erlang:list_to_integer
提供了类型检测BIF,形如is_type,eg:is_binary/1
递归终止的条件是一个子函数,返回值而不是继续调用函数,函数式编程没有循环,只有递归。
尾递归:使用一个变量保存递归过程中的中间结果,到最后一个元素的时候直接返回结果,需要提供一个参数只有一个的子函数用于返回
一个函数的参数是其他函数,则这个函数是高阶函数,调用时的传参方式:fun module:funname/arity
匿名函数,语法:fun(Args1) ->
Experssion1,Exp2,...,ExpN;
(Args2)->
Experssion1,Exp2,...,ExpN
end
函数作用域:存放所有变量对应值的地方,函数中任何地方都能访问,包括函数内的匿名函数,但是匿名函数中的变量在其所在的外部函数中不能访问。匿名函数一直持有所继承的作用域
过滤器:提取共同部分,调用时传入谓词(筛选条件)
折叠:把某个操作依次作用于列表每个元素,最后把所有元素归约成一个单一值。折叠是普遍适用的。
定义一个机器人记录:-record(robot,{ name, type = sumething, hobbies, details=[]}). 创建记录实例:#robot{name="xxx",type=handmade,details=[]}. 记录是元组之上的语法糖,使用点号来访问记录中的值异常:
共享记录:-indlude("records.hrl"). 将记录定义在records.hrl文件
1、属性列表(形如[{key,value}]的元组列表),通过proplists模块处理属性列表,没有插入函数,一般只用来存储配置
2、有序字典:orddict模块,适合存储小于75个数据量的情况。
1、字典:dict模块,接口和有序字典一样
2、通用平衡树:gb_trees模块,保留了元素顺序,如果需要按顺序访问,合适,分为智能模式和简单模式。
集合是值唯一的一组元素,有四个集合处理模块:ordsets(有序集合,主要实现小集合,最慢最简单的集合),sets(接口和ordsets一样,适用于大一些的数据规模,擅长读密集型处理),gb_sets(非读操作更快,控制手段更多,分智能模式和简单模式),sofs(有序列表实现,可在集合族和有向图之间进行双向转换),
两个模块:digraph(实现了有向图的构造和修改),digraph_utils(实现了图的后序和前序遍历等)
queue模块,使用了两个列表(栈)实现的。
并发:有许多独立运行的actor,但并不要求它们同时运行
并行:多个actor同时运行
erlang采取的是基于异步消息的轻进程,在erlang虚拟机上,创建一个erlang进程需要300个字的内存空间,创建时间几微秒,每个核启动一个线程充当调度器。
erlang进程就是一个函数,启动一个新进程使用spawn(函数名),参数是一个函数,spawn返回进程标识符pid,pid可作为地址进行进程间通信。
发送消息,操作符"!"也称bang符号,!左边是一个pid,右边可以是任意erlang数据项,数据被发给左边pid的进程。消息按照接收顺序会被放进接收进程的邮箱中,flush()命令查看。
接收消息:receive表达式,收到消息后进程处理完就退出了,所以需要递归调用自己
1、定义进程状态:借助递归函数,进程的状态保存到递归函数的参数中
2、隐藏消息实现:使用函数来处理消息的接收和发送
3、超时处理(防止死锁)属于receive语句中的一部分,Delay单位是毫秒,超时后没有收到和Match模式匹配的消息,会执行after部分:
receive
Match -> Expression1
after Delay ->
Expersion2
end.
4.选择性接收:通过嵌套调用对接收到的消息进行优先级排序,但是如果无用的消息太多会导致性能下降
5、邮箱风险的解决:确保有匹配不到的消息的处理,打日志,方便bug调试
1、链接:是两个进程之间的特殊关系,一个进程意外死亡时,与之有链接关系的进程也会死亡,阻止错误蔓延,建立链接函数:link/1,参数是pid,建立当前进程和pid的链接,为防止进程在链接建立成功之前就死亡了,提供了spawn_link函数,把创建进程和建立链接封装成了一个原子操作
2、重启进程:系统进程可以检查是否有进程死亡并重启死亡进程,process_flag(trap_exit,true)实现erlang进程转系统进程。
3、监控器:特殊的链接,监控器是单向的,两个进程间可设置多个监控器,监控器可以叠加,每个监控器有自己的标识,可以单独的移除,创建监控器:erlang:monitor/2,第一个参数永远是原子process,第二个参数是进程pid。监控进程死活,创建进程同时监控进程:spawn_monitor
4、给进程命名,方便在进程死亡后重启:erlang:register(Name,Pid)。进程死亡则自动失去名字。
1、用erlang实现一个简单的RPN计算器,输入一个list,输出计算结果:
-module(calc).
-export([rpn/1]).
rpn(L) when is_list(L) ->
[Res] = lists:foldl(fun rpn/2, [], string:tokens(L," ")),
Res.
rpn("+", [N1,N2|S]) -> [N2+N1|S];
rpn("-", [N1,N2|S]) -> [N2-N1|S];
rpn("*", [N1,N2|S]) -> [N2*N1|S];
rpn("/", [N1,N2|S]) -> [N2/N1|S];
rpn(X, Stack) -> [read(X)|Stack].
read(N) ->
case string:to_float(N) of
{error,no_float} -> list_to_integer(N);
{F,_} ->F
end.
2、erlang实现的事件提醒器
分为两部分完成,事件服务器和事件,当客户端向事件服务器请求新增一个事件,服务器创建事件进程,事件进程在事件时间到了的时候通知事件服务器,事件服务器转发给客户端。
要注意的几个点:
事件服务器要监控订阅的客户端,已经挂掉的客户端不需要再关注
客户端可以取消事件,所以事件服务器需要可以杀掉事件进程
erlang超时值最大只能是50天,为了支持能够设置超过50天的事件,需要自己处理一下时间。
事件代码:
-module(event).
-compile(export_all).
-record(state, {server, name ="", to_go=0}).
start(EventName, Delay) ->
spawn(?MODULE, init, [self(), EventName, Delay]).
start_link(EventName, Delay) ->
spawn_link(?MODULE, init, [self(),EventName,Delay]).
loop(S = #state{server=Server,to_go=[T|Next]}) ->
receive
{Server, Ref, cancel} ->
Server ! {Ref, ok}
after T*1000 ->
if Next =:= [] ->
Server ! {done, S#state.name};
Next =/= [] ->
loop(S#state{to_go=Next})
end
end.
init(Server, EventName, DateTime) ->
loop(#state{server=Server, name=EventName, to_go=normalize(DateTime)}).
cancel(Pid) ->
Ref = erlang:monitor(process, Pid),
Pid ! {self(), Ref, cancel},
receive
{Ref, ok} ->
erlang:demonitor(Ref, [flush]),
ok;
{'DOWN', Ref, process, Pid, _Reason} ->
ok
end.
normalize(N) ->
Limit = 49 * 24 * 60 * 60,
[N rem Limit | lists:duplicate(N div Limit, Limit)].
time_to_go(TimeOut={
{_,_,_},{_,_,_}}) ->
Now = calendar:local_time(),
ToGo = calendar:datatime_to_gregorian_seconds(TimeOut) -
calendar:datatime_to_gregorian_seconds(Now),
Secs = if ToGo > 0 -> ToGo;
ToGo =< 0 -> 0
end,
normalize(Secs).
事件服务器代码:
-module(evserv).
-compile(export_all).
-record(state, {events, clients}). %%记录事件和pid
-record(event, {name="", description="", pid, timeout={
{1970,1,1},{0,0,0}}}).
start() ->
register(?MODULE, Pid=spawn(?MODULE, init,[])),
Pid.
start_link() ->
register(?MODULE, Pid = spawn_link(?MODULE,init,[])),
Pid.
terminate() ->
?MODULE ! shutdown.
init() ->
loop(#state{events = orddict:new(), clients = orddict:new()}).
loop(S = #state{}) ->
receive
{Pid, MsgRef, {subscribe, Client}} -> %%订阅事件
Ref = erlang:monitor(process, Client), %%监控订阅的客户端
NewClients = orddict:store(Ref, Client, S#state.clients),%%使用有序字典定义客户端
Pid ! {MsgRef, ok},
loop(S#state{clients=NewClients});
{Pid, MsgRef, {add, Name, Description, TimeOut}} -> %%新增事件
case valid_datetime(TimeOut) of
true ->
EventPid = event:start_link(Name, TimeOut),
NewEvents = orddict:store(Name, #event{name=Name, description=Description,pid=EventPid,timeout=TimeOut},
S#state.events),
Pid ! {MsgRef, ok}, %%MsgRef是标志这是这条消息对应的返回消息
loop(S#state{events=NewEvents});
false ->
Pid ! {MsgRef, {error, bad_timeout}},
loop(S)
end;
{Pid, MsgRef, {cancel, Name}} -> %%取消事件
Events = case orddict:find(Name, S#state.events) of
{ok, E} ->
event:cancel(E#event.pid),
orddict:erase(Name, S#state.events);
error->
S#state.events
end,
Pid ! {MsgRef, ok},
loop(S#state{events=Events});
{done,Name} -> %%事件进程发来的事件时间到了的通知
case orddict:find(Name, S#state.events) of
{ok, E} ->
send_to_clients({done, E#event.name, E#event.description},S#state.clients),
NewEvents = orddict:erase(Name, S#state.events),
loop(S#state{events=NewEvents});
error ->
loop(S)
end;
shutdown -> %%服务器关机
exit(shutdown);
{'DOWN', Ref, process, _Pid, _Reason} -> %%客户进程死亡
loop(S#state{clients=orddict:erase(Ref, S#state.clients)});
code_change -> %%热更新
?MODULE:loop(S);
Unknown ->
io:format("Unknown message:~p~n",[Unknown]),
loop(S)
end.
send_to_clients(Msg, ClientDict) ->
orddict:map(fun(_Ref, Pid) -> Pid ! Msg end, ClientDict).
valid_datetime({Date, Time}) ->
try
caledar:valid_date(Date) andalso valid_time(Time)
catch
error:function_clause ->
false
end;
valid_datetime(_) ->
false.
valid_time({H,M,S}) -> valid_time(H,M,S).
valid_time(H,M,S) when H>= 0, H < 24,
M>=0, M < 60,
S>=0, s<60 ->true;
valid_time(_,_,_) ->false.
%%提供给客户端的订阅消息接口
subscribe(Pid) ->
Ref = erlang:monitor(process, whereis(?MODULE)),
?MODULE ! {self(), Ref, {subscribe,Pid}},
receive
{Ref, ok} ->
{ok, Ref};
{'DOWN', Ref, process, _Pid, Reason} ->
{error, Reason}
after 5000 ->
{error, timeout}
end.
add_event(Name, Description, TimeOut) ->
Ref = make_ref(),
?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
receive
{Ref, Msg} -> Msg
after 5000 ->
{error, timeout}
end.
cancel(Name) ->
Ref = make_ref(),
?MODULE ! {self(), Ref, {cancel, Name}},
receive
{Ref, ok} -> ok
after 5000 ->
{error, timeout}
end.
listen(Delay) ->
receive
M = {done, _Name, _Description} ->
[M | listen(0)]
after Delay*1000 ->
[]
end.
文章浏览阅读5.8k次。在大数据的发展当中,大数据技术生态的组件,也在不断地拓展开来,而其中的Hive组件,作为Hadoop的数据仓库工具,可以实现对Hadoop集群当中的大规模数据进行相应的数据处理。今天我们的大数据入门分享,就主要来讲讲,Hive应用场景。关于Hive,首先需要明确的一点就是,Hive并非数据库,Hive所提供的数据存储、查询和分析功能,本质上来说,并非传统数据库所提供的存储、查询、分析功能。Hive..._hive应用场景
文章浏览阅读496次。Zblog是由Zblog开发团队开发的一款小巧而强大的基于Asp和PHP平台的开源程序,但是插件市场上的Zblog采集插件,没有一款能打的,要么就是没有SEO文章内容处理,要么就是功能单一。很少有适合SEO站长的Zblog采集。人们都知道Zblog采集接口都是对Zblog采集不熟悉的人做的,很多人采取模拟登陆的方法进行发布文章,也有很多人直接操作数据库发布文章,然而这些都或多或少的产生各种问题,发布速度慢、文章内容未经严格过滤,导致安全性问题、不能发Tag、不能自动创建分类等。但是使用Zblog采._zblog 网页采集插件
文章浏览阅读2.4k次,点赞2次,收藏2次。restUI页面提交1.1 添加上传jar包1.2 提交任务job1.3 查看提交的任务2. 命令行提交./flink-1.9.3/bin/flink run -c com.qu.wc.StreamWordCount -p 2 FlinkTutorial-1.0-SNAPSHOT.jar3. 命令行查看正在运行的job./flink-1.9.3/bin/flink list4. 命令行查看所有job./flink-1.9.3/bin/flink list --all._flink定时运行job
文章浏览阅读1k次,点赞2次,收藏6次。这个项目是基于STM32的LED闪烁项目,主要目的是让学习者熟悉STM32的基本操作和编程方法。在这个项目中,我们将使用STM32作为控制器,通过对GPIO口的控制实现LED灯的闪烁。这个STM32 LED闪烁的项目是一个非常简单的入门项目,但它可以帮助学习者熟悉STM32的编程方法和GPIO口的使用。在这个项目中,我们通过对GPIO口的控制实现了LED灯的闪烁。LED闪烁是STM32入门课程的基础操作之一,它旨在教学生如何使用STM32开发板控制LED灯的闪烁。_嵌入式stm32闪烁led实验总结
文章浏览阅读63次。本文介绍了安装和部署Debezium的详细步骤,并演示了如何将Debezium服务托管到systemctl以进行方便的管理。本文将详细介绍如何安装和部署Debezium,并将其服务托管到systemctl。解压缩后,将得到一个名为"debezium"的目录,其中包含Debezium的二进制文件和其他必要的资源。注意替换"ExecStart"中的"/path/to/debezium"为实际的Debezium目录路径。接下来,需要下载Debezium的压缩包,并将其解压到所需的目录。
文章浏览阅读4.4k次。需求:在诗词曲文项目中,诗词整篇朗读的时候,文章没有读完会因为屏幕熄灭停止朗读。要求:在文章没有朗读完毕之前屏幕常亮,读完以后屏幕常亮关闭;1.权限配置:设置电源管理的权限。
文章浏览阅读2.3k次。目标检测简介、评估标准、经典算法_目标检测
文章浏览阅读6.3k次,点赞4次,收藏9次。实训时需要安装SQL server2008 R所以我上网上找了一个.exe 的安装包链接:https://pan.baidu.com/s/1_FkhB8XJy3Js_rFADhdtmA提取码:ztki注:解压后1.04G安装时Microsoft需下载.NET,更新安装后会自动安装如下:点击第一个傻瓜式安装,唯一注意的是在修改路径的时候如下不可修改:到安装实例的时候就可以修改啦数据..._sqlserver 127 0 01 无法连接
文章浏览阅读7.4k次。1. Object.keys(item); 获取到了key之后就可以遍历的时候直接使用这个进行遍历所有的key跟valuevar infoItem={ name:'xiaowu', age:'18',}//的出来的keys就是[name,age]var keys=Object.keys(infoItem);2. 通常用于以下实力中 <div *ngFor="let item of keys"> <div>{{item}}.._js 遍历对象的key
文章浏览阅读2.2w次,点赞51次,收藏310次。粒子群算法求解路径规划路径规划问题描述 给定环境信息,如果该环境内有障碍物,寻求起始点到目标点的最短路径, 并且路径不能与障碍物相交,如图 1.1.1 所示。1.2 粒子群算法求解1.2.1 求解思路 粒子群优化算法(PSO),粒子群中的每一个粒子都代表一个问题的可能解, 通过粒子个体的简单行为,群体内的信息交互实现问题求解的智能性。 在路径规划中,我们将每一条路径规划为一个粒子,每个粒子群群有 n 个粒 子,即有 n 条路径,同时,每个粒子又有 m 个染色体,即中间过渡点的_粒子群算法路径规划
文章浏览阅读353次。所谓稳健的评估指标,是指在评估的过程中数据的轻微变化并不会显著的影响一个统计指标。而不稳健的评估指标则相反,在对交易系统进行回测时,参数值的轻微变化会带来不稳健指标的大幅变化。对于不稳健的评估指标,任何对数据有影响的因素都会对测试结果产生过大的影响,这很容易导致数据过拟合。_rar 海龟
文章浏览阅读607次,点赞2次,收藏7次。–基于STM32F103ZET6的UART通讯实现一、什么是IAP,为什么要IAPIAP即为In Application Programming(在应用中编程),一般情况下,以STM32F10x系列芯片为主控制器的设备在出厂时就已经使用J-Link仿真器将应用代码烧录了,如果在设备使用过程中需要进行应用代码的更换、升级等操作的话,则可能需要将设备返回原厂并拆解出来再使用J-Link重新烧录代码,这就增加了很多不必要的麻烦。站在用户的角度来说,就是能让用户自己来更换设备里边的代码程序而厂家这边只需要提供给_value line devices connectivity line devices