16 使用类型保护让TS更聪明

这个小节我们来学习类型保护,在学习前面知识的时候我们有遇到过需要告诉编译器某个值是指定类型的场景,当时我们使用的是类型断言,这一节我们来看一个不同的场景: const valueList = [123, "abc"]; const getRandomValue = () => { const number = Math.random() * 10; // 这里取一个[0, 10)范围内的随机值 if (number < 5) return valueList[0]; // 如果随机数小于5则返回valueList里的第一个值,也就是123 else return valueList[1]; // 否则返回"abc" }; const item = getRandomValue(); if (item.length) { // error 类型“number”上不存在属性“length” console.log(item.length); // error 类型“number”上不存在属性“length” } else { console.log(item.toFixed()); // error 类型“string”上不存在属性“toFixed” } 上面这个例子中,getRandomValue 函数返回的元素是不固定的,有时返回数值类型,有时返回字符串类型。我们使用这个函数生成一个值 item,然后接下来的逻辑是通过是否有 length 属性来判断是字符串类型,如果没有 length 属性则为数值类型。在 js 中,这段逻辑是没问题的,但是在 TS 中,因为 TS 在编译阶段是无法知道 item 的类型的,所以当我们在 if 判断逻辑中访问 item 的 length 属性的时候就会报错,因为如果 item 为 number 类型的话是没有 length 属性的。...

March 13, 2024 · zlzong

17 使用显式复制断言给TS一个你一定会赋值的承诺

在讲解本小节的主要内容之前,我们先来补充两个关于null和undefined的知识点: (1) 严格模式下null和undefined赋值给其它类型值 当我们在 tsconfig.json 中将 strictNullChecks 设为 true 后,就不能再将 undefined 和 null 赋值给除它们自身和void 之外的任意类型值了,但有时我们确实需要给一个其它类型的值设置初始值为空,然后再进行赋值,这时我们可以自己使用联合类型来实现 null 或 undefined 赋值给其它类型: let str = "lison"; str = null; // error 不能将类型“null”分配给类型“string” let strNull: string | null = "lison"; // 这里你可以简单理解为,string | null即表示既可以是string类型也可以是null类型 strNull = null; // right strNull = undefined; // error 不能将类型“undefined”分配给类型“string | null” 注意,TS 会将 undefined 和 null 区别对待,这和 JS 的本意也是一致的,所以在 TS 中,string|undefined、string|null和string|undefined|null是三种不同的类型。 (2) 可选参数和可选属性 如果开启了 strictNullChecks,可选参数会被自动加上|undefined,来看例子: const sum = (x: number, y?...

March 13, 2024 · zlzong

17 函数式编程:一种越来越流行的编程范式

上一讲我们初步介绍了函数对象和 lambda 表达式,今天我们来讲讲它们的主要用途——函数式编程。 一个小例子 按惯例,我们还是从一个例子开始。想一下,如果给定一组文件名,要求数一下文件里的总文本行数,你会怎么做? 我们先规定一下函数的原型: int count_lines(const char** begin,const char** end); 也就是说,我们期待接受两个 C 字符串的迭代器,用来遍历所有的文件名;返回值代表文件中的总行数。 要测试行为是否正常,我们需要一个很小的 main 函数: int main(int argc, const char** argv) { int total_lines = count_lines(argv + 1, argv + argc); cout << "Total lines: "<< total_lines << endl; } 最传统的命令式编程大概会这样写代码: int count_file(const char* name) { int count = 0; ifstream ifs(name); string line; for (;;) { getline(ifs, line); if (!ifs) { break; } ++count; } return count; } int count_lines(const char** begin, const char** end) { int count = 0; for (; begin !...

March 13, 2024 · zlzong

18 类型别名和字面量类型—单调的类型

本小节我们来学习类型别名和字面量类型。类型别名我们之前在讲泛型的时候接触过,现在来详细学习下。 3.5.1 类型别名 类型别名就是给一种类型起个别的名字,之后只要使用这个类型的地方,都可以用这个名字作为类型代替,但是它只是起了一个名字,并不是创建了一个新类型。这种感觉就像 JS 中对象的赋值,你可以把一个对象赋给一个变量,使用这个对象的地方都可以用这个变量代替,但你并不是创建了一个新对象,而是通过引用来使用这个对象。 我们来看下怎么定义类型别名,使用 type 关键字: type TypeString = string; let str: TypeString; str = 123; // error Type '123' is not assignable to type 'string' 类型别名也可以使用泛型,来看例子: type PositionType<T> = { x: T; y: T }; const position1: PositionType<number> = { x: 1, y: -1 }; const position2: PositionType<string> = { x: "right", y: "top" }; 使用类型别名时也可以在属性中引用自己: type Child<T> = { current: T; child?: Child<T>; }; let ccc: Child<string> = { current: "first", child: { // error current: "second", child: { current: "third", child: "test" // 这个地方不符合type,造成最外层child处报错 } } }; 但是要注意,只可以在对象属性中引用类型别名自己,不能直接使用,比如下面这样是不对的:...

March 13, 2024 · zlzong

19 thread和future:领略异步中的未来

编译期的烧脑我们先告个段落,今天我们开始讲一个全新的话题——并发(concurrency)。 为什么要使用并发编程? 在本世纪初之前,大部分开发人员不常需要关心并发编程;用到的时候,也多半只是在单处理器上执行一些后台任务而已。只有少数为昂贵的工作站或服务器进行开发的程序员,才会需要为并发性能而烦恼。原因无他,程序员们享受着摩尔定律带来的免费性能提升,而高速的 Intel 单 CPU 是性价比最高的系统架构,可到了 2003 年左右,大家骤然发现,“免费午餐”已经结束了 [1]。主频的提升停滞了:在 2001 年,Intel 已经有了主频 2.0 GHz 的 CPU,而 18 年后,我现在正在使用的电脑,主频也仍然只是 2.5 GHz,虽然从单核变成了四核。服务器、台式机、笔记本、移动设备的处理器都转向了多核,计算要求则从单线程变成了多线程甚至异构——不仅要使用 CPU,还得使用 GPU。 如果你不熟悉进程和线程的话,我们就先来简单介绍一下它们的关系。我们编译完执行的 C++ 程序,那在操作系统看来就是一个进程了。而每个进程里可以有一个或多个线程: 每个进程有自己的独立地址空间,不与其他进程分享;一个进程里可以有多个线程,彼此共享同一个地址空间。 堆内存、文件、套接字等资源都归进程管理,同一个进程里的多个线程可以共享使用。每个进程占用的内存和其他资源,会在进程退出或被杀死时返回给操作系统。 并发应用开发可以用多进程或多线程的方式。多线程由于可以共享资源,效率较高;反之,多进程(默认)不共享地址空间和资源,开发较为麻烦,在需要共享数据时效率也较低。但多进程安全性较好,在某一个进程出问题时,其他进程一般不受影响;而在多线程的情况下,一个线程执行了非法操作会导致整个进程退出。 我们讲 C++ 里的并发,主要讲的就是多线程。它对开发人员的挑战是全方位的。从纯逻辑的角度,并发的思维模式就比单线程更为困难。在其之上,我们还得加上: 编译器和处理器的重排问题 原子操作和数据竞争 互斥锁和死锁问题 无锁算法 条件变量 信号量 …… 即使对于专家,并发编程都是困难的,上面列举的也只是部分难点而已。对于并发的基本挑战,Herb Sutter 在他的 Effective Concurrency 专栏给出了一个较为全面的概述 [2]。要对 C++ 的并发编程有全面的了解,则可以阅读曼宁出版的 C++ Concurrency in Action(有中文版,但翻译口碑不好)[3]。而我们今天主要要介绍的,则是并发编程的基本概念,包括传统的多线程开发,以及高层抽象 future(姑且译为未来量)的用法。 基于 thread 的多线程开发 我们先来看一个使用 thread 线程类 [4] 的简单例子: #include <chrono> #include <iostream> #include <mutex> #include <thread> using namespace std; mutex output_lock; void func(const char* name){ this_thread::sleep_for(100ms); lock_guard<mutex> guard{output_lock}; cout << "I am thread " << name << '\n'; } int main() { thread t1{func, "A"}; thread t2{func, "B"}; t1....

March 13, 2024 · zlzong

19 使用可辨识联合并保证每个case都被处理

我们可以把单例类型、联合类型、类型保护和类型别名这几种类型进行合并,来创建一个叫做可辨识联合的高级类型,它也可称作标签联合或代数数据类型。 所谓单例类型,你可以理解为符合单例模式的数据类型,比如枚举成员类型,字面量类型。 可辨识联合要求具有两个要素: 具有普通的单例类型属性(这个要作为辨识的特征,也是重要因素)。 一个类型别名,包含了那些类型的联合(即把几个类型封装为联合类型,并起一个别名)。 来看例子: interface Square { kind: "square"; // 这个就是具有辨识性的属性 size: number; } interface Rectangle { kind: "rectangle"; // 这个就是具有辨识性的属性 height: number; width: number; } interface Circle { kind: "circle"; // 这个就是具有辨识性的属性 radius: number; } type Shape = Square | Rectangle | Circle; // 这里使用三个接口组成一个联合类型,并赋给一个别名Shape,组成了一个可辨识联合。 function getArea(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math....

March 13, 2024 · zlzong

20 this,类型?

在 JavaScript 中,this 可以用来获取对全局对象、类实例对象、构建函数实例等的引用,在 TypeScript 中,this 也是一种类型,我们先来看个计算器 Counter 的例子: class Counter { constructor(public count: number = 0) {} add(value: number) { // 定义一个相加操作的方法 this.count += value; return this; } subtract(value: number) { // 定义一个相减操作的方法 this.count -= value; return this; } } let counter = new Counter(10); console.log(counter.count); // 10 counter.add(5).subtract(2); console.log(counter.count); // 13 我们给 Counter 类定义几个方法,每个方法都返回 this,这个 this 即指向实例,这样我们就可以通过链式调用的形式来使用这些方法。这个是没有问题的,但是如果我们要通过类继承的形式丰富这个 Counter 类,添加一些方法,依然返回 this,然后采用链式调用的形式调用,在过去版本的 TypeScript 中是有问题的,先来看我们继承的逻辑: class PowCounter extends Counter { constructor(public count: number = 0) { super(count); } pow(value: number) { // 定义一个幂运算操作的方法 this....

March 13, 2024 · zlzong

20 内存模型和atomic:理解并发的复杂性

上一讲我们讨论了一些并发编程的基本概念,今天我们来讨论一个略有点绕的问题,C++ 里的内存模型和原子量。 C++98 的执行顺序问题 C++98 的年代里,开发者们已经了解了线程的概念,但 C++ 的标准里则完全没有提到线程。从实践上,估计大家觉得不提线程,C++ 也一样能实现多线程的应用程序吧。不过,很多聪明人都忽略了,下面的事实可能会产生不符合直觉预期的结果: 为了优化的必要,编译器是可以调整代码的执行顺序的。唯一的要求是,程序的“可观测”外部行为是一致的。 处理器也会对代码的执行顺序进行调整(所谓的 CPU 乱序执行)。在单处理器的情况下,这种乱序无法被程序观察到;但在多处理器的情况下,在另外一个处理器上运行的另一个线程就可能会察觉到这种不同顺序的后果了。 对于上面的后一点,大部分开发者并没有意识到。原因有好几个方面: 多处理器的系统在那时还不常见 主流的 x86 体系架构仍保持着较严格的内存访问顺序 只有在数据竞争(data race)激烈的情况下才能看到“意外”的后果 举一个例子,假设我们有两个全局变量: int x = 0; int y = 0; 然后我们在一个线程里执行: x = 1; y = 2; 在另一个线程里执行: if (y == 2) { x = 3; y = 4; } 想一下,你认为上面的代码运行完之后,x、y 的数值有几种可能? 你如果认为有两种可能,1、2 和 3、4 的话,那说明你是按典型程序员的思维模式看问题的——没有像编译器和处理器一样处理问题。事实上,1、4 也是一种结果的可能。有两个基本的原因可以造成这一后果: 编译器没有义务一定按代码里给出的顺序产生代码。事实上,跟据上下文调整代码的执行顺序,使其最有利于处理器的架构,是优化中很重要的一步。就单个线程而言,先执行 x = 1 还是先执行 y = 2 完全是件无关紧要的事:它们没有外部“可观察”的区别。 在多处理器架构中,各个处理器可能存在缓存不一致性问题。取决于具体的处理器类型、缓存策略和变量地址,对变量 y 的写入有可能先反映到主内存中去。之所以这个问题似乎并不常见,是因为常见的 x86 和 x86-64 处理器是在顺序执行方面做得最保守的——大部分其他处理器,如 ARM、DEC Alpha、PA-RISC、IBM Power、IBM z/ 架构和 Intel Itanium 在内存序问题上都比较“松散”。x86 使用的内存模型基本提供了顺序一致性(sequential consistency);相对的,ARM 使用的内存模型就只是松散一致性(relaxed consistency)。较为严格的描述,请查看参考资料 [1] 和里面提供的进一步资料。...

March 13, 2024 · zlzong

21 索引类型:获取索引类型和索引值类型

我们这里要讲的,可不是前面讲接口的时候讲的索引类型。在学习接口内容的时候,我们讲过可以指定索引的类型。而本小节我们讲的索引类型包含两个内容:索引类型查询和索引访问操作符。 3.8.1 索引类型查询操作符 keyof操作符,连接一个类型,会返回一个由这个类型的所有属性名组成的联合类型。来看例子: interface Info { name: string; age: number; } let infoProp: keyof Info; infoProp = "name"; infoProp = "age"; infoProp = "no"; // error 不能将类型“"no"”分配给类型“"name" | "age"” 通过例子可以看到,这里的keyof Info其实相当于"name" | “age”。通过和泛型结合使用,TS 就可以检查使用了动态属性名的代码: function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] { // 这里使用泛型,并且约束泛型变量K的类型是"keyof T",也就是类型T的所有字段名组成的联合类型 return names.map(n => obj[n]); // 指定getValue的返回值类型为T[K][],即类型为T的值的属性值组成的数组 } const info = { name: "lison", age: 18 }; let values: string[] = getValue(info, ["name"]); values = getValue(info, ["age"]); // error 不能将类型“number[]”分配给类型“string[]” 3....

March 13, 2024 · zlzong

22 使用映射类型得到新的类型

3.9.1 基础 TS 提供了借助旧类型创建一个新类型的方式,也就是映射类型,它可以用相同的形式去转换旧类型中每个属性。来看个例子: interface Info { age: number; } 我们可以使用这个接口实现一个有且仅有一个 age 属性的对象,但如果我们想再创建一个只读版本的同款对象,那我们可能需要再重新定义一个接口,然后让 age 属性 readonly。如果接口就这么简单,你确实可以这么做,但是如果属性多了,而且这个结构以后会变,那就比较麻烦了。这种情况我们可以使用映射类型,下面来看例子: interface Info { age: number; } type ReadonlyType<T> = { readonly [P in keyof T]: T[P] }; // 这里定义了一个ReadonlyType<T>映射类型 type ReadonlyInfo = ReadonlyType<Info>; let info: ReadonlyInfo = { age: 18 }; info.age = 28; // error Cannot assign to 'age' because it is a constant or a read-only property 这个例子展示了如何通过一个普通的接口创建一个每个属性都只读的接口,这个过程有点像定义了一个函数,这个函数会遍历传入对象的每个属性并做处理。同理你也可以创建一个每个属性都是可选属性的接口: interface Info { age: number; } type ReadonlyType<T> = { readonly [P in keyof T]?...

March 13, 2024 · zlzong