开场白:大家好,我是吉帅振的Wikipedia(其他网络平台帐号英文名字完全相同),网络前端开发技师,工作5年,造访上海和北京,历经创业者公司,重新加入过穆萨邻近地区生活项目组,现在济南丘浚基础教育专门从事程式结构设计专业培训。
一、序言
关于甚么是泛型那个难题不太好提问,比如说在复试中,如果有参选人如此一来问我那个难题,可能我也给不下两个特别国际标准的国际标准答案。但是,他们能转作 Java 中C#的评注来提问那个难题:C#指的是类别模块化,将要原本这种具体内容的类别进行模块化。和表述表达式模块一样,他们能给C#表述二个类别模块,并在初始化时给C#传至明晰的类别模块。结构设计C#的目地是有效率束缚类别核心成员间的亲密关系,比如说表达式模块和codice、类或是USB核心成员和方法间的亲密关系。
二、C#类别模块
C#最常见的情景是用以束缚表达式模块的类别,他们能给表达式表述二个被初始化TNUMBERV12V4会传至明晰类别的模块。比如说下列表述的两个 reflect 表达式,它能转交两个任一类别的模块,并原封不动地回到模块的值和类别,那他们该怎样叙述那个表达式呢?好似想用上 unknown 了(只不过我想说的是 any,因为 any is 撒旦,所以还是用 unknown 吧)。
function reflect(param: unknown){
return param;
}
const str = reflect(string);// str 类别是 unknown
const num = reflect(1);// num 类别 unknown
这时,reflect 表达式虽然能转交两个任一类别的模块并通通地回到模块的值,但是codice类别不合乎他们的预期。因为他们期望codice类别与入参类别相异(比如说 number 对 number、string 对 string),而不是不论入品乐版甚么类别,codice一概是 unknown。这时,C#刚好能满足用户这样的政治理念,那怎样表述两个C#模块呢?具体来说,他们把模块 param 的类别表述为两个(类别微观的)模块、表达式,而不是两个明晰的类别,等到表达式初始化时再传至明晰的类别。比如说他们能通过尖括号<>语法给表达式表述两个C#模块 P,并指定 param 模块的类别为 P ,如下代码所示:
function reflect
(param: P){
return param;
}
这里他们能看到,尖括号中的 P 表示C#模块的表述,param 后的 P 表示模块的类别是C# P(即类别受 P 束缚)。他们也能使用C#显式地注解codice的类别,虽然没有那个必要(因为codice的类别能基于上下文推断出来)。比如说初始化如下所示的 reflect 时,他们能通过尖括号<>语法给C#参数 P 显式地传至两个明晰的类别。
function reflect
(param: P):P {
return param;
}
然后在初始化表达式时,他们也通过<>语法指定了如下所示的 string、number 类别入参,相应地,reflectStr 的类别是 string,reflectNum 的类别是 number。
const reflectStr = reflect(string);// str 类型是 string
const reflectNum = reflect(1);// num 类别 number
另外,如果初始化C#表达式时受C#束缚的模块有传值,C#模块的入参能从模块的类别中进行推断,而无须再显式指定类别(可缺省),因此上边的示例能简写为如下示例:
const reflectStr2= reflect(string);// str 类别是 string
const reflectNum2= reflect(1);// num 类别 number
C#不仅能束缚表达式整个模块的类别,还能束缚模块属性、核心成员的类别,比如说模块的类别能是数组、对象,如下示例:
function reflectArray
(param: P[]){
return param;
}
const reflectArr = reflectArray([1,1]);// reflectArr 是(string number)[]
这里他们束缚了 param 的类别是数组,数组的元素类别是C#入参。通过C#,他们能束缚表达式模块和codice的类别亲密关系。举两个他们比较熟悉的实际情景 React Hooks useState 为例,如下示例中,第2 行 return 的元组(因为 useState 回到的是长度为2、元素类别固定的数组)的第两个元素的类别就是C# S,第二个表达式类别元素的模块类别也是C# S。
function useState(state: S, initialValue?: S){
return [state,(s: S)=> void 0] as unknown as [S,(s: S)=> void];
}
注意:表达式的C#入参必须和模块/模块核心成员建立有效率的束缚亲密关系才有实际意义。比如说在下面示例中,他们表述了两个仅束缚codice类别的C#,它是没有任何意义的。
function uselessGenerics
(): P {
return void 0 as unknown as P;
}
他们能给表达式表述任何个数的C#入参,如下代码所示:
function reflectExtraParams(p1: P, p2: Q):[P, Q]{
return [p1, p2];
}
在上述代码中,他们表述了两个拥有两个C#入参(P 和 Q)的表达式 reflectExtraParams,并通过 P 和 Q 束缚表达式模块 p1、p2和codice的类别。
三、C#类
在类的表述中,他们还能使用C#用以束缚构造表达式、属性、方法的类别,如下代码所示:
class Memory{
store: S;
constructor(store: S){
this.store = store;
}
set(store: S){
this.store = store;
}
get(){
return this.store;
}
}
const numMemory = new Memory(1);//可缺省
const getNumMemory = numMemory.get();//类别是 number
numMemory.set(2);//只能写入 number 类别
const strMemory = new Memory();//缺省
const getStrMemory = strMemory.get();//类别是 string
strMemory.set(string);//只能写入 string 类别
具体来说,他们表述了两个支持读写的寄存器类 Memory,并使用C#束缚了 Memory 类的构造器表达式、set 和 get 方法形参的类别,最后实例化了泛型入参分别是 number 和 string 类别的两种寄存器。C#类和C#表达式类似的地方是,在创建类实例时,如果受C#束缚的模块传至了明晰值,则C#入参(确切地说是传至的类别)可缺省,比如说第14行、第18行,、C#入参就是能缺省的。
小贴士:对于 React 开发者而言,组件也支持C#,如下代码所示。
function GenericCom
(props:{ prop1: string }){
return <>;
};
在第1 行~第3 行,他们表述了两个C#组件 GenericCom,它转交了两个类别入参 P。在第4 行,通过 JSX 语法创建组件元素的同时,他们还显式指定了USB类别{ name: string }作为入参。
注意:这块的语法稍微有些奇怪,他们记住就能了。
四、C#类别
02讲中他们提到,他们能使用 Array<类别>的语法来表述数组类别,这里的 Array 本身就是一种类别。在 TypeScript 中,类别本身就能被表述为拥有不明晰的类别模块的C#,并且能转交明晰类别作为入参,从而衍生出更具体内容的类别,如下代码所示:
const reflectFn:
(param: P)=> P = reflect;// ok
这里他们为表达式 reflectFn 显式添加了C#类别注解,并将 reflect 表达式作为值赋给了它。他们也能把 reflectFn 的类别注解提取为两个能被复用的类别别名或是USB,如下代码所示:
type ReflectFuncton =
(param: P)=> P;
interface IReflectFuncton {
(param: P): P
}
const reflectFn2: ReflectFuncton = reflect;
const reflectFn3: IReflectFuncton = reflect;
将类别入参的表述移动到类别别名或接口名称后,这时表述的两个转交具体内容类别入参后回到两个新类别的类别就是C#类别。如下示例中,他们表述了两个能转交入参 P 的C#类别(GenericReflectFunction 和 IGenericReflectFunction )。
type GenericReflectFunction
= (param: P)=> P;
interface IGenericReflectFunction
{
(param: P): P;
}
const reflectFn4: GenericReflectFunction= reflect;//具象化C#
const reflectFn5: IGenericReflectFunction= reflect;//具象化C#
const reflectFn3Return = reflectFn4(string);//入参和codice都必须是 string 类别
const reflectFn4Return = reflectFn5(1);//入参和codice都必须是 number 类别
在C#表述中,他们甚至能使用一些类别操作符进行运算表达,使得C#能根据入参的类别衍生出各异的类别,如下代码所示:
type StringOrNumberArray= E extends string number ? E[]: E;
type StringArray = StringOrNumberArray;//类别是 string[]
type NumberArray = StringOrNumberArray;//类别是 number[]
type NeverGot = StringOrNumberArray;//类别是 boolean
这里他们表述了两个C#,如果入品乐版 number string 就会生成两个数组类别,否则就生成入参类别。而且,他们还使用了与 JavaScript 三元表达式完全一致的语法来表达类别运算的逻辑亲密关系,15讲中会更详细地介绍。发散一下,如果他们给上面那个C#传至了两个 string boolean 联合类别作为入参,将会得到甚么类别呢?且看如下所示示例:
type BooleanOrString = string boolean;
type WhatIsThis = StringOrNumberArray;//好似应该是 string boolean ?
type BooleanOrStringGot =
BooleanOrString extends string number
? BooleanOrString[]: BooleanOrString;// string boolean
嗯?难道不是 boolean string ?还真不是,如果你使用 VS Code 尝试了那个示例,并 hover 类别别名 WhatIsThis ,那么你会发现显示的类别将是 boolean string[]。BooleanOrStringGot 和 WhatIsThis 这两个类别别名的类别居然不一样,这 TM 是甚么逻辑?那个就是所谓的分配条件类别(Distributive Conditional Types)。
关于分配条件类别那个概念,官方的评注:在条件类别判断的情况下(比如说上边示例中出现的 extends),如果入品乐版联合类别,则会被拆解成两个个独立的(原子)类别(核心成员)进行类别运算。比如说上边示例中的 string boolean 入参,先被拆解成 string 和 boolean 这两个独立类别,再分别判断是否是 string number 类型的子集。因为 string 是子集而 boolean 不是,所以最终他们得到的 WhatIsThis 的类别是 boolean string[]。能接受入参的C#类别和表达式一样,都能对入参类别进行计算并回到新的类别,像是在做类别运算。
利用C#,他们能抽象封装出很多有用、复杂的类别束缚。比如说在 Redux Model 中束缚 State 和 Reducers 的类别表述亲密关系,他们能通过如下所示代码表述了两个既能接受 State 类别入参,又包含 state 和 reducers 这两个属性的USB类别C#,并通过 State 入参束缚了C#的 state 属性和 reducers 属性下 action 索引属性的类别亲密关系。
interface ReduxModel{
state: State,
reducers:{
[action: string]:(state: State, action: any)=> State
}
}
然后根据实际需要,他们传至了两个具体内容的 State 类别具象化 ReduxModel,并束缚了两个实际的 model,如下代码所示:
type ModelInterface ={ id: number; name: string };
const model: ReduxModel= {
state:{ id:1, name:乾元},// ok 类别必须是 ModelInterface
reducers:{
setId:(state, action:{ payload: number })=>({
state,
id: action.payload // ok must be number
}),
setName:(state, action:{ payload: string })=>({
state,
name: action.payload // ok must be string
})
}
}
在上述示例中,model 对象的 state 属性、reducers 属性的 setId、setName 方法的第两个模块 state 的类别都受到 ReduxModel C#入参 ModelInterface 的束缚。
注意:枚举类别不支持C#。
五、C#束缚
前面提到了C#就像是类别的表达式,它能抽象、封装并转交(类别)入参,而C#的入参也拥有类似表达式入参的特性。因此,他们能把C#入参限定在两个相对更明晰的集合内,以便对入参进行束缚。比如说最前边提到的通通回到模块的 reflect 表达式,他们期望把转交模块的类别限定在几种原始类别的集合中,这时就能使用“C#入参名 extends 类别”语法达到那个目地,如下代码所示:
function reflectSpecified
(param: P):P {
return param;
}
reflectSpecified(string);// ok
reflectSpecified(1);// ok
reflectSpecified(true);// ok
reflectSpecified(null);// ts(2345)null不能赋予类别number string boolean
在上述示例中,他们限定了C#入参只能是 number string boolean 的子集。同样,他们也能把USBC#入参束缚在特定的范围内,如下代码所示:
interface ReduxModelSpecified{
state: State
}
type ComputedReduxModel1= ReduxModelSpecified<{ id: number; name: string;}>;// ok
type ComputedReduxModel2= ReduxModelSpecified<{ id: number; name: string; age: number;}>;// ok
type ComputedReduxModel3= ReduxModelSpecified<{ id: string; name: number;}>;// ts(2344)
type ComputedReduxModel4= ReduxModelSpecified<{ id: number;}>;// ts(2344)
在上述示例中,ReduxModelSpecified C#仅转交{ id: number; name: string }USB类别的子类别作为入参。他们还能在多个不同的C#入参间设置束缚亲密关系,如下代码所示:
interface ObjSetter {
(obj: O, key: K, value: V): V;
}
const setValueOfObj: ObjSetter =(obj, key, value)=>(obj[key]= value);
setValueOfObj({ id:1, name:name},id,2);// ok
setValueOfObj({ id:1, name:name},name,new name);// ok
setValueOfObj({ id:1, name:name},age,2);// ts(2345)
setValueOfObj({ id:1, name:name},id,2);// ts(2345)
在设置对象属性值的表达式类别时,它拥有3 个C#入参:第1 个是对象,第2 个是第1 个入参属性名集合的子集,第3 个是指定属性类别的子类别(这里使用了 keyof 操作符,15讲中他们会详细介绍 TypeScript 类别相关的操作符)。另外,C#入参与表达式入参还有两个相似的地方是,它也能给C#入参指定默认值(默认类别),且语法和指定表达式默认模块完全一致,如下代码所示:
interface ReduxModelSpecified2{
state: State
}
type ComputedReduxModel5= ReduxModelSpecified2;// ok
type ComputedReduxModel6= ReduxModelSpecified2<{ id: number; name: string;}>;// ok
type ComputedReduxModel7= ReduxModelSpecified;// ts(2314)缺少两个类别模块
在上述示例中,他们表述了入参有默认类别的C# ReduxModelSpecified2,因此使用 ReduxModelSpecified2时类别入参可缺省。而 ReduxModelSpecified 的入参没有默认值,所以缺省入参时会提示两个类别错误。C#入参的束缚与默认值还能组合使用,如下代码所示:
interface ReduxModelMixed{
state: State
}
这里他们限定了C# ReduxModelMixed 入参 State 必须是{}类别的子类别,同时也指定了入参缺省时的默认类别是USB类别{ id: number; name: string;}。
五、总结
这一讲关于C#的知识点看起来不是太多,但是难点是怎样理解C#。他们能试着将C#理解为类别中的表达式,并通过抽象、封装类别运算逻辑实现类别可复用,以便更好地掌握C#。