06 异常:用还是不用,这是个问题

到现在为止,我们已经有好多次都提到异常了。今天,我们就来彻底地聊一聊异常。 首先,开宗明义,如果你不知道到底该不该用异常的话,那答案就是该用。如果你需要避免使用异常,原因必须是你有明确的需要避免使用异常的理由。 下面我们就开始说说异常。 没有异常的世界 我们先来看看没有异常的世界是什么样子的。最典型的情况就是 C 了。 假设我们要做一些矩阵的操作,定义了下面这个矩阵的数据结构: typedef struct { float* data; size_t nrows; size_t ncols; } matrix; 我们至少需要有初始化和清理的代码: enum matrix_err_code { MATRIX_SUCCESS, MATRIX_ERR_MEMORY_INSUFFICIENT, … }; int matrix_alloc(matrix* ptr, size_t nrows, size_t ncols) { size_t size = nrows * ncols * sizeof(float); float* data = malloc(size); if (data == NULL) { return MATRIX_ERR_MEMORY_INSUFFICIENT; } ptr->data = data; ptr->nrows = nrows; ptr->ncols = ncols; } void matrix_dealloc(matrix* ptr) { if (ptr->data == NULL) { return; } free(ptr->data); ptr->data = NULL; ptr->nrows = 0; ptr->ncols = 0; } 然后,我们做一下矩阵乘法吧。函数定义大概会是这个样子:...

March 13, 2024 · zlzong

06 Symbol-ES6新基础类型

symbol是 ES6 新增的一种基本数据类型,它和 number、string、boolean、undefined 和 null 是同类型的,object 是引用类型。它用来表示独一无二的值,通过 Symbol 函数生成。 本小节代码都是纯JavaScript代码,建议在非TypeScript环境练习,你可以在浏览器开发者工具的控制台里练习。但是因为TypeScript也支持Symbol,所以如果需要特别说明的地方,我们会提示在TypeScript中需要注意的内容。 我们先来看例子: const s = Symbol(); typeof s; // 'symbol' 我们使用Symbol函数生成了一个 symbol 类型的值 s。 注意:Symbol 前面不能加new关键字,直接调用即可创建一个独一无二的 symbol 类型的值。 我们可以在使用 Symbol 方法创建 symbol 类型值的时候传入一个参数,这个参数需要是字符串的。如果传入的参数不是字符串,会先调用传入参数的 toString 方法转为字符串。先来看例子: const s1 = Symbol("lison"); const s2 = Symbol("lison"); console.log(s1 === s2); // false // 补充:这里第三行代码可能会报一个错误:This condition will always return 'false' since the types 'unique symbol' and 'unique symbol' have no overlap. // 这是因为编译器检测到这里的s1 === s2始终是false,所以编译器提醒你这代码写的多余,建议你优化。 上面这个例子中使用 Symbol 方法创建了两个 symbol 值,方法中都传入了相同的字符串’lison’,但是s1 === s2却是 false,这就是我们说的,Symbol 方法会返回一个独一无二的值,这个值和任何一个值都不等,虽然我们传入的标识字符串都是"lison",但是确实两个不同的值。...

March 13, 2024 · zlzong

07 迭代器和好用的新for循环

我们已经讲过了容器。在使用容器的过程中,你也应该对迭代器(iterator)或多或少有了些了解。今天,我们就来系统地讲一下迭代器。 什么是迭代器? 迭代器是一个很通用的概念,并不是一个特定的类型。它实际上是一组对类型的要求([1])。它的最基本要求就是从一个端点出发,下一步、下一步地到达另一个端点。按照一般的中文习惯,也许“遍历”是比“迭代”更好的用词。我们可以遍历一个字符串的字符,遍历一个文件的内容,遍历目录里的所有文件,等等。这些都可以用迭代器来表达。 我在用 output_container.h 输出容器内容的时候,实际上就对容器的 begin 和 end 成员函数返回的对象类型提出了要求。假设前者返回的类型是 I,后者返回的类型是 S,这些要求是: I 对象支持 * 操作,解引用取得容器内的某个对象。 I 对象支持 ++,指向下一个对象。 I 对象可以和 I 或 S 对象进行相等比较,判断是否遍历到了特定位置(在 S 的情况下是是否结束了遍历)。 注意在 C++17 之前,begin 和 end 返回的类型 I 和 S 必须是相同的。从 C++17 开始,I 和 S 可以是不同的类型。这带来了更大的灵活性和更多的优化可能性。 上面的类型 I,多多少少就是一个满足输入迭代器(input iterator)的类型了。不过,output_container.h 只使用了前置 ++,但输入迭代器要求前置和后置 ++ 都得到支持。 输入迭代器不要求对同一迭代器可以多次使用 * 运算符,也不要求可以保存迭代器来重新遍历对象,换句话说,只要求可以单次访问。如果取消这些限制、允许多次访问的话,那迭代器同时满足了前向迭代器(forward iterator)。 一个前向迭代器的类型,如果同时支持 --(前置及后置),回到前一个对象,那它就是个双向迭代器(bidirectional iterator)。也就是说,可以正向遍历,也可以反向遍历。 一个双向迭代器,如果额外支持在整数类型上的 +、-、+=、-=,跳跃式地移动迭代器;支持 [],数组式的下标访问;支持迭代器的大小比较(之前只要求相等比较);那它就是个随机访问迭代器(random-access iterator)。 一个随机访问迭代器 i 和一个整数 n,在 *i 可解引用且 i + n 是合法迭代器的前提下,如果额外还满足 *(addressdof(*i) + n) 等价于 *(i + n),即保证迭代器指向的对象在内存里是连续存放的,那它(在 C++20 里)就是个连续迭代器(contiguous iterator)。...

March 13, 2024 · zlzong

07 深入学习枚举

枚举是 TypeScript 新增加的一种数据类型,这在其他很多语言中很常见,但是 JavaScript 却没有。使用枚举,我们可以给一些难以理解的常量赋予一组具有意义的直观的名字,使其更为直观,你可以理解枚举就是一个字典。枚举使用 enum 关键字定义,TypeScript 支持数字和字符串的枚举。 2.4.1. 数字枚举 我们先来通过数字枚举的简单例子,来看下枚举是做什么的: enum Status {// 这里你的TSLint可能会报一个:枚举声明只能与命名空间或其他枚举声明合并。这样的错误,这个不影响编译,声明合并的问题我们在后面的小节会讲。 Uploading, Success, Failed } console.log(Status.Uploading); // 0 console.log(Status["Success"]); // 1 console.log(Status.Failed); // 2 我们使用enum关键字定义了一个枚举值 Status,它包含三个字段,每个字段间用逗号隔开。我们使用枚举值的元素值时,就像访问对象的属性一样,你可以使用’.‘操作符和’[]‘两种形式访问里面的值,这和对象一样。 再来看输出的结果,Status.Uploading 是 0,Status['Success']是 1,Status.Failed 是 2,我们在定义枚举 Status 的时候,并没有指定索引号,是因为这是默认的编号,我们也可以自己指定: // 修改起始编号 enum Color { Red = 2, Blue, Yellow } console.log(Color.Red, Color.Blue, Color.Yellow); // 2 3 4 // 指定任意字段的索引值 enum Status { Success = 200, NotFound = 404, Error = 500 } console....

March 13, 2024 · zlzong

08 易用性改进 I:自动类型推断和初始化

在之前的几讲里,我们已经多多少少接触到了一些 C++11 以来增加的新特性。下面的两讲,我会重点讲一下现代 C++(C++11/14/17)带来的易用性改进。 就像我们 [开篇词] 中说的,我们主要是介绍 C++ 里好用的特性,而非让你死记规则。因此,这里讲到的内容,有时是一种简化的说法。对于日常使用,本讲介绍的应该能满足大部分的需求。对于复杂用法和边角情况,你可能还是需要查阅参考资料里的明细规则。 自动类型推断 如果要挑选 C++11 带来的最重大改变的话,自动类型推断肯定排名前三。如果只看易用性或表达能力的改进的话,那它就是“舍我其谁”的第一了。 auto 自动类型推断,顾名思义,就是编译器能够根据表达式的类型,自动决定变量的类型(从 C++14 开始,还有函数的返回类型),不再需要程序员手工声明([1])。但需要说明的是,auto 并没有改变 C++ 是静态类型语言这一事实——使用 auto 的变量(或函数返回值)的类型仍然是编译时就确定了,只不过编译器能自动帮你填充而已。 自动类型推断使得像下面这样累赘的表达式成为历史: // vector<int> v; for (vector<int>::iterator it = v.begin(), end = v.end(); it != end; ++it) { // 循环体 } 现在我们可以直接写(当然,是不使用基于范围的 for 循环的情况): for (auto it = v.begin(), end = v.end(); it != end; ++it) { // 循环体 } 不使用自动类型推断时,如果容器类型未知的话,我们还需要加上 typename(注意此处 const 引用还要求我们写 const_iterator 作为迭代器的类型): template <typename T> void foo(const T& container) { for (typename T::const_iterator it = v....

March 13, 2024 · zlzong

08 使用类型断言达到预期

学完前面的小节,你已经学习完了TypeScript的基本类型。从本小节开始,你将开始接触逻辑。在这之前,先来学习一个概念:类型断言。 虽然 TypeScript 很强大,但有时它还是不如我们了解一个值的类型,这时候我们更希望 TypeScript 不要帮我们进行类型检查,而是交给我们自己来,所以就用到了类型断言。类型断言有点像是一种类型转换,它把某个值强行指定为特定类型,我们先看个例子: const getLength = target => { if (target.length) { return target.length; } else { return target.toString().length; } }; 这个函数能够接收一个参数,并返回它的长度,我们可以传入字符串、数组或数值等类型的值。如果有 length 属性,说明参数是数组或字符串类型,如果是数值类型是没有 length 属性的,所以需要把数值类型转为字符串然后再获取 length 值。现在我们限定传入的值只能是字符串或数值类型的值: const getLength = (target: string | number): number => { if (target.length) { // error 报错信息看下方 return target.length; // error 报错信息看下方 } else { return target.toString().length; } }; 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错: // 类型"string | number"上不存在属性"length" // 类型"number"上不存在属性"length" 很显然,我们是要做判断的,我们判断如果 target.length 不为 undefined, 说明它是有 length 属性的,但我们的参数是string | number联合类型,所以在我们开始做判断的时候就会报错。这个时候就要用类型断言,将tagrget的类型断言成string类型。它有两种写法,一种是<type>value,一种是value as type,下面例子中我们用两种形式都写出来:...

March 13, 2024 · zlzong

09 易用性改进 II:字面量、静态断言和成员函数说明符

本讲我们继续易用性的话题,看看现代 C++ 带来的其他易用性改进。 自定义字面量 字面量(literal)是指在源代码中写出的固定常量,它们在 C++98 里只能是原生类型,如: "hello",字符串字面量,类型是 const char[6] 1,整数字面量,类型是 int 0.0,浮点数字面量,类型是 double 3.14f,浮点数字面量,类型是 float 123456789ul,无符号长整数字面量,类型是 unsigned long C++11 引入了自定义字面量,可以使用 operator"" 后缀,来将用户提供的字面量转换成实际的类型。C++14 则在标准库中加入了不少标准字面量。下面这个程序展示了它们的用法: #include <chrono> #include <complex> #include <iostream> #include <string> #include <thread> using namespace std; int main() { cout << "i * i = " << 1i * 1i << endl; cout << "Waiting for 500ms" << endl; this_thread::sleep_for(500ms); cout << "Hello world"s.substr(0, 5) << endl; } 输出是:...

March 13, 2024 · zlzong

09 使用接口定义几乎任意结构

本小节我们来学习接口,正如题目所说的,你可以使用接口定义几乎任意结构,本小节我们先来学习下接口的基本使用方法。 2.6.1. 基本用法 我们需要定义这样一个函数,参数是一个对象,里面包含两个字段:firstName 和 lastName,也就是英文的名和姓,然后返回一个拼接后的完整名字。来看下函数的定义: // 注:这段代码为纯JavaScript代码,请在JavaScript开发环境编写下面代码,在TypeScript环境会报一些类型错误 const getFullName = ({ firstName, lastName }) => { return `${firstName} ${lastName}`; }; 使用时传入参数: getFullName({ firstName: "Lison", lastName: "Li" }); // => 'Lison Li' 没有问题,我们得到了拼接后的完整名字,但是使用这个函数的人如果传入一些不是很理想的参数时,就会导致各种结果: getFullName(); // Uncaught TypeError: Cannot destructure property `a` of 'undefined' or 'null'. getFullName({ age: 18, phone: "13312345678" }); // 'undefined undefined' getFullName({ firstName: "Lison" }); // 'Lison undefined' 这些都是我们不想要的,在开发时难免会传入错误的参数,所以 TypeScript 能够帮我们在编译阶段就检测到这些错误。我们来完善下这个函数的定义: const getFullName = ({ firstName, lastName, }: { // 指定这个参数的类型,因为他是一个对象,所以这里来指定对象中每个字段的类型 firstName: string; // 指定属性名为firstName和lastName的字段的属性值必须为string类型 lastName: string; }) => { return `${firstName} ${lastName}`; }; 我们通过对象字面量的形式去限定我们传入的这个对象的结构,现在再来看下之前的调用会出现什么提示:...

March 13, 2024 · zlzong

10 接口的高阶用法

学习了上个小节接口的基础用法后,相信你已经能够使用接口来描述一些结构了。本小节我们来继续学习接口,学习接口的高阶用法。接口有一小部分知识与类的知识相关,所以我们放在讲解类的小节后面补充讲解,我们先来学习除了这一小部分之外剩下的接口的知识。 2.7.1 索引类型 我们可以使用接口描述索引的类型和通过索引得到的值的类型,比如一个数组[‘a’, ‘b’],数字索引0对应的通过索引得到的值为’a’。我们可以同时给索引和值都设置类型,看下面的示例: interface RoleDic { [id: number]: string; } const role1: RoleDic = { 0: "super_admin", 1: "admin" }; const role2: RoleDic = { s: "super_admin", // error 不能将类型"{ s: string; a: string; }"分配给类型"RoleDic"。 a: "admin" }; const role3: RoleDic = ["super_admin", "admin"]; 上面的例子中 role3 定义了一个数组,索引为数值类型,值为字符串类型。 你也可以给索引设置readonly,从而防止索引返回值被修改。 interface RoleDic { readonly [id: number]: string; } const role: RoleDic = { 0: "super_admin" }; role[0] = "admin"; // error 类型"RoleDic"中的索引签名仅允许读取 这里有的点需要注意,你可以设置索引类型为 number。但是这样如果你将属性名设置为字符串类型,则会报错;但是如果你设置索引类型为字符串类型,那么即便你的属性名设置的是数值类型,也没问题。因为 JS 在访问属性值的时候,如果属性名是数值类型,会先将数值类型转为字符串,然后再去访问。你可以看下这个例子:...

March 13, 2024 · zlzong

10-1 C++ 函数可以直接返回一个对象吗?

内存和资源管理是 C++ 最强的能力之一,也是 C++ 最复杂和最需要思考的地方。写 Java 的时候,我们只需要无脑地把所有对象都 new 出来。反正所有的对象只能放在堆区,又反正又垃圾回收器帮我们管理内存。然而,在 C++ 中,我们需要思考是把对象放在栈上,还是用 new 把对象放在堆上。默认情况下,对象会放在栈上,这样的好处是我们不会忘记释放对象的内存而造成内存泄漏。不过如果我们把一个大对象放在栈上,又将其作为参数或者返回值传递,就必须要考虑对象拷贝的开销了。C++ 由于和 C 兼容,默认情况下参数是按值传递 (call by value) 的,在传递参数和返回值的时候都会拷贝一遍对象。对于参数,我们尚可以将参数声明为引用类型 T& 来避免对象拷贝。而对于返回值的拷贝开销,则是不能声明为引用类型来解决的。 何时必须返回一个对象 假设我们想写一个 range 函数: vector<int> range(int begin, int end, int step=1) { vector<int> res; for (int i = begin; i < end; i += step) { res.push_back(i); } return res; } 这段代码会返回一个 vector<int> 对象,也就是我们不希望看到的:放在栈上的大对象。调用这个函数会产生返回值的临时对象,从而需要拷贝列表中的所有元素。很显然,你不能直接把返回值类型改成 vector<int>& 来避免对象拷贝——编译器会产生一个警告:warning: reference to local variable ‘res’ returned,你返回了一个临时变量的引用,这个引用指向了一个栈上的地址,而这个地址随时可能被回收。这也是 C++ 初学者容易犯的一个错误。既然不能返回一个引用,又想避免对象拷贝的开销,很多“老” C++ 程序员会进行一个人肉优化:把返回值作为引用参数传进去。按这种方法,range() 函数可以改写如下: vector<int> range(vector<int>& out, int begin, int end, int step=1) { for (int i = begin; i < end; i += step) { out....

March 13, 2024 · zlzong