C#和TS/JS的對比學習02:函式與方法

functionMC發表於2022-04-01

程式本質上,就是由資料和處理資料的方法構成。函式和方法,這兩個名詞雖然字面不同,但意義上其實沒有區別。只是因為它們出現的地方有異,給予了不同的名稱,比如在全域性環境中,叫函式,在物件或類中,叫方法。而C#沒有全域性的概念,所以絕大多數時候,都叫方法。本節內容比較多,列一下目錄:

  1. 基本概念
  2. 函式的宣告和呼叫
  3. 函式表示式
  4. 通過Function的建構函式來宣告函式(JS/TS)
  5. 函式/變數提升(JS)
  6. 值引數
  7. 引用引數/輸出引數
  8. 可選引數/預設值引數
  9. 陣列引數/剩餘引數
  10. 擴充套件方法
  11. 閉包和委託捕獲

 

 

一、基本概念

JS:

①一等公民,和number、string、bool等一樣,是一種值;

②是object的子型別;

③可以全域性定義和使用;

④當函式作為物件的屬性時,稱之為方法

TS:

和JS一樣,只是多了型別約束

C #

①類中有兩個成員:資料成員和方法成員,方法規定了類的行為

②只能在類裡申明、類裡使用;

③方法可否視為一種值?C#中,方法不是一種值,但使用委託時,有類似表現

 

二、函式宣告和呼叫

JS/TS的函式可以宣告在全域性、物件和類中,C#只能在類中宣告。三者的引數和返回值等概念及用法沒有什麼不同,只是JS沒有型別約束,TS在JS基礎上增加了型別約束(還可以通過介面約束),而C#本身就是強型別。C#中多了一些修飾符,比如public等訪問修飾符,以及static、abtract等,TS中,當方法在類中申明時,也引入了部分修飾符。 

//==========JS==========

//
全域性宣告和呼叫 function sum(x,y){ return x + y; }
sum(1,2);
//物件中宣告和呼叫 const mc = { name: 'MC', sayHi: function(){console.log('Hi,i am MC');} , //還可以寫成 sayHi(){console.log('Hi,i am MC');} }
mc.sayHi();
//類中宣告(暫略,比較類時再具體談)
//==========TS==========

//
全域性宣告和呼叫,多了型別約束 function sum(x:number,y:number):number{ return x + y; }
sum(1,2);
//物件中宣告和呼叫,通過介面約束 interface{ name: string, sayHi():void } const person = { name: 'MC', sayHi(){console.log('Hi,i am MC');} }
person.sayHi();
//類中宣告(暫略,比較類時再具體談)
//==========C#==========

//需要在類中宣告,如果不是靜態方法,需要建立物件後,才能呼叫
public class Person
{
    public string Name{get;set}
    public int Sum(int x, int y){return x + y;}
    public void SayHi(){Console.WriteLine("Hi, i am MC");}
}
var p1 = new Person();
p1.SayHi();

 

三、函式表示式(JS/TS)

僅限於JS/TS,使用非常靈活,是函式作為一種值的突出表現。C#中沒有此概念,但是通過委託貌似能實現類似功能。

//==========JS==========

//將函式賦值給變數,變數sum也是方法的名稱
let sum = function(x,y){
    return x + y;
}
//方法呼叫
sum(5,6)
//==========TS==========

//通過型別推斷來宣告
let sum = function(x:number,y:number):number{
    return x + y;
}

//完整的寫法應該是這樣
let sum1:(x:number,y:number) => number = function(x:number,y:number):number{
    return x + y;
}

//也可以通過介面來約束申明
interface ISum{
    (x:number,y:number):number
}
let sum2:ISum = function(x:number,y:number):number{
    return x + y;
}
//==========C#==========

//定義一個委託型別
delegate int DeleSum(int x,int y); 
class HelloWorld
{
    static void Main(string[] args)
    {
        //定義一個委託物件,並將匿名函式“賦值”給委託“變數”
        DeleSum deleSum = delegate(int x,int y){return x + y;};
        Console.WriteLine(deleSum(2,2));
     }

}

//對比一下,都使用Lambda表示式,像不像?
//TS中:let sum = (int x,int y)=>{return x + y;}
//C#中,使用自定義委託型別:DeleSum deleSum = (int x,int y)=>{return x + y;}
//C#中,使用內建泛型委託:Func<int,int,int> sum =  (int x,int y)=>{return x + y;}

 

四、通過Function的建構函式來宣告函式/有點拗口(JS/TS)

僅限於JS/TS,極少使用。JS中,幾個型別都有相對應的包裝類,都有對應的構造方法,如number>Number,string>Stringarray>Arrayfunction>Functionobject>Object等。所以函式也可以通過建構函式建立。

//==========JS==========

//建構函式的引數,最後一個為返回值,前面的均為引數
let sum = new Function('x','y','return x+y');
console.log(sum(1,2));

//TS?不知道咋搞,型別約束放在哪?

 

五、函式提升(JS/TS)

僅限於JS/TS,在全域性或一個作用域中,編譯時,變數和函式的定義會先執行,函式定義優先於變數定義。函式提升僅限於通過“函式宣告”定義的方法,函式表示式定義的方法,不存在變數提升;變數提升僅限var定義的變數。let和const定義的變數,不存在變數提升。

//==========JS==========

//全域性中,雖然函式宣告在後面,但先執行了
console.log(sum1(1,2));
function sum1(x,y){return x+y;}

//函式作用域中,函式宣告也提前到了作用域的頂部
function f1(){
    console.log(sum2(1,2));
    function sum2(x,y){return x+y;}
}

//TS中有一樣的表現
//C#中不存在變數提升

 

六、值引數

形參和實參是值複製關係,呼叫方法時,實參的值複製給了形參。如果是基本型別,直接複製值,如果是引用型別,則複製引用地址。C#和JS/TS,基本一致。

//==========JS==========

//引數為值型別(複製值)
function sum(x,y){
    return x + y;
}
//呼叫時分別將1和2的值,複製給了形參x和y
sum(1,2);

//引數為引用型別(複製引用地址)
function sayName(x){
    console.log(x.name);
    x.name = 'functionMC';
}
let p1 = {
    name: 'MC',
    age: 18
}
//呼叫時將p1的引用地址複製給了形參x,兩者指向的堆中的值是同一個
sayName(p1);//輸出MC
console.log(p1.name);//輸出functionMC
//==========TS==========

//引數為值型別(複製值)
function sum(x:number,y:number):number{
    return x + y;
}
//呼叫時分別將1和2的值,複製給了x和y
sum(1,2);

//引數為引用型別。注:此處使用介面來約束形參和實參
interface IPerson{
    name: string,
    age: number
}
function sayName(x:IPerson):void{
    console.log(x.name);
    x.name = 'functionMC';
}
let p1:IPerson = {
    name: 'MC',
    age: 18
}
sayName(p1);//輸出MC
console.log(p1.name);//輸出functionMC
//==========C#==========

public class Program
{
    public static void Main()
    {
        //靜態方法中,不能直接呼叫例項成員,所以先將自己例項化
        Program program = new Program();

        //值型別引數,方法呼叫時,直接將值複製給形參
        program.Sum(1, 2); //結果為3

        //引用型別引數,方法呼叫時,將引用地址複製給形參
        //形參和實參指向的堆中的資料,是同一個
        var p1 = new Person() { Name = "MC", Age = 18 };
        program.SayName(p1); //輸入MC
        Console.WriteLine(p1.Name); //輸出functionMC
    }

    //定義一個使用值型別引數的方法
    public int Sum(int x, int y)
    {
        return x + y;
    }

    //定義一個使用引用型別引數的方法
    public void SayName(Person p)
    {
        Console.WriteLine(p.Name);
        p.Name = "functionMC";
    }
}

//自定義類,用來測試引用型別引數
public class Person
{
    public string? Name { get; set; }
    public int? Age { get; set; }
}

 

七、引用引數和輸出引數

引用參和輸出參,是C#中的概念。和值引數不同的是,實參作為形參的別名直接進入方法體中運算。所以,在方法體中如果改變了形參,也會同時改變實參。JS/TS中,因為var的作用域問題,也會產生類似結果。

//==========C#==========

//引用引數使用ref,輸出引數用out,原理和用法參不多
//在申明和呼叫的時候都要用ref或out關鍵詞
//呼叫時,只能使用變數
//out的特殊在於,在呼叫的方法體中,在給輸出引數賦值
//out在方法呼叫裡,變數可以不用賦值,賦值也沒有意義,因為方法體中需要賦值
class HelloWorld
{
    static void Main(string[] args)
    {
        Count a1 = new Count();
        int a2 = 10;

        //呼叫時,也要用ref關鍵詞修飾實參,且實參只能用變數
        RefMethod(ref a1, ref a2);
        Console.WriteLine($"a1值變成了{a1.Val},a2值變成了{a2}");
    }

    //方法定義時,使用ref關鍵詞修飾形參       
    static void RefMethod(ref Count c1, ref int i1)
    {
        //形參和實參是同一個,形參值變了,實參值也會變
        c1.Val += 2; 
    i1 += 2; 
      
    }
}
    
class Count
{
    public int Val = 20;
}
//==========JS/TS==========

//方法體中,直接找到全域性的變數count修改值
var count = 10;
function Method(){
    count += 2;
}
Method();
console.log(count);

 

八、可選引數/預設值引數

C#和TS都是強型別,所以方法引數要受到一定約束,可選引數、陣列引數等,都是在可約束條件下的增加靈活性。而JS的引數則不受任務約束,愛傳不傳,愛傳啥就傳啥。

//==========JS==========

//JS中沒有可選引數的概念,因為它不受約束
function f1(a,b){
    return a + b;
}
//愛咋咋滴
f1(1,2,3);
f1(1);
f1(1,'MC');
f1();
//==========TS==========

//“?”號定義可選引數
function f1(a:string,b?:string):void{
    console.log(a +'-'+ b);
}
//可傳可不傳,不傳時預設為undefined
f1('function','MC');//結果function-MC
f1('function');//結果function-undefined


//設定引數預設值
function f2(a:string,b:string='MC'):void{
    console.log(a +'-'+ b);
}
f2('function','MC');//結果function-MC
f2('function');//結果function-MC
//==========C#==========

public class Program
{
    public static void Main()
    {
        f1("function", "MC");//輸出結果function-MC
        f1("function");//輸出結果function-MC

        f2("function", "MC");//輸出結果function-MC
        f2("function");//輸出結果function-

    }

    //可選引數,設定預設值 
    static void f1(string a, string b = "MC")
    {
        Console.WriteLine(a + "-" + b);
    }

    //可空引數,如果不傳,則為null 
    static void f2(string a, string? b = null)
    {
        Console.WriteLine(a + "-" + b);
    }
}

 

九、陣列引數/剩餘引數

C#和TS都是強型別,所以方法引數要受到一定約束,可選引數、陣列引數等,都是在可約束條件下的增加靈活性。而JS的引數則不受任務約束,愛傳不傳,愛傳啥就傳啥。

//==========C#==========

public class Program
{
    public static void Main()
    {
        //呼叫方式一
        f1(1, 2, 3, 4);
        f1(1, 2, 3);
        f1(1, 2);
        f1();

        //呼叫方式二
        var a1 = new int[] { 1, 2, 3, 4, 5, 6 };
        f1(a1);
    }

    //使用關鍵詞params定義陣列引數 
    static void f1(params int[] intVals)
    {
        if ((intVals != null) && (intVals.Length != 0))
        {
            foreach (var item in intVals)
            {
                Console.WriteLine(item);
            }
        }

    }
}
//==========TS==========

//TS中用“...”定義剩餘引數
function push(array: any[], ...items: any[]):void {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);//結果[1,2,3]
push(a, 2, 3, 4);//結果[1,2,3,2,3,4]

 

十、擴充套件方法

擴充套件方法是C#中的概念,通過新類擴充套件定義新的方法,呼叫時,直接用原物件呼叫,就好像這個方法屬於原類一樣。JS和TS中,不動類,一樣也可以擴充套件,直接粗魯的“.”符號就可以,即使是引入了類,也能通過原型隨意擴充套件,和C#不一樣的是,實質上這個方法是新增到了原對像裡。

//==========C#==========

public class Program
{
    public static void Main()
    {
        var cal = new Cal(2, 4);
        cal.Sum();//結果為6,原類的方法
        cal.Avg();//結果為3,新類的擴充套件方法
    }

}

//原類
internal class Cal
{
    private int d1;
    private int d2;
    public Cal(int d1, int d2)
    {
        this.d1 = d1;
        this.d2 = d2;
    }
    public int Sum()
    {
        return d1 + d2;
    }
}

//在一個靜態的新類裡,"靜靜的"增加了一個新的方法
internal static class CalExtend
{
    //公開的靜態方法
    //引數為原型別,且使用this關鍵詞修飾
    static public int Avg(this Cal c1)
    {
        return c1.Sum() / 2;
    }
}
//==========JS==========

let a = {
    name:'MC',
    sayHi(){console.log('HI,MC');}
};
a.sayHi();

//隨意的擴充套件一個方法
a.sayHello = ()=>{console.log('Hello,MC')};
a.sayHello();
//==========TS==========

//下面這個案例,無法執行,提示Object沒有assign方法,TS中去掉這個方法了?
//下面的程式碼,去掉型別,可以在JS中執行
class Cal{
    x:number;
    y:number;
    constructor(x:number,y:number){
        this.x = x;
        this.y = y;
    }

    sum():number{
        return this.x + this.y;
    }
        
}

//隨意的新增一個擴充套件方法
Object.assign(Cal.prototype, {
    avg():number{
        return 2;
    }
});
let cal = new Cal(2,4);
cal.sum();
cal.avg();

 

十一、閉包和委託捕獲

JS/TS中,對於作用域的巢狀,內層作用域可以看到和使用外層作用域的東西,就像一個隱私玻璃,裡面可以看外面,外面看不到裡面。閉包可以簡單的類比為,外層作用域派出的,混入內層作用域的一個函式間諜,通過它將內層作用域的東西“偷出來”,這樣外層作用域也能看到和使用內層作用域的東西,而且這個函式間諜還很敬業,把內層的環境也一起打包帶了出來,使得內層環境不會塌陷。巧得是,C#中也有類似的功能,叫匿名方法的捕獲。

//==========JS/TS==========

function outF(){
    const x = '我是內層的x';
    function inF(){
        return x;
    }
    return inF();
}

//inF就是我們派出的間諜函式-閉包
//他不僅帶出了x,還把他潛入的作用域,整個都一鍋端了出來
console.log(outF());//結果為'我是內層的x'
console.log(x);//報錯,提示x沒有定義,外層直接向內層要是要不到的
public class Program
{
    public static void Main()
    {
        //定義一個委託物件
        Func<int> f1;
        
        //下面是內層作用域
        {
            int x = 3;
            f1 = () => { return x; };//捕獲了變數x
        }

        //委託物件f1將捕獲到的x帶到了外層作用域,獲得變數x
        Console.WriteLine(f1());
        Console.WriteLine(x);//報錯提示當前上下文不存在x

    }

}

 

相關文章