做信号处理、控制等软件程序时,经常会需要将一个s域的传递函数变成实际可执行的代码。如果不是经常遇到,估计得查资料查半天才能找到方法。为了方便自己和大家以后遇到同样的问题不再走冤枉路,今天把整个顺序做一个记录和说明。
做信号处理,离不开强大的matlab,所以这里做的说明以matlab和C语言作为例子。如果要转换成别的语言,差别不大。
整体转换的顺序为:S域传递函数--->通过matlab转换为Z域传递函数---->转换为C代码。
首先,拿到一个S域的传递函数,其表达方式类似于如下图片所示:
连续传递函数
那么如何将这个传递函数转换为Z域传递函数(数字化)呢?用matlab的c2d函数。
c2d最常用的参数形式为:
sysd=c2d(sys,Ts,method)
其中sys表示连续系统传递函数,Ts表示采样时间,method表示转换的方法。
method可以选5种:
那么sys又是怎么得到的呢?
控制系统中的传递函数G(s)通常描述为:
传递函数表达式
在matlab中,用tf函数来获得:
sys=tf(num,den)
num=[b0 b1 b2 .... bm] 是分子,den=[a0 a1 a2 .... an]是分母。
好了,回到最初的例子,将其表达式转换为matlab模型:
H=tf([1 1],[1 4 5])
matlab结果
然后将其变为数字系统的传递函数,采样时间为1ms:
HD=c2d(H,0.001,‘zoh’)
得到的离散数字域传递函数
其他几种变换方法,大家可以自己试一下,看看结果有何不同。
好了,得到了Z域传递函数,接下去就是要把传递函数变为可执行的C代码,代码中x表示输入,y表示输出。把分子和分母都除以z^2可得:
DSP形式的传递函数
按照这个公式带入
用C语言的数组x[3],y[3],分别来存储输入和输出的值,x[0]=x(n),x[1]=x(n-1)..以此类推,上面的公式可以表示为:
y[0]=x[0]*0+x[1]*0.0009985-x[2]*0.0009975+y[1]*1.996-y[2]*0.996;
x[2]=x[1];
x[1]=x[0];
y[2]=y[1];
y[1]=y[0];
代码中,y[0]就表示当前运算周期的输出,x[0]表示当前周期的输入。
到此,一个S域传递函数就被我们给搬到了C代码中。
JavaScript中,函数是一等公民,同时函数也是对象,可以像其他对象一样被传递、赋值、修改和调用。在函数调用时,可以通过调用函数对象的call、apply和bind方法来改变其执行上下文或生成一个新的函数,这些方法也是 JavaScript函数的重要部分。
有时候,我们需要在调用函数时动态地改变执行上下文,或者为函数生成一个绑定了特定上下文的新函数。这时,就需要用到call、apply和bind这三个专门用来改变函数执行上下文的方法。
在本文中,我们将手写JavaScript的call、apply和bind方法,让读者在深入了解其内部实现的同时,提升对这些方法的理解和运用。
一、基本概念
call、apply和bind方法是JavaScript的三个基本方法,它们都可以用来改变函数执行上下文中的this值。
其中,call和apply方法是直接对函数进行调用,并且允许我们在调用时手动传入this指向的对象和函数参数,两者的区别在于参数的传递方式不同:call方法的参数是一个一个地传入,而apply方法的参数是一个数组或类数组对象形式传入。
相比之下,bind方法则是返回一个新的函数,并且该新函数的执行上下文中的 this值会被永久绑定到bind方法的第一个参数所指向的对象,这也是bind方法与call和appl 方法最大的区别。
代码举例:
1、call方法的代码示例
function fn(greeting) {
console.log(greeting + ', ' + this.name + '!');
}
var user={ name: 'Alex' };
// 需要改变this指向的函数fn调用Function.prototype身上的call方法
// call方法会直接调用函数,第一参数为要指向的对象,第二个参数开始为依次传入函数的参数
fn.call( user, "hello" ) // "hello, Alex!"
2、apply方法的代码示例
function fn(greeting) {
console.log(greeting + ', ' + this.name + '!');
}
var user={ name: 'Alex' };
// 需要改变this指向的函数fn调用Function.prototype身上的apply方法
// apply方法会直接调用函数,第一参数为要指向的对象,要传入的参数放入第二参数的数组中
fn.apply( user, ["hello"] ) // "hello, Alex!"
3、bind方法的代码示例
function fn(greeting1, greeting2) {
console.log(greeting1 + ', ' + this.name + '!',greeting2);
}
var user={ name: 'Alex' };
// 需要改变this指向的函数fn调用Function.prototype身上的call方法
// bind方法会返回一个新函数,第一参数为要指向的对象,第二个参数开始为依次传入函数的参数
var newFn=fn.bind(user, "hello")
// 可以在调用返回函数的时候继续补充传参,这就是函数柯里化
newFn("hi") // "hello, Alex!" "hi"
// 这里需要注意,bind方法只能改变一次this指向,第二次改变的时候,依然执行第一次改变时的结果
// 不过可以在第二次改变的时候补充传参
var newFn1=newFn.bind({ name: 'new' }, "hi")
newFn1() // "hello, Alex!" "hi"
注意:bind方法只能改变一次this指向,第二次改变后再调用,获取的还是第一次改变的this指向,bind返回的函数可以继续补充传递参数。
总之,使用call、apply和bind方法可以让我们更加灵活地操作函数的执行上下文,并且方便地将一个函数应用于不同的对象上,提高了代码的重用性和可读性。
二、手写代码
1、手写call方法
call方法的作用是改变函数的执行上下文,其实现思路主要有以下几步:
(1)将调用call方法的函数绑定到需要指向的对象上;
(2)执行绑定后的函数,获取执行结果并返回。
下面是 call 方法的代码实现:
Function.prototype.myCall=function(obj, ...args) {
// 如果没有传入要绑定的obj,或者值为null或undefined则赋值为window
obj=obj || window;
// 为obj添加一个属性,其值也指向该函数
const symbol=Symbol('fn');
obj[symbol]=this;
// 利用obj调用该函数,此时函数里的this就是obj了,并获取函数的返回结果
const result=obj[symbol](...args);
// 删掉绑定的函数
delete obj[symbol];
// 导出函数的返回结果
return result;
}
2、手写apply方法
apply方法和call方法的作用相同,也是改变函数的执行上下文,只是apply方法接受一个包含参数的数组作为函数的第二个参数。其实现思路主要有以下几步:
(1)将调用apply方法的函数绑定到需要指向的对象上;
(2)执行绑定后的函数,获取执行结果并返回。
下面是apply方法的代码实现:
Function.prototype.myApply=function(obj, args) {
// 如果没有传入要绑定的obj,或者值为null或undefined则赋值为window
obj=obj|| window;
// 为obj添加一个属性,其值也指向该函数
const symbol=Symbol('fn');
obj[symbol]=this;
// 利用obj调用该函数,此时函数里的this就是obj了,并获取函数的返回结果
// 将数组用展开运算符展开,作为实参传入函数中
const result=obj[symbol](...args);
// 删掉绑定的函数
delete obj[symbol];
// 导出函数的返回结果
return result;
}
3、手写bind方法
bind方法的作用是将函数与指定的上下文对象进行绑定,并返回一个绑定后的函数。其实现思路主要有以下几步:
(1)创建一个新函数,绑定上下文对象和新函数的参数;
(2)返回绑定后的新函数。
下面是bind方法的代码实现:
Function.prototype.myBind=function(obj, ...args1) {
// 缓存调用bind的函数
const self=this;
// 嵌套一层函数,返回函数调用apply方法的结果
// 此处也可以补充传参
return function(...args2) {
return self.apply(obj, [...args1, ...args2]);
}
}
在上述实现中,使用了rest参数语法(...args)来获取函数的参数,以及扩展语法(...)来合并函数的参数。实现了自定义的call、apply和bind方法以后,就可以方便地通过这些方法来改变函数的执行上下文,实现更加灵活和多样化的编程需求。
三、常见应用场景
1、函数继承
在进行函数继承时,可以使用call或apply方法将父函数的执行上下文绑定到子函数上,从而实现在子函数中调用父函数的方法或属性,例如:
function A(name) {
this.name=name
this.sayHi=function() {
console.log('Hi, ' + this.name)
}
}
function B(name) {
A.call(this, name) // 将父类的实例绑定到子类上
}
const b=new B('Tom')
b.sayHi() // Hi, Tom
2、改变函数执行上下文
在调用函数时,可以使用call或apply方法改变函数的执行上下文,从而实现在不同的环境中使用同一个函数,例如:
const obj1={ name: 'Tom' }
const obj2={ name: 'Jerry' }
function sayHi() {
console.log('Hi, ' + this.name)
}
sayHi.call(obj1) // Hi, Tom
sayHi.call(obj2) // Hi, Jerry
3、函数柯里化
函数柯里化(Currying)是一种特殊的函数套用技术,即将一个N元函数转换为n个一元函数,从而实现对函数参数的逐步细化。在函数柯里化过程中,可以使用bind方法来实现对函数的参数预置,例如:
function add(a, b, c) {
return a + b + c
}
const add2=add.bind(null, 2) // 将 add 函数的第一个参数预置为 2
console.log(add2(3, 4)) // 9
在上述代码中,通过bind方法将add函数的第一个参数预置为2,从而生成了一个新函数add2,当调用add2函数时只需要传入剩余的两个参数,就可以得到预期的结果。
以上就是call、apply和bind方法的常见应用场景,它们可以极大地丰富我们编程的技巧和思路,提高代码的可读性和可维护性。
总结
虽然JavaScript中内置了call、apply和bind方法,但它们的背后的实现原理并不复杂,我们可以通过手写这些方法来更好地理解它们的本质。
手写call、apply和bind方法的主要思路就是利用 arguments 对象以及 Function.prototype 来模拟函数的执行环境和传参过程,然后使用 Function.prototype的call、apply和bind方法来绑定函数的执行上下文和参数列表。
尽管手写这些方法可能只在一些特殊场景下才会用到,但它们对于我们理解JavaScript中的函数调用机制和上下文切换等概念具有重要意义。
总的来说,熟练掌握函数的调用、应用和绑定方法,可以帮助我们更加灵活地开发JavaScript应用程序,提高代码的可读性、可维护性和性能,从而为构建更好的Web应用奠定坚实的基础。
上一篇文章使用C#编写一个.NET分析器文章发布以后,很多小伙伴都对最新的NativeAOT函数导出比较感兴趣,今天故写一篇短文来介绍一下如何使用它。
在以前,如果有其他语言需要调用C#编写的库,那基本上只有通过各种RPC的方式(HTTP、GRPC)或者引入一层C++代理层的方式来调用。
自从微软开始积极开发和研究Native AOT以后,我们有了新的方式。那就是直接使用Native AOT函数导出的方式,其它语言(C++、Go、Java各种支持调用导出函数的语言)就可以直接调用C#导出的函数来使用C#库。
废话不多说,让我们开始尝试。
我们先来一个简单的尝试,就是使用C#编写一个用于对两个整数求和的Add方法,然后使用C语言调用它。
1.首先我们需要创建一个新的类库项目。这个大家都会了,可以直接使用命令行新建,也可以通过VS等IDE工具新建。
dotnet new classlib -o CSharpDllExport
2.为我们的项目加入Native AOT的支持,根据.NET的版本不同有不同的方式。
如果你是.NET6则需要引入Microsoft.DotNet.ILCompiler
这个Nuget包,需要指定为7.0.0-preview.7.22375.6
,新版本的话只允许.NET7以上使用。更多详情请看hez2010的博客 https://www.cnblogs.com/hez2010/p/dotnet-with-native-aot.html
如果是.NET7那么只需要在项目属性中加入<PublishAot>true</PublishAot>
即可,笔者直接使用的.NET7,所以如下配置就行。
3.编写一个静态方法,并且为它打上UnmanagedCallersOnly
特性,告诉编译器我们需要将它作为函数导出,指定名称为Add。
using System.Runtime.InteropServices;
namespace CSharpDllExport
{
public class DoSomethings
{
[UnmanagedCallersOnly(EntryPoint="Add")]
public static int Add(int a, int b)
{
return a + b;
}
}
}
4.使用dotnet publish -p:NativeLib=Shared -r win-x64 -c Release
命令发布共享库。共享库的扩展名在不同的操作系统上不一样,如.dll
、.dylib
、.so
。当然我们也可以发布静态库,只需要修改为-p:NativeLib=Static
即可。
5.使用DLL Export Viewer
工具打开生成的.dll
文件,查看函数导出是否成功,如下图所示,我们成功的把ADD方法导出了,另外那个是默认导出用于Debugger的方法,我们可以忽略。工具下载链接放在文末。
6.编写一个C语言项目来测试一下我们的ADD方法是否可用。
#define PathToLibrary "E:\MyCode\BlogCodes\CSharp-Dll-Export\CSharpDllExport\CSharpDllExport\bin\Release\net7.0\win-x64\publish\CSharpDllExport.dll"
// 导入必要的头文件
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
int callAddFunc(char* path, char* funcName, int a, int b);
int main()
{
// 检查文件是否存在
if (access(PathToLibrary, 0)==-1)
{
puts("没有在指定的路径找到库文件");
return 0;
}
// 计算两个值的和
int sum=callAddFunc(PathToLibrary, "Add", 2, 8);
printf("两个值的和是 %d \n", sum);
}
int callAddFunc(char* path, char* funcName, int firstInt, int secondInt)
{
// 调用 C# 共享库的函数来计算两个数的和
HINSTANCE handle=LoadLibraryA(path);
typedef int(*myFunc)(int, int);
myFunc MyImport=(myFunc)GetProcAddress(handle, funcName);
int result=MyImport(firstInt, secondInt);
return result;
}
7.跑起来看看这样我们就完成了一个C#函数导出的项目,并且通过C语言调用了C#导出的dll。同样我们可以使用Go的syscall
、Java的JNI
、Python的ctypes
来调用我们生成的dll,在这里就不再演示了。
使用这种方法导出的函数同样有一些限制,以下是在决定导出哪种托管方法时要考虑的一些限制:
如果是引用类型的话注意需要传递指针或者序列化以后的结构体数据,比如我们编写一个方法连接两个string
,那么C#这边就应该这样写:
[UnmanagedCallersOnly(EntryPoint="ConcatString")]
public static IntPtr ConcatString(IntPtr first, IntPtr second)
{
// 从指针转换为string
string my1String=Marshal.PtrToStringAnsi(first);
string my2String=Marshal.PtrToStringAnsi(second);
// 连接两个string
string concat=my1String + my2String;
// 将申请非托管内存string转换为指针
IntPtr concatPointer=Marshal.StringToHGlobalAnsi(concat);
// 返回指针
return concatPointer;
}
对应的C代码也应该传递指针,如下所示:
// 拼接两个字符串
char* result=callConcatStringFunc(PathToLibrary, "ConcatString", ".NET", " yyds");
printf("拼接符串的结果为 %s \n", result);
....
char* callConcatStringFunc(char* path, char* funcName, char* firstString, char* secondString)
{
HINSTANCE handle=LoadLibraryA(path);
typedef char* (*myFunc)(char*, char*);
myFunc MyImport=(myFunc)GetProcAddress(handle, funcName);
// 传递指针并且返回指针
char* result=MyImport(firstString, secondString);
return result;
}
运行一下,结果如下所示:
*请认真填写需求信息,我们会在24小时内与您取得联系。