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