第壹章第7节 C#和TS语言对比-函数方法

17 阅读9分钟

这节难度有点飙升,我把委托、事件、捕获、闭包、扩展方法等全放一块了。放一块的原因,就是为了让这些概念变得简单些。另外,关于函数和方法这两个名称,有些人觉得在全局使用时,叫函数;在类或对象中时,叫方法。我认为,两者是一个意思,可以互换。

一、函数声明和调用

C#的函数只能在类中声明,而TS可以声明在全局、对象和类中。C#中多了一些修饰符,比如public、static、abtract等,TS中的方法如果在类中申明时,也可以使用一些修饰符。

//C#中声明函数和调用=================================================================
public class Program
{
    public static void Main()
    {
        //调用实例方法,需要先实例化对象
        var p1 = new Person();
        p1.SayHi(); 
        //通过类名调用静态方法
        int sum = Person.Sum(1,2);
    }
}
public class Person
{
    //static静态方法
    public static int Sum(int x, int y)
    {
        return x + y;
    }
    //实例方法
    public void SayHi()
    {
        Console.WriteLine("Hi, i am MC");
    }
}


//TS中声明函数和调用=================================================================
//全局声明和调用
//***注意,和var定义变量一样,function定义的函数在全局作用域中,定义会被提升到文档的开始
//***函数表达式定义的函数,不存在定义提升的情况
function sum(x:number,y:number):number{
    return x + y;
}
sum(1,2);

//通过对象声明
const person = {
    name: 'MC',
    sayHi(){
      console.log('Hi,i am MC');
    }
}
person.sayHi();

//通过类声明
class Person {
  name: string;
  sayHello() {
    console.log(`Hello, I'm ${this.name}`);
  }
}
let person = new Person();
person.name = "mc";
person.sayHello();

//通过函数的构造函数来创建,最后一个参数为返回值,前面的均为参数。基本不会用到。
//TS中,为基本类型提供了包装函数,都有对应的构造方法
//如number>Number,string>String,array>Array,function>Function,object>Object等
let sum = new Function('x','y','return x+y');
sum(1,2);

二、函数的参数

2.1 值参数(形参和实参)

形参和实参是值复制关系,调用方法时,实参的值复制给了形参。如果是基本类型,直接复制值,如果是引用类型,则复制引用地址。C#和TS,基本一致。

//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; }
}



//TS的值参数==========================================================================
//参数为值类型(复制值)
function sum(x:number,y:number):number{
    return x + y;
}
//调用时分别将1和2的值,复制给了x和y
sum(1,2);

//参数为引用类型。注:此处使用type来约束形参和实参
type Person = {
    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

//***在TS的方法中,存在一个内置对象arguments,实际上实参组成的数组
function test() {
    for (let i = 0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}
test('Hello', 'World', '!');

2.2 引用参数和输出参数(仅C#)

引用参和输出参,是C#中的概念。和值参数不同的是,实参作为形参的别名直接进入方法体中运算。所以,在方法体中如果改变了形参,也会同时改变实参。TS中,因为var的作用域问题,也会产生类似结果。

//C#的引用参数和输出参数=============================================================

//引用参数使用ref,输出参数用out,原理和用法参不多
//在申明和调用的时候都要加ref或out关键词
//调用时,实参只能使用变量,不能使用实际值
//ref只要用于在方法体内改变量的值,out主要用于输出多个方法体的结果
class Program
{
    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;
}

//输入参数out
class Program
{
    static void Main(string[] args)
    {
        string str = "123";
        int number;
        bool result = int.TryParse(str, out number); //TryParse是int类型自带静态方法
    }
}

2.3 可选参数和参数默认值

C#和TS都是强类型,所以方法参数要受到一定约束,可选参数、数组参数等,都是在可约束条件下的增加灵活性。而JS的参数则不受任务约束,爱传不传,爱传啥就传啥。

//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);
    }
}


//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

2.4 数组参数/剩余参数

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中的数组参数=====================================================================
//...定义剩余参数
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]



//==========1、函数定义式声明,注意参数和返回值的类型声明==========
//无返回值是,返回值用void,或者省略
function add(x :string, y :string) :string {
    let z : string = x + " " + y
	return z;
}
add("a","b") //调用,参数类型要一致


//==========2、函数表达式,只有使用函数表达式,函数才可以作为值传递==========
const add = function (x: string, y: string): string {
    return x + " " + y
}
//函数作为值传递,更多作为参数传递,就是C#中的委托
const fn2 = function () {
}
const fn3 = fn2
//有时我们只是声明一个函数变量,而没有具体的实现
const add:(string,string):string //先申明
add = function(x:string,y:string):string { //再实现
  return x+""+y
}


//==========3、箭头函数,等同于函数表达式,写法更加简图,常作为参数传入函数==========
//定义箭头函数
const add = (x:string, y:string):string=>{
   return x + " " + y
}
console.log(add("a","b"))
//箭头函数作为参数传入
const arr = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
const a2 = arr.map((s:string):number=> {
    return s.length
});
//继续简写,可以推断类型,arr.map((s)=>{return s.length})
//继续简写,arr.map(s=>s.length)
console.log(a2); // logs [ 8, 6, 7, 9 ]


//==========4、异步函数==========
//async和await必须一起使用,实际上是promise的语法糖
//具有传染性,调用异步函数时,也需要使用异步方式,使用async或者promise语法
async function foo(): Promise<void> {
  try {
    await aPromise() //aPromise()本身也是异步函数
  } catch (error) {
    console.log(error)
  }
}
async function foo(): Promise<number> {
  return 1 //返回值即使不是promise,也是隐式都使用promise进行包装
}

三、函数作为值传递

TS中函数是一等公民,function是一种类型,定义的具体函数也是一个值,所以可以作为方法的参数和返回值进行传递。在C#中,函数不是一种类型,也不是一个值,需要通过委托来实现类似功能。如果从TS的角度来理解委托,委托其实很简单。所以,本节先说TS。

3.1 TS的函数表达式

//1、函数表达式的定义=================================================================
//通过类型推断来声明
let sum = function(x:number,y:number):number{
    return x + y;
}

//先定义再赋值
let sum1:(x:number,y:number) => number
sum1 = function(x:number,y:number):number{
    return x + y;
}

//也可以通过type约束
type Sum = {
    (x:number,y:number):number
}
let sum2:Sum = function(x:number,y:number):number{
    return x + y;
}

//使用箭头函数来简化定义
let sum = (x: number, y: number): number => x + y;


//2、函数作为参数使用================================================================
// 使用箭头函数定义一个函数,返回值为将参数加 1。函数定义建议使用const
const addOne = (num: number): number => num+1;

// 定义一个函数,接受一个函数作为参数,并返回该函数的结果
function applyFunction(func: (num: number) => number, num: number): number {
  return func(num);
}

// 调用 applyFunction 函数,传递 addOne 函数和 5 作为参数
const result = applyFunction(addOne, 5);
console.log(result); 

3.2 C#的委托

//1、委托的演变======================================================================
//自定义一个委托类型(类似自定义一个类),可以认为是一个方法的模型
//以此模型创建的实例对象,参数和返回值的类型要保持一致
delegate int DeleSum(int x); 
public class Program
{
    public static void Main(string[] args)
    {   
        //创建一个委托对象,并使用Func1方法赋值
        //类似于TS中的const deleSum = function(int x){return x+1};
        //下行代码可以简写为:DeleSum deleSum = AddOne
        DeleSum deleSum = new DeleSum(AddOne);
        
        //先调用一下deleSum
        deleSum(1); //结果为2

        //再实现将AddOne方法作为参数传入
        ApplyFunction(deleSum,2); //结果为3
    }

    public int AddOne(int x)
    {
        return x + 1;
    }
  
    public int ApplyFunction(DeleSum func, int num)
    {
        return func(num);
    }
}

//上面我们创建委托对象时,还需要先定义方法,再给委托对象赋值,下面开始简化
//第一步简化,匿名函数
DeleSum deleSum = delegate (int x){return x+1};
//第二步简化,箭头函数
DeleSum deleSum = (int x) => {return x+1};
//第三步简化,简化箭头函数
DeleSum deleSum = (int x) => x+1;

//上面我们仍然要自定义一个委托类型,然后再创建委托对象
//C#内置了几个泛型的委托类型,我们不需要自定义委托类型,就可以直接使用
Func<int,int> deleSum = (int x) => x+1;
//内置的泛型委托类型主要有Action<T1,T2...>和Func<T1,T2...TResult>
//Action没有返回值,Func的最后一个泛型参数是返回值,入参可以有任意个(好像是0-16个)
//有了Action和Func,基本上不用自己再定义委托类型

//最后对比一下,都使用Lambda表达式后,C#和TS有什么不一样
//JS
let sum = (int x,int y):int => x + y;
//C#
Func<int,int,int> sum =  (int x,int y) =>  x + y;
var a = (int x, int y) => x + y; //利用类型推断
//可见,已经几乎一样了。所以,如果你从TS的角度去理解委托,也不难。


//2、多播委托========================================================================
//形式上,虽然委托的使用已经无限接近函数表达式
//但本质上,委托和函数表达式还是两个不一样的东西。另外,委托有一些特性,比如多播
class Program
{
    static void Main()
    {
        Action<string> multicastDelegate = null;

        multicastDelegate += PrintMessage1;
        multicastDelegate += PrintMessage2;

        //执行委托对象multicastDelegate后,两个方法都会被调用
        multicastDelegate("Hello!");
    }

    static void PrintMessage1(string message)
    {
        Console.WriteLine("From PrintMessage1: " + message);
    }

    static void PrintMessage2(string message)
    {
        Console.WriteLine("From PrintMessage2: " + message);
    }
}



//3、模仿泛型委托,在TS中实现类似功能,建议全局定义===================================
type Action = ()=>void;
type Action<T1> = (t1:T1)=>void;
type Action<T1, T2> = (t1:T1, t2:T2)=>void;
......
type Func<TRusult> = ()=>TRusult;
type Func<T1, TRusult> = (t1:T1)=>TRusult;
type Action<T1, T2, TRusult> = (t1:T1, t2:T2)=>TRusult;
......
import type { Action, Func } from './types';

function demo(func:Func<number,bool>, num:number){
  func(num);
}
const f1 = (a:number):bool=>{
  a>0 ? true : false;
}
demo(f1,10);

3.3 C#的事件

事件是基于委托实现的,在委托的基础上,又抽象的一层。开始可以不用在委托和事件里面绕,直接记用法。

//1、委托和事件的关系=================================================================
delegate void MyDelegate(int num);

class Program
{
    static void Main()
    {
        //实例化包含事件成员的类,获取到事件
        EventClass eventClass = new EventClass();
        //订阅绑定事件回调
        eventClass.MyEvent += EventHandler1;
        eventClass.MyEvent += EventHandler2;
        //调用TriggerEvent()方法,触发事件
        //也可以直接使用eventClass.MyEvent?.Invoke(10);
        eventClass.TriggerEvent(10);
    }
  
    //定义事件的类
    class EventClass
    {
        //事件是类的成员,使用event修饰词,类型必须是委托类型
        public event MyDelegate MyEvent;

        public void TriggerEvent(int num)
        {
            MyEvent?.Invoke(num); //触发事件,或者叫发布事件
        }
    }

    //事件回调,触发/发布事件时,执行的业务逻辑。还需要订阅绑定才能生效。
    static void EventHandler1(int num)
    {
        Console.WriteLine("通过事件调用 EventHandler1: " + num);
    }
    static void EventHandler2(int num)
    {
        Console.WriteLine("通过事件调用 EventHandler2: " + num);
    }
}



//2、实际使用过程中使用框架内置的委托类型==============================================
//EventHandler----------
//EnentHander:public delegate void EventHandler(object sender, EventArgs e);
class Program
{
    static void Main()
    {
        MyEventSource eventSource = new MyEventSource();
        eventSource.SomeEvent += EventSource_SomeEvent;
        eventSource.TriggerEvent();
    }
    static void EventSource_SomeEvent(object sender, EventArgs e)
    {
        Console.WriteLine("SomeEvent 发生了!");
    }
}

class MyEventSource
{
    public event EventHandler SomeEvent;
    public void TriggerEvent()
    {
        SomeEvent?.Invoke(this, EventArgs.Empty);
    }
}

//EventHandler<TEventArgs>
//EventHandler只能使用预定义的参数类型EventArgs,泛型版本可以自定义参数类型
//public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) 
                                              //where TEventArgs : EventArgs;
class Program
{
    static void Main()
    {
        MyEventWithArgs eventSource = new MyEventWithArgs();
        eventSource.SomeEventWithArgs += EventSource_SomeEventWithArgs;
        eventSource.TriggerEventWithArgs(42);
    }

    static void EventSource_SomeEventWithArgs(object sender, MyEventArgs e)
    {
        Console.WriteLine($"SomeEventWithArgs 发生了,参数值: {e.Value}");
    }
}

class MyEventWithArgs
{
    public event EventHandler<MyEventArgs> SomeEventWithArgs;
    public void TriggerEventWithArgs(int value)
    {
        SomeEventWithArgs?.Invoke(this, new MyEventArgs(value));
    }
}

class MyEventArgs : EventArgs
{
    public int Value { get; set; }
    public MyEventArgs(int value)
    {
        this.Value = value;
    }
}

四、闭包和委托捕获

4.1 TS的闭包

//1、嵌套作用域======================================================================
//以下存在两个嵌套作用域,一个是outerFunction的作用域,一个是innerFunction的作用域
//在innerFunction作用域内,可以访问outerFunction作用域的变量x和y,反之则不行
//类似隐私玻璃,里面可以看见外面,外面不能看见里面
function outerFunction(y) {
  let x = 10;
  function innerFunction() {
    console.log(x,y);
  }
}


//2、实现闭包========================================================================
//作用1:外围作用域可以看到内围作用域
//作用2:函数执行后,函数内部的变量生命周期依然存在
//基本使用就是在嵌套函数的基础上,把内部的函数return出来
function outerFunction() {
  let x = 10;
  function innerFunction() {
    console.log(x);
  }
  return innerFunction; //将内部函数return出来
}

let otherInnerFunction = outerFunction(); //返回内部函数innerFunction
//此时,外部函数已经调用,正常情况下x的生命周期已经结束,但是otherInnerFunction仍然持有着x
//......

//最后,调用otherInnerFunction,输出10
//x的生命周期直到调用otherInnerFunction时才结束
//otherInnerFunction是在全局作用域访问的,但它访问到了内部作用域outerFunction的值
//内部函数innerFunction就像一个工具人,将outerFunction作用域内的值带了出来
otherInnerFunction();

4.2 C#的委托捕获

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
    }

}

五、扩展方法

5.1 C#的扩展方法

C#中扩展方法的应用非常广泛,AspNetCore框架的依赖注入和Linq中,就大量使用了扩展方法。通过扩展类定义新的方法(扩展类就是一个新的类),调用时,直接用原对象调用,就好像这个方法属于原类一样。

class Program
{
    public static void Main()
    {
        var cal = new Cal(2, 4);
        cal.Sum();//结果为6,原类的方法
        cal.Avg();//结果为3,新类的扩展方法
    }
}

//原类
public 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;
    }
}

//扩展类必须是静态类,默认命名来原类+Extension
public static class CalExtension
{
    //扩展方法必须是公开的静态方法
    //第一个参数为原类型,且使用this关键词修饰
    //后面的参数为扩展方法本身需要的参数,和普通方法一样
    public static int Avg(this Cal cal)
    {
        return cal.Sum()/2; //在扩展方法中可以调用原对象的所有成员
    }
}

5.2 TS的扩展方法

5.2.1 使用declare和原型对象

JS的对象是可以随意添加方法的,但TS是强类型语言,不能随意修改类型结构。在TS中实现扩展方法,操作比较繁琐,但原理还是比较简单。基本思路包括两步:(1)使用declare为已存在的类型添加类型申明,这个时候需要使用interface,因为使用interface申明的类型可以合并;(2)在类型的原型对象上,添加方法,关于原型对象及原型链,参见《原型》章节。

//在全局范围内,为String类型,添加一个reverse方法的类型申明
//利用interface申明的类型可以合并的特性
declare global {
  interface String {
    reverse(): string;
  }
}

//在String类型的原型对象上添加reverse方法
String.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

//所有字符串对象,都可以访问原型对象上的方法reverse
"abc".reverse();
5.2.2 使用装饰器和原型对象

还可以通过装饰器扩展方法,详见《装饰器》章节。

六、方法重载

在C#和TS的类中,均可实现方法重载,方法名称相同,但参数的类型、位置或数量不同,即可实现方法重载。调用函数时,根据参数的类型、位置和数量进行匹配,从而实现同一个方法名,调用不同的方法。类中的构造函数,也是方法,所以可以实现构造函数的重载。

//C#的方法重载=======================================================================
class Program
{
    static void Main()
    {
        MyClass myClass = new MyClass();
        myClass.DoSomething(5);
        myClass.DoSomething("Hello");
    }
}

class MyClass
{
    public void DoSomething(int num)
    {
        Console.WriteLine($"执行了参数为整数 {num} 的方法");
    }

    public void DoSomething(string str)
    {
        Console.WriteLine($"执行了参数为字符串 {str} 的方法");
    }
}

//TS的方法重载=======================================================================
class MyClass {
  doSomething(num: number): void {
    console.log(`执行了参数为数字 ${num}`);
  }

  doSomething(str: string): void {
    console.log(`执行了参数为字符串 ${str}`);
  }
}

let myClass = new MyClass();
myClass.doSomething(5);
myClass.doSomething('Hello');

//TS还可以骚操作---------------------------------
class MyClass {
  doSomething(num: number): void;
  doSomething(str: string): void;

  doSomething(param: number | string): void {
    if (typeof param === 'number') {
      console.log(`执行了参数为数字 ${param}`);
    } else if (typeof param ==='string') {
      console.log(`执行了参数为字符串 ${param}`);
    }
  }
}

let myClass = new MyClass();
myClass.doSomething(5);
myClass.doSomething('Hello');