做信號處理、控制等軟件程序時,經常會需要將一個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小時內與您取得聯系。