matlab调用函数netplot,【原创】Matlab.NET混合编程技巧之——直接调用Matlab内置函数(附源码)...

在我的上一篇文章【原创】混编技巧之——找出Matlab内置函数中,已经大概的介绍了matlab内置函数在混合编程中的优点,并通过程序找出了matlab中的大部分内置函数,当然更多人关心是如何像我所说得那样,不用直接编译,就直接在C#中调用这些内置函数。本文就带你揭开这些谜团。

声明,这篇文章是需要一点点混合编程基础的,基本概念和过程要懂一点,如果能简单成功混编一个简单的计算或者绘图例子,可以更容易理解。

1.传统的混合编程步骤

传统的混合编程有2种方式:

1)Matlab编写好M函数,利用deploytool编译m函数生成dll,在C#项目中引用并调用;

2)基于接口的编写方式,也是利用deploytool工具,过程繁琐一点,对编程人员素质要求高一点,但不需要进行繁琐的数据类型转换。我的博客有一篇文章专门介绍了这个混合编程方式,也有例子,大家有兴趣的可以看看:外链网址已屏蔽

不管上面用哪种方式,Matlab和C#混编的基本步骤,大概都是下面的过程:

1) 编写M函数,并首先在Matlab中测试是正确可以调用的。注意命名规范,注释规范;

2) 使用命令打开 deploytool工具,设置项目名称,选择类型:.NET Assembly,然后新建一个类,并添加编写好的M函数

3) 编译,生成dll,并在C#项目中添加引用(还需要引用对应版本的MWArray),利用对象浏览器查看生成dll的方法结构,并根据Matlab和C#的类型转换规则,进行数据转换即可, 如果是接口的编程,这个过程相对要简单。

2.深入解析传统混编所生成的代码

2.1 第一步:编写M函数,并测试可以使用

为了好我们今天的目的相匹配,特意封装一个简单的内置函数,plot,来画一个简单的图形,如下所示M函数

1 function PlotTest(n)2 %编写一个简单的函数,对plot进行简单封装一下3 plot(1:n,1:n);4 %测试正确,才可以进行下一步工作

注意,混编必须是m函数function的形式才能被调用。上述函数简单测试一下,没有问题(复杂的函数一定要多测试,否则后续调试非常困难)。继续下一步。

2.2 第二步:在Matlab中使用deploytool建立混编项目

在Matlab工作区输入命令:deploytool,然后得到下面界面,输入混编项目的名称,选择存储位置,关键的是类型那里一定要选择".NET Assembly"。如下图所示:

A082612121-39295.jpg

选择“OK”之后,下一步matlab界面右侧会出现项目解决方案,需要添加类名称和M文件。这个类名称,就是编译完成之后C#项目中的类对象名称,然后添加我们刚才上一步编写的“PlotTest.m”,然后编译即可,如下图所示:

A082614246-39296.jpg

到此为止,一个常规 简单的混编已经完成了60%了。编译完成之后,打开“Package”选项卡,即可看到生成的dll文件,然后点击右键,打开文件夹即可,如下图所示:

A082616355-39297.jpg

2.3 查看混编生成的代码

这个过程很关键,其实包含很多信息,只不过95%以上的人都没有注意到其实混编生成的dll是有源文件的,通过查看源文件就应该知道混编的原理,只不过这是matlab自动生成 而已。那看看生成的源码吧。

打开Matlab混编项目的目录,可以看到有2个文件夹,"distrib”,“src”2个文件夹。"distrib"文件夹就是上面图中生成的dll,注意有2个dll,1个是“项目名称.dll”,一个是“项目名称Native.dll”,这2个dll的差别可以通过"distrib"文件夹源码来观察。“distrib”就是源代码的文件夹。如下图所示,src文件夹的文件示意图:

A082618715-39298.jpg

我们2.2中新建的类名是TestDemo,所以生成的的源码名称也是TestDemo,看看这2个cs文件中的代码,同时类的方法也可以在VS中通过对象浏览器来查看dll有哪些方法以及方法的参数类型。直接贴这2个cs文件的代码,顺便解释和对比下:

TestDemo.cs文件源码:

%E5%A4%96%E9%93%BE%E7%BD%91%E5%9D%80%E5%B7%B2%E5%B1%8F%E8%94%BD

%E5%A4%96%E9%93%BE%E7%BD%91%E5%9D%80%E5%B7%B2%E5%B1%8F%E8%94%BD

1 /*

2 * MATLAB Compiler: 4.17 (R2012a)3 * Date: Mon Sep 09 16:19:01 20134 * Arguments: "-B" "macro_default" "-W" "dotnet:PlotTest,TestDemo,0.0,private" "-T"5 * "link:lib" "-d" "D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest\src" "-w"6 * "enable:specified_file_mismatch" "-w" "enable:repeated_file" "-w"7 * "enable:switch_ignored" "-w" "enable:missing_lib_sentinel" "-w" "enable:demo_license"8 * "-v" "class{TestDemo:D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest.m}"9 */

10 usingSystem;11 usingSystem.Reflection;12 usingSystem.IO;13 using.Arrays;14 using.Utility;15

16 #if SHARED

17 [assembly: System.Reflection.AssemblyKeyFile(@"")]18 #endif

19

20 namespacePlotTest21 {22

23 ///

24 ///The TestDemo class provides a CLS compliant, MWArray interface to the M-functions25 ///contained in the files:26 ///

27 ///D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest.m28 ///

29 ///deployprint.m30 ///

31 ///printdlg.m32 ///

33 ///

34 ///@Version 0.035 ///

36 public classTestDemo : IDisposable37 {38 #region Constructors

39

40 ///

41 ///The static constructor instantiates and initializes the MATLAB Compiler Runtime42 ///instance.43 ///

44 staticTestDemo()45 {46 if(MWMCR.MCRAppInitialized)47 {48 Assembly assembly=Assembly.GetExecutingAssembly();49

50 string ctfFilePath=assembly.Location;51

52 int lastDelimiter= ctfFilePath.LastIndexOf(@"\");53

54 ctfFilePath= ctfFilePath.Remove(lastDelimiter, (ctfFilePath.Length -lastDelimiter));55

56 string ctfFileName = "PlotTest.ctf";57

58 Stream embeddedCtfStream = null;59

60 String[] resourceStrings =assembly.GetManifestResourceNames();61

62 foreach (String name inresourceStrings)63 {64 if(name.Contains(ctfFileName))65 {66 embeddedCtfStream =assembly.GetManifestResourceStream(name);67 break;68 }69 }70 mcr= new MWMCR("",71 ctfFilePath, embeddedCtfStream, true);72 }73 else

74 {75 throw new ApplicationException("MWArray assembly could not be initialized");76 }77 }78

79

80 ///

81 ///Constructs a new instance of the TestDemo class.82 ///

83 publicTestDemo()84 {85 }86

87

88 #endregion Constructors

89

90 #region Finalize

91

92 ///

93 ///Class destructor called by the CLR garbage collector.94 ///

95 ~TestDemo()96 {97 Dispose(false);98 }99

100

101 ///

102 ///Frees the native resources associated with this object103 ///

104 public voidDispose()105 {106 Dispose(true);107

108 GC.SuppressFinalize(this);109 }110

111

112 ///

113 ///Internal dispose function114 ///

115 protected virtual void Dispose(booldisposing)116 {117 if (!disposed)118 {119 disposed= true;120

121 if(disposing)122 {123 //Free managed resources;

124 }125

126 //Free native resources

127 }128 }129

130

131 #endregion Finalize

132

133 #region Methods

134

135 ///

136 ///Provides a void output, 0-input MWArrayinterface to the PlotTest M-function.137 ///

138 ///

139 ///M-Documentation:140 ///编写一个简单的函数,对plot进行简单封装一下141 ///

142 ///143 public voidPlotTest()144 {145 mcr.EvaluateFunction(0, "PlotTest", newMWArray[]{});146 }147

148

149 ///

150 ///Provides a void output, 1-input MWArrayinterface to the PlotTest M-function.151 ///

152 ///

153 ///M-Documentation:154 ///编写一个简单的函数,对plot进行简单封装一下155 ///

156 /// Input argument #1

157 ///158 public voidPlotTest(MWArray n)159 {160 mcr.EvaluateFunction(0, "PlotTest", n);161 }162

163

164 ///

165 ///Provides the standard 0-input MWArray interface to the PlotTest M-function.166 ///

167 ///

168 ///M-Documentation:169 ///编写一个简单的函数,对plot进行简单封装一下170 ///

171 /// The number of output arguments to return.

172 /// An Array of length "numArgsOut" containing the output173 ///arguments.

174 ///175 public MWArray[] PlotTest(intnumArgsOut)176 {177 return mcr.EvaluateFunction(numArgsOut, "PlotTest", newMWArray[]{});178 }179

180

181 ///

182 ///Provides the standard 1-input MWArray interface to the PlotTest M-function.183 ///

184 ///

185 ///M-Documentation:186 ///编写一个简单的函数,对plot进行简单封装一下187 ///

188 /// The number of output arguments to return.

189 /// Input argument #1

190 /// An Array of length "numArgsOut" containing the output191 ///arguments.

192 ///193 public MWArray[] PlotTest(intnumArgsOut, MWArray n)194 {195 return mcr.EvaluateFunction(numArgsOut, "PlotTest", n);196 }197

198

199

200 ///

201 ///This method will cause a MATLAB figure window to behave as a modal dialog box.202 ///The method will not return until all the figure windows associated with this203 ///component have been closed.204 ///

205 ///

206 ///An application should only call this method when required to keep the207 ///MATLAB figure window from disappearing. Other techniques, such as calling208 ///Console.ReadLine() from the application should be considered where209 ///possible.

210 ///211 public voidWaitForFiguresToDie()212 {213 mcr.WaitForFiguresToDie();214 }215

216

217

218 #endregion Methods

219

220 #region Class Members

221

222 private static MWMCR mcr= null;223

224 private bool disposed= false;225

226 #endregion Class Members

227 }228 }

View Code

TestDemoNative.cs文件源码:

%E5%A4%96%E9%93%BE%E7%BD%91%E5%9D%80%E5%B7%B2%E5%B1%8F%E8%94%BD

%E5%A4%96%E9%93%BE%E7%BD%91%E5%9D%80%E5%B7%B2%E5%B1%8F%E8%94%BD

1 /*

2 * MATLAB Compiler: 4.17 (R2012a)3 * Date: Mon Sep 09 16:19:01 20134 * Arguments: "-B" "macro_default" "-W" "dotnet:PlotTest,TestDemo,0.0,private" "-T"5 * "link:lib" "-d" "D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest\src" "-w"6 * "enable:specified_file_mismatch" "-w" "enable:repeated_file" "-w"7 * "enable:switch_ignored" "-w" "enable:missing_lib_sentinel" "-w" "enable:demo_license"8 * "-v" "class{TestDemo:D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest.m}"9 */

10 usingSystem;11 usingSystem.Reflection;12 usingSystem.IO;13 using.Arrays;14 using.Utility;15

16 #if SHARED

17 [assembly: System.Reflection.AssemblyKeyFile(@"")]18 #endif

19

20 namespacePlotTestNative21 {22

23 ///

24 ///The TestDemo class provides a CLS compliant, Object (native) interface to the25 ///M-functions contained in the files:26 ///

27 ///D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest.m28 ///

29 ///deployprint.m30 ///

31 ///printdlg.m32 ///

33 ///

34 ///@Version 0.035 ///

36 public classTestDemo : IDisposable37 {38 #region Constructors

39

40 ///

41 ///The static constructor instantiates and initializes the MATLAB Compiler Runtime42 ///instance.43 ///

44 staticTestDemo()45 {46 if(MWMCR.MCRAppInitialized)47 {48 Assembly assembly=Assembly.GetExecutingAssembly();49

50 string ctfFilePath=assembly.Location;51

52 int lastDelimiter= ctfFilePath.LastIndexOf(@"\");53

54 ctfFilePath= ctfFilePath.Remove(lastDelimiter, (ctfFilePath.Length -lastDelimiter));55

56 string ctfFileName = "PlotTest.ctf";57

58 Stream embeddedCtfStream = null;59

60 String[] resourceStrings =assembly.GetManifestResourceNames();61

62 foreach (String name inresourceStrings)63 {64 if(name.Contains(ctfFileName))65 {66 embeddedCtfStream =assembly.GetManifestResourceStream(name);67 break;68 }69 }70 mcr= new MWMCR("",71 ctfFilePath, embeddedCtfStream, true);72 }73 else

74 {75 throw new ApplicationException("MWArray assembly could not be initialized");76 }77 }78

79

80 ///

81 ///Constructs a new instance of the TestDemo class.82 ///

83 publicTestDemo()84 {85 }86

87

88 #endregion Constructors

89

90 #region Finalize

91

92 ///

93 ///Class destructor called by the CLR garbage collector.94 ///

95 ~TestDemo()96 {97 Dispose(false);98 }99

100

101 ///

102 ///Frees the native resources associated with this object103 ///

104 public voidDispose()105 {106 Dispose(true);107

108 GC.SuppressFinalize(this);109 }110

111

112 ///

113 ///Internal dispose function114 ///

115 protected virtual void Dispose(booldisposing)116 {117 if (!disposed)118 {119 disposed= true;120

121 if(disposing)122 {123 //Free managed resources;

124 }125

126 //Free native resources

127 }128 }129

130

131 #endregion Finalize

132

133 #region Methods

134

135 ///

136 ///Provides a void output, 0-input Objectinterface to the PlotTest M-function.137 ///

138 ///

139 ///M-Documentation:140 ///编写一个简单的函数,对plot进行简单封装一下141 ///

142 ///143 public voidPlotTest()144 {145 mcr.EvaluateFunction(0, "PlotTest", newObject[]{});146 }147

148

149 ///

150 ///Provides a void output, 1-input Objectinterface to the PlotTest M-function.151 ///

152 ///

153 ///M-Documentation:154 ///编写一个简单的函数,对plot进行简单封装一下155 ///

156 /// Input argument #1

157 ///158 public voidPlotTest(Object n)159 {160 mcr.EvaluateFunction(0, "PlotTest", n);161 }162

163

164 ///

165 ///Provides the standard 0-input Object interface to the PlotTest M-function.166 ///

167 ///

168 ///M-Documentation:169 ///编写一个简单的函数,对plot进行简单封装一下170 ///

171 /// The number of output arguments to return.

172 /// An Array of length "numArgsOut" containing the output173 ///arguments.

174 ///175 public Object[] PlotTest(intnumArgsOut)176 {177 return mcr.EvaluateFunction(numArgsOut, "PlotTest", newObject[]{});178 }179

180

181 ///

182 ///Provides the standard 1-input Object interface to the PlotTest M-function.183 ///

184 ///

185 ///M-Documentation:186 ///编写一个简单的函数,对plot进行简单封装一下187 ///

188 /// The number of output arguments to return.

189 /// Input argument #1

190 /// An Array of length "numArgsOut" containing the output191 ///arguments.

192 ///193 public Object[] PlotTest(intnumArgsOut, Object n)194 {195 return mcr.EvaluateFunction(numArgsOut, "PlotTest", n);196 }197

198

199 ///

200 ///Provides an interface for the PlotTest function in which the input and output201 ///arguments are specified as an array of Objects.202 ///

203 ///

204 ///This method will allocate and return by reference the output argument205 ///array.

206 ///M-Documentation:207 ///编写一个简单的函数,对plot进行简单封装一下208 ///

209 /// The number of output arguments to return

210 /// Array of Object output arguments

211 /// Array of Object input arguments

212 /// Array of Object representing variable input213 ///arguments

214 ///215 [MATLABSignature("PlotTest", 1, 0, 0)]216 protected void PlotTest(int numArgsOut, ref Object[] argsOut, Object[] argsIn, paramsObject[] varArgsIn)217 {218 mcr.EvaluateFunctionForTypeSafeCall("PlotTest", numArgsOut, refargsOut, argsIn, varArgsIn);219 }220

221 ///

222 ///This method will cause a MATLAB figure window to behave as a modal dialog box.223 ///The method will not return until all the figure windows associated with this224 ///component have been closed.225 ///

226 ///

227 ///An application should only call this method when required to keep the228 ///MATLAB figure window from disappearing. Other techniques, such as calling229 ///Console.ReadLine() from the application should be considered where230 ///possible.

231 ///232 public voidWaitForFiguresToDie()233 {234 mcr.WaitForFiguresToDie();235 }236

237

238

239 #endregion Methods

240

241 #region Class Members

242

243 private static MWMCR mcr= null;244

245 private bool disposed= false;246

247 #endregion Class Members

248 }249 }

View Code

对比大家就可以发现,只不过一个更加傻瓜化,参数都是Object了,其实这样反而不好,增加了类型转换的代价,如果知道,为何不给一个正确的给他呢。关于这2个dll的速度,曾经听说过是有差别的,博客园有人给过测试,我没实际测试过,还是习惯用传统的TestDemo.cs,因为参数类型都是Object,不直观,出了问题也有点头疼。

2.4 上述Matlab自动生成代码的要点

后面某些类或者方法的XML注释就不说了,自动生成的东西,可以对照M文件的注释来看。

1.首先看第一段的注释信息:

1 /*

2 * MATLAB Compiler: 4.17 (R2012a)3 * Date: Mon Sep 09 16:19:01 20134 * Arguments: "-B" "macro_default" "-W" "dotnet:PlotTest,TestDemo,0.0,private" "-T"5 * "link:lib" "-d" "D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest\src" "-w"6 * "enable:specified_file_mismatch" "-w" "enable:repeated_file" "-w"7 * "enable:switch_ignored" "-w" "enable:missing_lib_sentinel" "-w" "enable:demo_license"8 * "-v" "class{TestDemo:D:\Work\DevelopMent_SVN\Matlab\MatlabBlog\PlotTest.m}"9 */

10 usingSystem;11 usingSystem.Reflection;12 usingSystem.IO;13 using.Arrays;14 using .Utility;

上面这段信息主要是说明当前Matlab编译器的版本,因为编译的版本和部署的MCR版本必须对应起来,否则是不能运行的。然后有编译日期,以及编译的参数,类名以及M函数的地址等信息,其实知道这些参数,理论上是可以自己在程序里面调用Matlab的编译器进行编译工作的,只不过比较复杂,能力有限,研究不下去。下面的引用大家应该明白,这个是MWArray.dll里面的命名空间,所以混编的项目都要引用对应版本的MWArray.dll

2.关键的静态构造函数

1 staticTestDemo()2 {3 if(MWMCR.MCRAppInitialized)4 {5 Assembly assembly=Assembly.GetExecutingAssembly();6 string ctfFilePath=assembly.Location;7 int lastDelimiter= ctfFilePath.LastIndexOf(@"\");8 ctfFilePath= ctfFilePath.Remove(lastDelimiter, (ctfFilePath.Length -lastDelimiter));9 string ctfFileName = "PlotTest.ctf";10 Stream embeddedCtfStream = null;11 String[] resourceStrings =assembly.GetManifestResourceNames();12 foreach (String name inresourceStrings)13 {14 if(name.Contains(ctfFileName))15 {16 embeddedCtfStream =assembly.GetManifestResourceStream(name);17 break;18 }19 }20 mcr= new MWMCR("",ctfFilePath, embeddedCtfStream, true);21 }22 else

23 {24 throw new ApplicationException("MWArray assembly could not be initialized");25 }26 }

如果有一些C#的开发和编程经验看上面的代码问题应该不大,否则还真有点难说清楚,简单的说几个要点:

1)这个构造函数的作用主要是检测MCR对象是否初始化,如果没有,则寻找程序集,并拼接资源的位置,正确进行初始化

2) 上面的ctf其实是包含在dll程序集里面的,以资源的形式,这个文件是核心,它才真正的包括了Matlab编译之后的,MCR可以运行的中间程序。

3) 必须要合法正确的ctf文件和dll文件才能 正确的初始化mcr对象,合法的意思,是Matlab内部有校验机制,包括对相关版本,在以前的版本生成文件中,很明显,现在2012a里面都隐藏起来了,不过要求都一样。

4) 上面方法其实已经间接的告诉了我们怎么初始化mcr对象,有了mcr对象,一切都好办了,因为它才是MCR的核心。

3.PlotTest封装的方法代码

1 public voidPlotTest()2 {3 mcr.EvaluateFunction(0, "PlotTest", newMWArray[]{});4 }5 public voidPlotTest(MWArray n)6 {7 mcr.EvaluateFunction(0, "PlotTest", n);8 }9 public MWArray[] PlotTest(intnumArgsOut)10 {11 return mcr.EvaluateFunction(numArgsOut, "PlotTest", newMWArray[]{});12 }13 public MWArray[] PlotTest(intnumArgsOut, MWArray n)14 {15 return mcr.EvaluateFunction(numArgsOut, "PlotTest", n);16 }

看了字段代码,再对应mcr的初始化,其实都很明朗了。通过mcr的EvaluateFunction来调用M函数。上面的代码有几个重载方法,可以实用很多不同的情况,有时候,这些方法的个数会更多,其实没多大必要,也可以自己编译一下,把没用的删掉,保留少数几个有用的即可。同时也可以看到,这里直接通过字符串来传递函数名称的,因此必须保证这个函数能被mcr搜索到。比如我们这里的"PlotTest"这个函数其实就包含了ctf文件中(注意ctf文件是可以和dll分开的,在混编项目里可以设置)。

3.上述代码到内置函数的调用

上述已经讲解了整个mcr调用的过程,其实就是通过mcr的EvaluateFunction来调用M函数,但要保证对于的函数名称在mcr搜索的范围内。那么我们是不是可以假设:内置函数都在MCR内部,应该是可以搜索到的,那么把上面的函数名称换一下,是不是也是可行的。这个假设也是我最早接触时候的想法,有了假设,当然要去验证。现在看来这个当然是肯定的,那么不妨重新演示一遍。过程不详细讲了,代码也有注释,混编要引用的MWArray.dll和命名空间也不提了,看代码:

1 usingSystem;2 usingSystem.Collections.Generic;3 usingSystem.Linq;4 usingSystem.Text;5 usingSystem.Reflection;6 usingSystem.IO;7

8 using.Utility;9 using.Arrays;10

11

12 namespaceBuildInFunctionDemo13 {14 classProgram15 {16 staticMWMCR mcr;17 static void Main(string[] args)18 {19 #region 首先使用PlotTest.dll来初始化mcr,因为这个dll是混编“合法”产生的,只有这样才能顺利启动mcr

20 if(MWMCR.MCRAppInitialized)21 {22 string path = bine(System.Environment.CurrentDirectory, "PlotTest.dll");23 Assembly assembly =Assembly.LoadFile(path);24 string ctfFilePath =assembly.Location;25 int lastDelimiter = ctfFilePath.LastIndexOf(@"\");26 ctfFilePath = ctfFilePath.Remove(lastDelimiter, (ctfFilePath.Length -lastDelimiter));27 string ctfFileName = "PlotTest.ctf";28 Stream embeddedCtfStream = null;29 String[] resourceStrings =assembly.GetManifestResourceNames();30

31 foreach (String name inresourceStrings)32 {33 if(name.Contains(ctfFileName))34 {35 embeddedCtfStream =assembly.GetManifestResourceStream(name);36 break;37 }38 }39 mcr = new MWMCR("",ctfFilePath, embeddedCtfStream, true);40 }41 else

42 {43 throw new ApplicationException("MWArray assembly could not be initialized");44 }45 #endregion

46

47 #region 直接调用混编dll中的封装函数进行测试

48 mcr.EvaluateFunction(0, "PlotTest", 5);49

50 //注意这里要断点调试才能看到效果哦,因为mcr会把图绘制在一个Figure上面,51 //后面的会覆盖前面的,这里暂停一下,可以看前面的效果52 //下面就是直接调用matlab的plot函数的效果

53 MWNumericArray x = new double[]{1,2,3,4,5};54 MWNumericArray y = new double[]{2,1,2.8,5.3,4.7};55 mcr.EvaluateFunction(0, "plot",x,y );56 #endregion

57

58 Console.ReadKey();59 }60 }61 }

唯一要注意的就是50-52的说明,要加断点看2次绘制的效果。分别截图如下:

A082620824-39299.jpg_small.jpg

A082623043-39300.jpg_small.jpg=

4.总结

抛砖引玉,这里只是一个思路,附代码下载吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值