自学内容网 自学内容网

C++20中的Concepts与TypeScript

C++20中的Concepts与TypeScript

大家好!上一篇聊了C++20中概念(Concepts),这是一个非常赞的特性,极大简化了模板编程,但是如果跳出C++去查看一下其他编程语言的特性,就会发现,这样类似的特性其实早就已经有了。语言是相通的,相互借鉴的,所以呈现出越来越像的趋势。

  • Java:通过泛型(Generics)和接口(Interfaces),以及extends关键字进行类型约束。
  • C#:通过泛型(Generics)和接口(Interfaces),以及where关键字进行类型约束。
  • TypeScript:通过泛型(Generics)和extends关键字进行类型约束。

这些语言特性都可以用于定义和约束泛型参数,确保类型安全,同时提高代码的可读性和可维护性。每个语言有其独特的语法和实现方式,但在本质上,它们都为开发者提供了强大的类型检查和约束能力。

今天我们来聊聊在TypeScript中如何实现类似C++20中概念(Concepts)的功能。虽然TypeScript没有直接等同于C++20的Concepts,但我们可以通过泛型(Generics)和类型守卫(Type Guards)来实现类似的类型约束(Type Constraints。接下来,我们通过几个具体的例子来展示如何在TypeScript中实现这些约束。

c++-concept-ts

什么是泛型?

泛型这个概念,其实挺简单,就是让我们的函数、接口或者类能够处理多种类型的数据,而不是被限制在一种特定类型上。可以这么理解,就是你写了一个方法,它能够根据你传入的数据类型,自己“变”成去处理这种类型的方法。

举个例子,假如我们有一个函数,它的作用是返回传进去的参数。传统的写法,你可能写:

function identity(arg: number): number {
  return arg;
}

但这样我们只能处理 number 类型的数据。那我们要处理 string 类型呢?要再写一个函数吗?当然不是,我们可以用泛型来解决:

function identity<T>(arg: T): T {
  return arg;
}

看到了吧,我们给函数名后面加了 <T>,T 代表一种类型,这个类型是传进函数的时候才决定的。所以我们可以这样用:

console.log(identity<number>(123)); // 输出 123
console.log(identity<string>("Hello")); // 输出 Hello

泛型如何使用?

泛型不仅可以用在函数上,还可以用在接口和类上。

泛型接口

有时候,我们希望用接口来定义某个数据类型的集合,比如:

interface GenericIdentityFn<T> {
  (arg: T): T;
}

let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(123)); // 输出 123

泛型类

我们也可以把泛型用在类上:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
console.log(myGenericNumber.add(1, 2)); // 输出 3

类型约束

有时候,我们希望泛型不仅仅是任意一种类型,而是某种特定类型的子类型,这时候我们就要用到类型约束了。举个例子,我们希望泛型参数一定要有 length 属性:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 这里可以访问 length 属性,因为我们约束了 T 必须有 length
  return arg;
}

loggingIdentity({length: 10, value: "Hello"}); // 没问题
// loggingIdentity(123); // 报错:因为 number 类型没有 length 属性

这里我们用 extends 关键字约束 T 必须有 length 属性,所以传入的参数必须是一个带有 length 属性的对象。

泛型如何实现类似于C++20中的概念

让我们来看一下如何在TypeScript中对泛型进行约束,以确保它们满足特定的条件,类似于C++20中的概念。

示例1:检查类型是否支持+运算符

由于TypeScript不支持运算符重载,我们可以通过接口来确保给定的类型具有特定的方法,从而模拟我们需要的行为。

interface Addable {
  valueOf(): number;
}

function add<T extends Addable>(a: T, b: T): number {
  return a.valueOf() + b.valueOf();
}

console.log(add(1, 2));         // 正常:3
console.log(add(new Number(1.5), new Number(2.5)));  // 正常:4

在这个例子中:

  • Addable接口确保类型必须有一个valueOf方法并返回一个数字。
  • add函数使用泛型并限制为T extends Addable

但是对于如下代码,

console.log(add("a", "b"));  

将会产生这样的编译错误,应该还是非常清晰和容易理解的。

error TS2345: Argument of type '"a"' is not assignable to parameter of type 'Addable'.

示例2:检查类型是否具有length属性

我们创建一个函数,检查类型是否有length属性并且是可迭代的:

interface HasLength {
  length: number;
}

function printLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

printLength("Hello");  // 正常:5
printLength([1, 2, 3, 4]);  // 正常:4
// 

在这个例子中:

  • HasLength接口确保类型必须有一个length属性。
  • printLength函数使用泛型并限制为扩展自HasLength的类型。

但是对于如下代码,

printLength(123); 

将会产生这样的编译错误

error TS2345: Argument of type '123' is not assignable to parameter of type 'HasLength'.

示例3:类型守卫检查可迭代性

TypeScript的类型守卫允许你创建在运行时执行检查的函数,从而缩小类型范围。这些检查能提供类似Concepts的高级验证。为了确保一个类型是可迭代的,我们可以创建一个类型守卫函数:

type IterableType = {
  [Symbol.iterator](): Iterator<any>;
}

function isIterable<T>(obj: T): obj is T & IterableType {
  return typeof (obj as any)[Symbol.iterator] === 'function';
}

function printAll<T>(container: T): void {
  if (isIterable(container)) {
    for (const item of container) {
      console.log(item);
    }
  } else {
    console.log("不可迭代");
  }
}

printAll([1, 2, 3]);  // 正常:1 2 3
printAll("Hello");  // 正常:H e l l o
// printAll(123);  // 正常:不可迭代

在这个例子中:

  • isIterable函数检查一个对象是否具有[Symbol.iterator]方法,从而判断其可迭代性。
  • printAll函数仅在对象可迭代时执行迭代操作。

示例4:组合多种约束

我们可以使用类型守卫组合多种约束,确保类型满足多个条件:

interface HasBegin {
  begin(): void;
}

interface HasEnd {
  end(): void;
}

type CompleteType = HasBegin & HasEnd;

function hasBegin<T>(obj: T): obj is T & HasBegin {
  return typeof (obj as any).begin === 'function';
}

function hasEnd<T>(obj: T): obj is T & HasEnd {
  return typeof (obj as any).end === 'function';
}

function process<T>(obj: T): void {
  if (hasBegin(obj) && hasEnd(obj)) {
    obj.begin();
    obj.end();
  } else {
    console.log("对象不符合要求");
  }
}

const validObject = {
  begin: () => console.log("Begin"),
  end: () => console.log("End")
};

const invalidObject = {
  begin: () => console.log("Begin")
};

process(validObject);  // 正常:Begin End
process(invalidObject);  // 正常:对象不符合要求

在这个例子中:

  • CompleteType表示具有beginend方法的类型。

  • process函数使用类型守卫hasBeginhasEnd确保对象满足要求。

总结

C++-is-a-general-purpose-programming-language

虽然TypeScript没有完全等同于C++20的Concepts,但通过TypeScript的类型系统、泛型和类型约束,可以实现类似的功能。定义接口、使用类型约束,我们可以确保传递给函数的类型满足特定的条件,从而使代码更加健壮和类型安全。

希望这些示例能帮助你更好地理解如何在TypeScript中实现类似C++20 Concepts的功能,同时,又可以让你对C++20 Concepts有更深入的理解。如果有任何问题或者需要进一步的帮助,欢迎随时提问!


原文地址:https://blog.csdn.net/2404_88048702/article/details/143844835

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!